puppeteer 시작하기

간단한 페이지의 웹 크롤링은 axios, cheerio를 사용해서 처리할 수 있지만, 최근 spa 페이지로 개발되는 웹 환경과 웹크롤러 접근을 막는 일부 브라우저들이 발생함에 따라 웹 크롤링을 위해 만들어진 라이브러리가 필요해졌다. 이때 사용하는 것이 puppeteer이다. puppeteer로는 userAgent를 bot이 아닌 일반 브라우저로 속여서 접근이 가능하다. (axios에서도 가능은 함)

> npm i puppeteer

puppeteer는 패키지를 다운받을 때 크로미움 브라우저를 함께 다운받는데, 이는 서버에서 크롬 브라우저를 실제 실행하여 처리하기위해 다운받아지는 것으로, 용량을 좀 차지한다는 점 참고하자(메모리 1G정도)

이제 puppeteer를 실제 코드에 적용해보자.

index.js

const parse = require("csv-parse/lib/sync");
const fs = require("fs");
const puppeteer = require("puppeteer");

const csv = fs.readFileSync("csv/data.csv");
const records = parse(csv.toString("utf-8"));

const crawler = async () => {
  const browser = await puppeteer.launch({ headless: false }); // browser 객체 생성
  const page1 = await browser.newPage();
  const page2 = await browser.newPage();
  const page3 = await browser.newPage();
  await page1.goto("<https://github.com/wonieeVicky>");
  await page2.goto("<https://www.naver.com>");
  await page3.goto("<https://www.google.com>");
  await page1.waitForTimeout(3000); // 3초 대기
  await page2.waitForTimeout(1000); // 1초 대기
  await page3.waitForTimeout(2000); // 1초 대기
  await page1.close(); // 페이지 Close
  await page2.close(); // 페이지 Close
  await page3.close(); // 페이지 Close
  await browser.close(); // 브라우저 Close
};

crawler();

위와 같이 처리한 뒤 npm start를 하면 자동으로 차례대로 page1, page2, page3을 방문 한 뒤 정해진 대기 시간이 지나면 페이지가 close되고, 브라우저까지 최종 종료되는 것을 확인할 수 있다.

headless 옵션 이해하기

puppeteer.launch({ headless: false }); 옵션에서 headlesstrue로 하면 어떻게 될까? headless는 화면을 의미한다. 화면이 없는 브라우저를 사용하는 것을 의미한다.

그런데 왜 화면이 없는 브라우저를 써야하는 걸까? 크롤러가 움직이는 환경은 특정 서버이다. 서버는 보통 명령 크롬프트로 움직이는 것이 대부분이며 눈에 보이지 않기 때문에 최대한 코드로만 조작하는 것을 의미한다.

따라서 개발 시에만 headlessfalse로 두고, 이외에는 별도의 옵션을 주지않으면 true로 처리되므로 서버에서 동작시키면 된다. 만일 headless 상태에서 디버깅을 하고 싶을 경우 console.log를 사용하면된다.

//..
const crawler = async () => {
	const browser = await puppeteer.launch({ headless: process.env.NODE_ENV === "production" });
  const page1 = await browser.newPage();
  const page2 = await browser.newPage();
  const page3 = await browser.newPage();
  await page1.goto("<https://github.com/wonieeVicky>");
  await page2.goto("<https://www.naver.com>");
  await page3.goto("<https://www.google.com>");
	console.log('working');
  await page1.close(); // 페이지 Close
  await page2.close(); // 페이지 Close
  await page3.close(); // 페이지 Close
  await browser.close(); // 브라우저 Close
};

crawler();
> npm start

> [email protected] start
> node index

working

또 현재 구조상 page1~page3까지 await 속성에 따라 순차적으로 방문이 진행되므로 이는 동시다발적인 진행이라고 보기 어렵다. 이러한 구조를 개선하기 위해 Promise.all 을 사용하여 처리한다.

const crawler = async () => {
  const browser = await puppeteer.launch({ headless: process.env.NODE_ENV === "production" });
  const [page1, page2, page3] = await Promise.all([
		browser.newPage(), 
		browser.newPage(), 
		browser.newPage()
	]);
  await Promise.all([
    page1.goto("<https://github.com/wonieeVicky>"),
    page2.goto("<https://www.naver.com>"),
    page3.goto("<https://www.google.com>"),
  ]);
  await Promise.all([
		page1.waitForTimeout(3000), 
		page2.waitForTimeout(500), 
		page3.waitForTimeout(2000)
	]);
  await page1.close();
  await page2.close();
  await page3.close();
  await browser.close();
};

위처럼 처리하면 순차처리가 아닌 동시 처리로 변경해줄 수 있다.

첫 puppeteer 크롤링

puppeteer 옵션을 어느정도 알아봤다면 실제 puppeteer를 사용해 크롤링을 구현해본다.

const parse = require("csv-parse/lib/sync");
const fs = require("fs");
const puppeteer = require("puppeteer");

const csv = fs.readFileSync("csv/data.csv");
const records = parse(csv.toString("utf-8"));

const crawler = async () => {
  // try ~ catch는 async 함수 내부에서 사용하여 에러를 잡는다.
  try {
    const browser = await puppeteer.launch({ headless: process.env.NODE_ENV === "production" });
		// Promise.all로 동시 크롤링 
    await Promise.all(
      records.map(async (r, i) => {
        // 내부 async 함수에 대한 try ~ catch 적용
        try {
          const page = await browser.newPage(); // 동시에 페이지 10개 오픈
          await page.goto(r[1]); // 동시에 페이지 방문
          const scoreEl = await page.$(".score.score_left .star_score"); // 별점 엘리먼트로 이동
          if (scoreEl) {
            // 태그를 잘 찾았으면 evaluate 함수를 통해 찾은 태그로 textContent를 반환
            const text = await page.evaluate((tag) => tag.textContent, scoreEl); // 평점
            // console.log(r[0], "평점", text.trim());
          }
          await page.waitForTimeout(3000); // 웹크롤러 방지 코드에 걸리지 않도록 처리
          await page.close();
        } catch (e) {
          console.error(e);
        }
      })
    );
    await browser.close(); // 브라우저 Close
  } catch (e) {
    console.error(e);
  }
};

crawler();