Puppeteer란?
- Puppeteer는 DevTools 프로토콜을 통해 Chrome 또는 Chromium을 제어하는 고급 API를 제공하는 노드 라이브러리입니다. Puppeteer는 기본적으로 헤드리스로 실행되지만 전체(헤드리스가 아닌) Chrome 또는 Chromium을 실행하도록 구성할 수 있습니다.
- 위의 정의는 공식문서를 번역한 것입니다.
- 개인적으로는 “크롬을 이용한 UI 테스트 툴” 이라고 정의했고, 간단하게 SPA 크롤링하기 좋은 툴! 이라고 생각한다.
언제, 왜 사용했나?
- 국비지원 학원에서 마지막 프로젝트때 필요한 raw data를 얻기 위해서 몇몇 사이트들을 크롤링 할 필요가 있었다.
- 과거에 댓글 크롤링할때 사용했던 python을 이용할까 하다가 Javascript를 배우기도 했고, 요즘 nodeJS 모듈이 정말 정말 많다고 들어서 이것저것 검색을 했다.
- 구글에 크롤링 검색 시 당연하게도 처음으로 python, 그리고 js는 보통 cheerio + axios를 이용한 방법이 제일 많이 나왔다.
- 그래서 한번은 해본 python 보다는(이미 다 까먹었지만..) 새롭게 js를 이용해서 할 생각으로 node 설치 후 cheerio + axios 조합으로 크롤링을 하기로 했다.
SPA라는 난관에 봉착했다.
- 당연히 cheerio + axios 정도면 충분하다고 생각했는데 나의 너무 큰 오산이었다.
- 과거에 python으로 했을 때는 SPA/SSR이 그렇게 많지 않았는데 요즘 트렌트가 SPA였어서 html 태그를 검색하는 것보다 버튼을 눌러서 새로운 데이터나 사진을 변경해서 읽는 부분이 너무 많았다.
- 그래도 다들 이 방법으로 했으니 나도 되겠지..하면서 시도를 하다가 반복되는 것도 너무 심하고 약간 아니라는 생각도 들어서 SPA용으로 사용할 수 있는 라이브러리를 찾았다.
- 그러다가 찾은 것이 puppeteer였다.
SPA 컨트롤은 어느정도 가능했다. 하지만…
- 어떤 라이브러리나 api도 장점과 단점이 있듯이 puppeteer가 SPA를 컨트롤 하기에는 좋았지만 몇가지 단점도 존재했다.
- 첫번째. 조금 마이너한 라이브러리이다보니 한국어로 된 자료가 엄청 많지는 않았다.
- 두번째. 개인적으로 사용한 경험으로는 뭔가 정확히 작동되는 것 같지가 않았다. 예를 들어서 버튼 클릭을 해야되는 경우가 제일 많았는데, 분명히 사용하라고 만들어둔 메서드가 제대로 동작을 안하는 일이 발생했다.
- 물론 결국 우리의 친구 stackoverflow에서 잘 동작하는 클릭 코드를 찾아내서 사용했지만 라이브러리 자체에서 제공하는 부분이 조금 미흡하다는 생각이 들었다.
- 거기에 나의 숙련도가 낮은 것도 이유였다.
- 여튼 그래도 이것저것 잘 만져서 잘 사용해서 원하는 부분의 크롤링은 성공적으로 끝냈다.
기본 사용법
const puppeteer = require(“puppeteer”);
//모듈 선언
function runCrawl() {
const browser = await puppeteer.launch({ headless: true }); // 1
const page = await browser.newPage(); // 2
await page.setViewport({ width: 1920, height: 1080 }); // 3
1
2
3
4
5
6
7
8
9
10
11
| await page.goto("url"); // 4
/*
크롤링 코드
...
*/
page.close(); // 5
browser.close(); // 6 } ``` - 주석 설명 1. 브라우저를 실행시킨다. (headless:) 는 true면 헤드리스, false면 브라우저가 실행된다. 처음 사용할때나 어떻게 동작하는지 보고 싶다면 false로 두면 된다. 2. 새로운 탭 실행 3. 새로운 탭의 크기 설정 4. 해당 url로 이동 5. 탭 닫기 6. 브라우저 종료
|
- 기본적인 동작코드는 이런 방식이고 중앙의 크롤링 코드 부분에서 크롤링을 하면 된다.
사용한 함수
element.evaluate((arg) => arg.click());
1
2
| const btn = await page.waitForSelector(accoSelector.SELLER_INFO.BUTTON);
await btn.evaluate((b) => b.click());
|
- 단순히 클릭하는 코드이다. 버튼이나 a태그 div 등 거의 모든 element를 클릭할 수 있다.
- !!!!!다만 사실 이게 제일 중요한 함수다!!!!!
- 별표 5개도 안아깝다.. 진짜 이거만 써도 거의 모든 동작을 다 할 수 있다.
- SPA를 컨트롤하려고 puppeteer를 사용하는 건데 처음에는 ElementHandle을 반환해주고 내부적으로 아래 코드의 클릭만을 사용했다.
1
2
3
4
5
6
7
8
| // 초기 페이지에서 3번 클릭해서 넘겨야 하는 부분이 있었다.
arg.click();
arg.click();
arg.click();
// await arg.click();
// await arg.click();
// await arg.click();
|
- 당연히 될줄 알았는데 안됐다. 그래서 비동기 문제인가 싶어서 아래 주석과 같이 await를 붙였는데도 안됐다. (click의 반환값은 promise다)
- 이 문제로 3번 클릭하면 되는 걸 거의 10번클릭하는 코드를 사용해서라도 해결해보려 했지만 실패했다.
- 다행히도 추후에 위의 evaluate(click) 코드를 사용해서 문제를 해결했다.
page.$$
1
2
| // page 내에서 selector를 찾아서 배열로 반환해준다.
await page.$$(selector);
|
1
2
3
4
5
6
7
8
| <!-- 현재 페이지에 아래 같은 html 태그가 존재할때 -->
<div>
<ul>
<li class="pupp"></li>
<li class="pupp"></li>
<li class="pupp"></li>
</ul>
</div>
|
1
2
| // elements는 pupp 클래스 element를 3개 가진 배열이다.
const elements = await page.$$('div > ul > li.pupp');
|
- 참고로 puppeteer를 사용하면 ElementHandle 같이 puppeteer에서 정의한 래퍼로 감싸서 반환해준다.
element.$eval
1
2
3
4
5
| const elements = await page.$$(selector);
for (const element of elements) {
const iconName = await element.$eval(attr, (el) => el.getAttribute("alt"));
}
|
1
2
| // puppeteer로 찾은 element에서 속성을 검색할때 사용한다.
element.$eval(attr, (el) => el.getAttribute("alt"));
|
page.$
1
2
| const numberOfPictures = await page.$(numberOfPictureSelector);
const count = await page.evaluate((el) => el.textContent, numberOfPictures);
|
- page.$는 document.querySelector과 같다고 보면된다.
- page.evaluate 첫번째는 evaluate 내부에서 javascript 코드를 사용할 수 있고 두번째는 위의 방식처럼 func, arg 로 사용할 수 있다.
page.waitForTimeout
1
| await page.waitForTimeout(200);
|
- 메서드명만 봐도 알 수 있듯이 sleep같은 메서드이다.
- 공식문서에 이제는 사용하지 않고
new Promise(r => setTimeout(r, milliseconds));
을 추천하고 있다.
page.$x
1
2
3
| const savePicture = async (page, index, title) => {
const [target] = await page.$x(`//*[@id='${index}']/div/span/img`);
};
|
getProperty(“attribute”), jsonValue();
1
2
3
4
5
6
7
8
| const savePicture = async (page, index, title) => {
const [target] = await page.$x(`//*[@id='${index}']/div/span/img`);
const src = await target.getProperty("src");
const image = await src.jsonValue();
makeFolder(`${__dirname}\\..\\lowData\\${title}\\images`);
await download(image, `${__dirname}\\..\\lowData\\${title}\\images\\image${index}.jpg`);
};
|
- XPath를 이용해서 가져온 객체에서 attr을 찾고 그 값을 가져오는 코드다.
- 솔직하게 말해서 이 부분은 스택오버플로에서 가져온 코드라서 내용은 알겠는데 각 메서드의 정확한 기능은 모르겠다.
사용후기
- 처음에는 당연하지만, 문법을 몰라서 헤메는 부분이 많았는데 사용하다보니 은근히 사용법이 쉬워서 나중에는 이렇게 이렇게 하면 되겠지~ 라는 생각이 많이 들고 selector 때문에 코드가 지저분 해지는 것 말고는 꽤 잘 사용했다.
- 버전이 조금 더 업그레이드 되면 더 사용하기 좋아질 것 같다.
Leave a comment