금일 달성 항목
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 말고 다른 방식으로 크롤링해야할 것 같았습니다.
[알게된 점]
너무 큰 사이트를 크롤링하면 막힐 가능성이 높아 조심해야한다는걸 알았습니다.
'혼자 고민해보기_ 개발 > TIL (Today I Learned)' 카테고리의 다른 글
20230901(금)_ 최종프로젝트 진행 (0) | 2023.09.02 |
---|---|
20230831(목)_ 최종프로젝트 진행 (0) | 2023.09.02 |
20230829(화)_ 최종프로젝트 진행 (0) | 2023.08.29 |
20230828(월)_ 최종프로젝트 진행 (0) | 2023.08.28 |
20230825(금)_ 최종프로젝트 진행 (0) | 2023.08.25 |