혼자 고민해보기_ 개발/TIL (Today I Learned)

20230830(수)_ 최종프로젝트 진행

nuri-story 2023. 8. 31. 23:18

금일 달성 항목

1)  puppeteer로 채용공고 데이터 크롤링 하기


문제 해결 과정 1 - 채용공고 데이터 크롤링, 상세페이지 정보 가져오기

[문제]

채용정보 리스트는 쉽게 가져왔으나 상세페이지에 진입해서 회사 정보를 가져와야하는 상황이었습니다. 

 

[시도 및 해결]

잡코리아에서 크롤링하려고 했는데, 자꾸만 잡코리아 측에서 차단을 당했습니다.

리스트 자체는 잘 크롤링 해왔는데 상세페이지는 잡코리아측에서 막는 것 같습니다.

그래서 어쩔 수 없이 faker로 더미 데이터로 가져와야하는 상황이었습니다...

import puppeteer from "puppeteer";
import cheerio from "cheerio";
import { Injectable } from "@nestjs/common";
import { Cron } from "@nestjs/schedule";
import { Jobposting } from "src/domain/jobposting.entity";
import { InjectRepository } from "@nestjs/typeorm";
import { Repository } from "typeorm";
import { Company } from "src/domain/company.entity";
import { faker } from "@faker-js/faker";

@Injectable()
export class JobcrawlerService {
  constructor(
    @InjectRepository(Jobposting)
    private readonly jobpostingRepository: Repository<Jobposting>,
    @InjectRepository(Company)
    private readonly companyRepository: Repository<Company>
  ) {}

  // 퍼피티어 데이터 기본 실행
  private async getPuppeteerData(url: string): Promise<string> {
    // 브라우저를 실행한다.
    // 옵션으로 headless모드를 끌 수 있다.
    const browser = await puppeteer.launch({
      headless: "new",
    });
    // 새로운 페이지를 연다.
    const page = await browser.newPage();
    // 페이지의 크기를 설정한다
    await page.setViewport({
      width: 1366,
      height: 768,
    });
    // 초수를 무제한으로 설정한다
    page.setDefaultNavigationTimeout(0);

    //URL에 접속한다
    await page.goto(url);
    // 콘텐츠를 실행하고
    const content = await page.content();
    // 브라우저를 종료한다
    browser.close();

    return content;
  }


  jobParsing(page) {
    const $ = cheerio.load(page);
    const jobs = [];
    // 채용공고 크롤링
    const $jobList = $(".devloopArea");

    $jobList.filter((idx, node) => {
      // 회사 아이디
      const dataInfo = $(node).attr("data-info"); // data-info 속성 값 가져오기
      const companyId = dataInfo.split("|")[0].trim() || 1; // '|'로 분리된 값 중 첫 번째 값을 가져옵니다

      // 회사이름
      const companyTitle = $(node)
        .find(".tplCo > a.link.normalLog")
        .text()
        .trim();

      // 채용공고 타이틀
      const title = $(node)
        .find(".titBx > strong > a.normalLog:eq(0)")
        .text()
        .trim();
      if (title === "") {
        return false;
      }

      // 경력
      const career = $(node)
        .find(".titBx > .etc:eq(0) > .cell:eq(0)")
        .text()
        .trim();

      // 급여 관련 // 공백은 '면접 후 결정'으로 변경
      const salary =
        $(node)
          .find(".titBx > .etc:eq(0) > .cell:eq(4)")
          .text()
          .trim()
          .replace(
            /\s*주임급\s*|\s*사원급 외\s*|\s*사원급\s*|\s*주임~대리급\s*/g,
            ""
          ) || "면접 후 결정";

      // 학력
      const education = $(node)
        .find(".titBx > .etc:eq(0) > .cell:eq(1)")
        .text()
        .trim();

      // 정규직 관련
      const workType = $(node)
        .find(".titBx > .etc:eq(0) > .cell:eq(3)")
        .text()
        .trim()
        .replace(/\s*외\s*/g, ""); // 여백과 "외" 문자열을 제거
      // 지역
      const workArea = $(node)
        .find(".titBx > .etc:eq(0) > .cell:eq(2)")
        .text()
        .trim();

      // 기타 내용
      const content = $(node).find(".titBx > .dsc:eq(0)").text().trim();

      // 채용 마감일
      const dueDate =
        $(node).find(".odd > .date:eq(0)").text().trim() || "상시 채용";

      const jobposting = {
        companyTitle,
        companyId: Number(companyId),
        title,
        career,
        salary,
        education,
        workType,
        workArea,
        content,
        dueDate,
      };

      jobs.push(jobposting);
    });
    console.log(jobs);
    return jobs;
  }

  // @Cron('0 8 * * *') // 매일 오전 8시에 실행
  async crawlJobs() {
    let jobInfo: {
      companyTitle: string;
      companyId: number;
      title: string;
      career: string;
      salary: string;
      education: string;
      workType: string;
      workArea: string;
      content: string;
      dueDate: string;
    }[] = [];
    const companyLists = [];

    console.time("코드 실행시간");

    let i = 1;
    while (i <= 1) {
      const jobpostingUrl = `https://www.jobkorea.co.kr/recruit/joblist?menucode=local&localorder=1#anchorGICnt_${i}`;
      const jobpostingContent = await this.getPuppeteerData(jobpostingUrl);
      jobInfo = this.jobParsing(jobpostingContent);
   
      i++;

    }

    console.timeEnd("코드 실행시간");

    const jobpostingEntities: Jobposting[] = [];

   
    for (let job of jobInfo) {
      let company = await this.companyRepository.findOne({
        where: { title: job.companyTitle },
      });
      console.log(company);
      if (!company) {
        company = await this.companyRepository.save({
          email: faker.internet.email(),
          password: faker.internet.password(),
          title: job.companyTitle,
          introduction: faker.lorem.paragraph(),
          business: faker.lorem.paragraph(),
          employees: faker.lorem.paragraph(),
          image: faker.lorem.paragraph(),
          website: faker.lorem.paragraph(),
          address: faker.lorem.paragraph(),
        });
      }
      const jobEntity = this.jobpostingRepository.create({
        company: { id: company.id },
        companyId: company.id,
        title: job.title,
        career: job.career,
        salary: job.salary,
        education: job.education,
        workType: job.workType,
        workArea: job.workArea,
        content: job.content,
        dueDate: job.dueDate,
      });
      this.jobpostingRepository.insert(jobEntity);
    }
   
    return jobInfo;
  }
 
}

또한 문제가 puppeteer가 엄청 느렸습니다. 크롤링하는데 너무 오랜 시간이 걸렸습니다..

그래서 puppeteer 말고 다른 방식으로 크롤링해야할 것 같았습니다. 

 

[알게된 점]

너무 큰 사이트를 크롤링하면 막힐 가능성이 높아 조심해야한다는걸 알았습니다.