금일 달성 항목
1) puppeteer로 채용공고 데이터 크롤링 하기
문제 해결 과정 1 - 채용공고 데이터 크롤링, 상세페이지 정보 가져오기
[문제]
채용공고 크롤링은 성공하였으나, 각각의 채용공고를 작성한 회사의 정보를 가져오기 위해서는 상세페이지에 진입해서 가져와야하는 상황이었습니다.
[시도 및 해결]
1. 튜터님에게 방향성을 여쭈어보았습니다.
첫번째 방법은, Puppeteer를 통해 리스트url을 통해 채용공고 데이터를 가져오고 각각의 채용공고를 선택해서 진입하여 상세페이지 정보를 가져오는 것입니다.
두번째 방법은, 상세페이지에 진입하면 가지고 있는 회사의 ID를 (data-info)를 가져와서 매칭시키는 방법이었습니다.
추가로 데이터를 Pasing할때 너무 빠른 속도로 가져오게되면 블락을 당할 수 있으니
2. 채용리스트를 가져오는 nest.js코드는 그대로 두고 간단하게 테스트하기 위해서
axios와 cheerio로 간단하게 구현하여 해당 데이터를 가져왔습니다.
const axios = require("axios");
const cheerio = require("cheerio");
const faker = require("faker");
// 상세페이지 테스트
const getHTML = async (keyword) => {
try {
const html = (
await axios.get(
` https://www.jobkorea.co.kr/Recruit/GI_Read/${encodeURI(
keyword
)}?rPageCode=AM&logpath=21`
)
).data;
return html;
} catch (e) {
console.log(e);
}
};
const parsing = async (page) => {
const $ = cheerio.load(page);
const jobs = [];
const $jobList = $(".artReadCoInfo");
$jobList.each((idx, node) => {
// 이메일
const email = faker.internet.email(); // 랜덤한 이메일
// 비밀번호
const password = faker.internet.password(); // 랜덤한 패스워드 생성
// 회사명
const title = $(node).find(".tbRow > .coInfo > h4.hd_4").text().trim();
const introduction = faker.lorem.paragraph();
const business = $(node)
.find(".tbList dt:contains('산업')")
.next("dd")
.text()
.trim();
const employees = $(node)
.find(".tbList dt:contains('사원수')")
.next("dd")
.text()
.trim();
const image = "http:" + $(node).find(".coLogo img").attr("src");
console.log(image);
jobs.push({
email, //랜덤 생성
password, //랜덤 생성
title, //회사명
introduction, //산업
business, // 뭐하는 곳인지
employees, // 사원 수
image, // 회사 이미지
});
});
const $companyLocation = $(".artReadWork");
$companyLocation.each((idx, node) => {
const address = $(node).find(".address > strong.girIcn").text().trim();
jobs.push({
address,
});
});
const $companyWebsite = $(".artReadJobSum");
$companyWebsite.each((idx, node) => {
const websiteLabel = $(node)
.find(".tbList dt")
.filter(function () {
return $(this).text().trim() === "홈페이지";
});
const website = websiteLabel
.next()
.find("a.devCoHomepageLink")
.attr("href");
jobs.push({
website,
});
});
return jobs;
};
const getJob = async (keyword) => {
const html = await getHTML(keyword);
const jobs = await parsing(html);
// console.log(jobs);
return jobs;
};
// // 페이지 번호를 계속 바꿔주면서 추가해주는 것
// const getFullJob = async () => {
// let jobs = [];
// let i = 42725747;
// while (i <= 42725748) {
// const job = await getJob(
// `https://www.jobkorea.co.kr/Recruit/GI_Read/${i}?rPageCode=AM&logpath=21`
// );
// jobs = jobs.concat(job);
// i++;
// }
// console.log(jobs.length);
// };
// getFullJob();
getJob(42714430);
여기서 문제는 화면안에 필요한 데이터들이 흩어져있어 저런식으로 가져온 데이터들을 company라는 것에 하나로 묶어야하는 상황이었습니다. (웹사이트, 주소는 다른 html 영역에 있었음)
const $companyLocation = $(".artReadWork");
$companyLocation.each((idx, node) => {
const address = $(node).find(".address > strong.girIcn").text().trim();
jobs.push({
address,
});
});
const $companyWebsite = $(".artReadJobSum");
$companyWebsite.each((idx, node) => {
const websiteLabel = $(node)
.find(".tbList dt")
.filter(function () {
return $(this).text().trim() === "홈페이지";
});
const website = websiteLabel
.next()
.find("a.devCoHomepageLink")
.attr("href");
jobs.push({
website,
});
});
그래서 동료분의 도움으로두개의 영역을 분할해서 다시 파싱하는 방법으로 진행하였습니다.
companyParsing(page: string, companyId: string) {
const $ = cheerio.load(page);
const companies: {
companyId: number;
password?: string; //랜덤 생성
title?: string; //회사명
introduction?: string; //산업
business?: string; // 뭐하는 곳인지
employees?: string; // 사원 수
image?: string;
website: string;
address: string;
}[] = [];
let company;
const $companyList = $('.artReadCoInfo');
$companyList.each((idx, node) => {
// 이메일faker.internet.email()
// const email = 'abcdefg@naver.com'; // 랜덤한 이메일
const email = faker.internet.email(); // 랜덤한 이메일
// 비밀번호
const password = faker.internet.password(); // 랜덤한 패스워드 생성
// 회사명
const title =
$(node).find('.tbRow > .coInfo > h4.hd_4').text().trim() || '회사 이름';
// 소개
const introduction = faker.lorem.paragraph();
// 산업
const business =
$(node).find(".tbList dt:contains('산업')").next('dd').text().trim() ||
'기타 산업';
// 사원수
const employees =
$(node)
.find(".tbList dt:contains('사원수')")
.next('dd')
.text()
.trim() || '0';
// 기업 이미지
const image =
'http:' + $(node).find('.coLogo img').attr('src') ||
'https://img.freepik.com/free-photo/central-business-district-in-singapore_335224-638.jpg';
company = {
companyId: Number(companyId),
email, //랜덤 생성
password, //랜덤 생성
title, //회사명
introduction, //산업
business, // 뭐하는 곳인지
employees, // 사원 수
image, // 회사 이미지
};
companies.push(company);
});
const $companyLocation = $('.artReadWork');
$companyLocation.each((idx, node) => {
const address =
$(node).find('.address > strong.girIcn').text().trim() || '주소';
companies[idx].address = address; //주소
});
const $companyWebsite = $('.artReadJobSum');
$companyWebsite.each((idx, node) => {
const websiteLabel = $(node)
.find('.tbList dt')
.filter(function () {
return $(this).text().trim() === '홈페이지';
});
const website =
websiteLabel.next().find('a.devCoHomepageLink').attr('href') ||
'웹 사이트';
companies[idx].website = website; //주소
});
console.log(company);
// console.log('채용 공고 크롤링 부분:', jobs);
// console.log('회사 크롤링 부분:', companies);
return companies;
}
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(); // '|'로 분리된 값 중 첫 번째 값을 가져옵니다
// 채용공고 타이틀
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 = {
companyId,
title,
career,
salary,
education,
workType,
workArea,
content,
dueDate,
};
jobs.push(jobposting);
});
console.log(jobs);
return jobs;
}
그 외에도 테스트를하는 과정에서 다양한 오류가 떴습니다.
companyId를 찾을 수 없다.
company.email 를 찾을 수 없다.
timeouterror: navigation timeout of 30000 ms exceeded = 30초를 초과했다.
error: Error: Field 'email' doesn't have a default value
[Nest] 58551 - 08/29/2023, 11:22:15 PM ERROR [ExceptionsHandler] Field 'email' doesn't have a default value
QueryFailedError: Field 'email' doesn't have a default value
at Query.onResult (/Users/hyerim/Desktop/sparta/12. jobposting-project/jobposting-project/src/driver/mysql/MysqlQueryRunner.ts:222:33)
at Query.execute (/Users/hyerim/Desktop/sparta/12. jobposting-project/jobposting-project/node_modules/mysql2/lib/commands/command.js:36:14)
at PoolConnection.handlePacket (/Users/hyerim/Desktop/sparta/12. jobposting-project/jobposting-project/node_modules/mysql2/lib/connection.js:478:34)
at PacketParser.onPacket (/Users/hyerim/Desktop/sparta/12. jobposting-project/jobposting-project/node_modules/mysql2/lib/connection.js:97:12)
at PacketParser.executeStart (/Users/hyerim/Desktop/sparta/12. jobposting-project/jobposting-project/node_modules/mysql2/lib/packet_parser.js:75:16)
at Socket.<anonymous> (/Users/hyerim/Desktop/sparta/12. jobposting-project/jobposting-project/node_modules/mysql2/lib/connection.js:104:25)
at Socket.emit (node:events:512:28)
at addChunk (node:internal/streams/readable:343:12)
at readableAddChunk (node:internal/streams/readable:316:9)
at Socket.Readable.push (node:internal/streams/readable:253:10)
아직 테스트 과정이라 제대로 파싱이 되지 않고 있지만, 성공하게 되면 다시 til을 올리겠습니다.
[알게된 점]
타입과 객체의 구조 등으로 불러오는 과정이 아직도 너무 미숙해서 실수가 많은 것 같습니다.
그리고 코드가 돌아가는 구조에 대해서도 아직도 이해가 부족하다고 느꼈고... 공부를 많이 해야할 것 같습니다.
'혼자 고민해보기_ 개발 > TIL (Today I Learned)' 카테고리의 다른 글
20230831(목)_ 최종프로젝트 진행 (0) | 2023.09.02 |
---|---|
20230830(수)_ 최종프로젝트 진행 (0) | 2023.08.31 |
20230828(월)_ 최종프로젝트 진행 (0) | 2023.08.28 |
20230825(금)_ 최종프로젝트 진행 (0) | 2023.08.25 |
20230824(목)_ 최종프로젝트 진행 (1) | 2023.08.25 |