티스토리 뷰
Puppeteer | Puppeteer
build
pptr.dev
GitHub - puppeteer/puppeteer: Node.js API for Chrome
Node.js API for Chrome . Contribute to puppeteer/puppeteer development by creating an account on GitHub.
github.com
-- puppeteer 크롤링을 고르게된 이유
python에서는 selenium이 유명하다
selenium은 크롬에서 뿐만 아니라 다른브라우저에서 사용가능하다.
node에서는 puppeteer라고 한다.
puppeteer는상대적으로 빠른 속도를 자랑한다.
하지만 크롬에서만 사용이 가능하다.
나의 상황
뉴스의 정보를 끌어와야함.
네이버로 치면 사회 탭의 사건사고 탭안에 있는 내용을 가져와야함.
여기에 puppeteer를 사용할 수 있다면 puppeteer를 사용하지 않을 이유가 없다.
왜냐.
셀레니움은 상대적으로 느리기도 하며 nodejs에서는 지원이 좋지 않다.
라이브러리가 있지만 작동안되는 기능들이 많기도 하고 상대적으로 업데이트가 느리다.
-- puppeteer와 puppeteer-core의 차이점
처음엔 본인의 생각으로 puppeteer-core는 puppeteer의 주요기능만을 가지고 왔다고 하여 훨씬 가볍고 성능도 빠르고 좋을것 같아서 사용을 했다.
하지만 사용을 하면서 여러 예제를 보면서 puppeteer와 core의 확연한 차이를 느낄수가 없었다.
그래서 찾아본 결과
puppeteer는 Chromium을 자동으로 다운로드하고 설치하는 편리한 기능을 제공한다고 한다.
즉 별도의 chrome을 설치할 필요가 없고 Chrome DevTools Protocal를 사용하여 Chrome브라우저의 기능을 조작한다.
puppeteer-core는 딱하나 다른점이 있다.
바로 chromium을 자동으로 설치하지 않는다. 대신에 로컬에 이미 설치된 chrome 또는 chromium을 사용한다.
즉, 로컬의 chrome을 사용하는가 를 기준으로 puppeteer와 puppeteer-core의 사용 기준을 정할수가 있고
chromium을 사용안하는 모듈은 라이브러리의 크기가 더 작을수 있기 때문에 그 기준으로 사용을 정할수 있다.
이렇게 알아보고 난뒤 어차피 자동으로 chromium을 제공한다고 하면 용량과 라이브러리가 크게 중요하지 않는 프로젝트라면 puppeteer를 사용할것 같다.
현재 프로젝트는 puppeteer-core를 사용하는데 추후에 성능상에 문제가 없다고 하면 puppeteer로 교체해도 무방할것 같다.
-- 맞이한 문제점 1
An `executablePath` or `channel` must be specified for `puppeteer-core`
해당 문제 발생.
해당 퍼피티어 코어에 채널을 지정해줘야 한다는 오류였다.
여기서 말하는 채널이란 무엇인가?
채널은 puppeteer의 내부 통신을 관리하는 것이다.
채널은 일종의 통신 파이프로 node.js와 브라우저 사이에서 메시지를 주고 받을 수 있게 한다.
즉 어느 어떤 브라우저로 연결할것인가? 를 나타내는 것이다.
let browser = await puppeteer.launch({
headless: false,
executablePath: executablePath('chrome'), // Puppeteer가 로컬 시스템에서 Chrome 브라우저를 찾아 실행
});
여기서는 chrome브라우저를 사용할 것 이기 때문에 해당 채널을 chrome으로 지정해주었다.
-- 맞이한 문제점 2
원하는 데이터들의 정보는 해당날짜에 게시된 뉴스게시물들을 가져오는것이다.
주체가 나라면 기사 더보기를 눌러서 가져오면 그만이지만 데이터를 가져오는 주체는 puppeteer이다.
우리는 puppeteer에게 기사 더보기를 끝까지 누르게 명령을 한다음 해당 날짜의 모든 게시글을 가져와야 한다.
async function clickMoreUntilEnd(page) {
while (true) {
await page.waitForSelector(
'.section_latest .section_more .section_more_inner',
); // 더보기 버튼이 로드될 때까지 기다림.
const moreButton = await page.$(
'.section_latest .section_more .section_more_inner',
); // 더보기 버튼 요소를 가져옴.
if (!moreButton) break; // 만약 더 이상 더보기 버튼이 없으면 종료.
await moreButton.click(); // 더보기 버튼을 클릭.
}
}
하지만 위 코드의 문제는 한번밖에 클릭을 하지 못한다는 것이다.
왜냐 하면 여기서 나타나는 에러가
Error: Node is either not clickable or not an HTMLElement
이 에러 였다.
이 문제는 노드가 클릭하려고 하는 요소가 없거나 클릭을 하고자 하는 요소가 클릭을 할수가 없을때 나오는 에러이다.
즉, puppeteer가 클릭을 하려고 하는 것보다 웹 페이지의 더보기가 더 느리게 나오는 것이다.
이를 해결하기 위해
await moreButton.evaluate((b) => b.click());
클릭 버튼을 위와 같이 변경했다.
위 코드를 설명하자면 puppeteer를 사용하여 특정 페이지 요소에 대해 JavaScript 함수(click())를 실행한다.
evaluate()메서드는 웹 페이지의 컨텍스트에서 JavaScript를 실행 할수 있도록 허용하며 JavaScript함수를 실행한다.
즉 위의 코드는 브라우저에서 해당 요소를 클릭하는 것과 동일한 동작을 수행한다.
이렇게 바꿔 실행하니
해당 날짜에 맞는 뉴스 게시글의 끝까지 더보기버튼을 눌러 진행한걸 확인 할수가 있었다.
-- 맞이한 문제점 3
위의 문제 2를 해결함과 동시에 나온 문제이다.
시간 초과가 되어 데이터를 가져 올수가 없었다.
어디 부분에서 시간 초과가 나는지 확인 해보기 위해
로그를 찍어가며 확인 한 결과
clickMoreUntilEnd함수를 빠져나오지 못하고 있었다.
어디가 문제인지 곰곰히 생각해 보았다.
심플하게 생각해보면 쉽게 접근해 볼 수가 있었다.
const moreButton = await page.$(
'.section_latest .section_more .section_more_inner',
); // 더보기 버튼 요소를 가져옴.
if (!moreButton) break; // 만약 더 이상 더보기 버튼이 없으면 종료.
더보기가 다 눌리는 것은 확인을 했고 데이터를 가져와서 가공하는것은 clickMoreUntilEnd()함수를 빠져나와야 가능한것이니
break조건이 안맞기 때문에 while문을 나오지 못하는것이 아닐까?
위 조건은 moreButton이 없을 경우에 해당 while문을 나가는 것이다.
하지만 뉴스 게시물 페이지에서 더보기 버튼이 없지만 완전히 없어진게 아니라 display:none으로 화면상에서만 안보이게 한것이였다.
어디가 원인인지만 알면 그 부분은 쉽게 해결을 할 수가 있다.
더보기가 없는 줄 알았지만 있었고 그 부분이 사람눈에만 안보이게 설정을 해놨던 것이 원인이라면 그 원인을 조건에 넣어주면 된다.
const endpoint = await page.$(
'.section_latest .section_more a[style="display: none;"]',
); // 화면에 보이지 않는 더보기 버튼 요소를 가져옴.
if (endpoint) break;
화면에 보이지 않는 더보기 버튼이 있다면 그때 while문을 탈출 하면된다.
데이터를 가져오는것을 성공 했다.
-- puppeteer 크롤링과정
async getNews(board: string) {
let browser = await puppeteer.launch({
headless: false,
executablePath: executablePath('chrome'),
});
try {
const page = await browser.newPage();
} finally {
await browser.close();
}
}
1. puppeteer.launch()
- 브라우저를 실행한다.
- headless : false는 브라우저가 로컬에서도 보이게 설정하는 옵션이며
true로 설정할시에는 화면이 보이지 않고 백그라운드에서 데이터 스크랩핑이 돌아가게 된다.
- executablePath('chrome')은 puppeteer가 사용할 브라우저의 실행 경로를 설정한다.
'chrome'같은 경우에는 로컬의 chrome을 자동으로 찾아서 사용하도록 puppeteer에 지시 한다.
2. await browser.newPage()
- 새로운 페이지를 생성하고 해당 페이지 객체를 반환한다. 이 객체를 통하여 웹페이지를 조작하는 것이다.
3. await browser.close()
- 어떤 일이 생기든지 작업이 끝나거나 오류가 생기거나 할때 브라우저인스턴스를 종료한다.
-- 결과
import { Injectable } from '@nestjs/common';
import puppeteer, { executablePath } from 'puppeteer-core';
@Injectable()
export class NewsService {
async getNews(board: string) {
let browser = await puppeteer.launch({
headless: false,
executablePath: executablePath('chrome'),
});
try {
const page = await browser.newPage();
page.setDefaultNavigationTimeout(2 * 60 * 1000); // 최대 탐색시간 밀리초
await Promise.all([
// await page.waitForNavigation(), //페이지가 다음 탐색을 완료할 때까지 대기하는 Puppeteer의 메서드, but 이 메서드 사용하면 "chrome이 자동화된 테스트 소프트웨어에 의해 제어되고 있습니다." 문구가 뜨면서 페이지 안뜸.
await page.goto('https://news.naver.com/breakingnews/section/102/249'),
await page.click('._CALENDAR_LAYER_TOGGLE'),
await page.click(`.calendar .calendar-day-${today()}`),
]);
await clickMorePageEnd(page);
// 페이지 내의 여러 요소를 선택하고 해당 요소들에 대해 사용자가 제공한 함수를 실행;
return await page.$$eval(
'.section_latest_article .section_article >ul >li .sa_item_inner .sa_item_flex .sa_text',
(resultItems) => {
return resultItems
.filter((resultItem) => {
const title = resultItem.querySelector('a').innerText;
const keywords = [
'사고','폭행','화재','낙뢰','지진','재난',
'홍수','폭발','둔기','살인','절도','충돌',
'칼부림','묻지마',
];
return keywords.some((keyword) => title.includes(keyword));
})
.map((resultItem) => {
const titleElement = resultItem.querySelector('a');
const textElement = resultItem.querySelector('.sa_text_lede');
const newsCompanyElement =
resultItem.querySelector('.sa_text_press');
const url = titleElement.href;
const title = titleElement.innerText;
const text = textElement ? textElement.textContent.trim() : ''; // 텍스트가 있는 경우에만 가져오고, 없는 경우 빈 문자열 반환
const newsCompany = newsCompanyElement
? newsCompanyElement.textContent.trim()
: '';
return { url, title, text, newsCompany };
});
},
);
} finally {
// await browser.close();
}
}
}
const today = () => {
const today = new Date();
const year = today.getFullYear();
const month = today.getMonth() + 1;
const strMonth = month < 10 ? '0' + month : month; // 한 자리 수의 월인 경우 앞에 0을 추가하여 두 자리로 만듭니다.
const day = today.getDate();
const strDay = day < 10 ? '0' + day : day; // 한 자리 수의 날짜인 경우 앞에 0을 추가하여 두 자리로 만듭니다.
const formattedDate = `${year}-${strMonth}-${strDay}`;
return formattedDate;
};
async function clickMorePageEnd(page) {
while (true) {
await page.waitForSelector(
'.section_latest .section_more .section_more_inner',
); // 더보기 버튼이 로드될 때까지 기다림.
const moreButton = await page.$(
'.section_latest .section_more .section_more_inner',
); // 더보기 버튼 요소를 가져옴.
const endpoint = await page.$(
'.section_latest .section_more a[style="display: none;"]',
); // 화면에 보이지 않는 더보기 버튼 요소를 가져옴.
if (endpoint) break; // 화면에 보이지 않는 더보기 버튼이 있다면 제일 끝이므로 while문 벗어나기
await moreButton.evaluate((b) => b.click()); // 더보기 버튼을 클릭.
}
}
원하는 정보를 원하는 만큼 가져올수가 있다.
'project > sparta' 카테고리의 다른 글
[최종 프로젝트]실시간 구조 요청 보내기1 (위치 기반 서비스) (0) | 2024.04.08 |
---|---|
[최종 프로젝트] Puppeteer 크롤링하기2<문자열 유사성 알고리즘> (0) | 2024.04.05 |
[최종 프로젝트] 개발일정 논의 및 기술 결정 (0) | 2024.03.28 |
바닐라 자바스크립트 조건에 따른 카드 정렬[팀프로젝트] (0) | 2024.01.12 |
TIL 10일차 + fetch(), Promise.all() (1) | 2024.01.05 |