코딩/expressJs

퍼펫티어와 cheerio 간단하게 익히기 : 스크래핑을 해보자

김 숨 2022. 6. 29. 23:08

퍼펫티어(Puppeteer)

번역하면 인형극 이라고 한다.

Chrome 팀이 개발한 Node 라이브러리 Headless chrome을 제어 할 수 있다. 고로  puppeteer 를 사용하기 위해서는 Chrome 이나 Chromium 이 필요하다.

https://developer.chrome.com/docs/puppeteer/

 

Puppeteer - Chrome Developers

Puppeteer is a Node library which provides a high-level API to control headless Chrome or Chromium over the DevTools Protocol. It can also be configured to use full (non-headless) Chrome or Chromium.

developer.chrome.com

Headless Browser가 뭔데?

CLI (Command Line interface) 에서 작동하는 브라우저이다.
일반적인 브라우저와는 다르게 백그라운드에서 동작하며, 일반적인 브라우저와 같이 웹페이지에 접속하여 HTML, CSS 으로 DOM Tree 와 CSSOM Tree 를 만들고, JS 엔진을 구동한다.
유일한 차이점은 GUI인 일반 브라우저와는 다르게 만든 화면을 사용자에게 보여주지 않는다는 점이다.
보여주는 화면 없이도, 화면 테스트나 스크린샷을 찍는것등 다양한 기능 동작이 가능하며, 사용자가 실제 사용하는 환경과 비슷하게 테스트가 가능하다.

 

퍼펫티어로 할 수 있는 것들

  • 페이지의 스크린샷과 PDF를 생성
  • SPA(단일 페이지 애플리케이션)를 크롤링하고 사전 렌더링된 콘텐츠("SSR"(서버 측 렌더링))를 생성합니다.
  • 양식 제출, UI 테스트, 키보드 입력과 같은 작업을 자동화합니다.
  •  최신 JavaScript 및 브라우저 기능을 사용하여 최신 버전의 Chrome에서 테스트가 가능하다
  • 성능 문제를 진단하는 데 도움이 되도록 사이트의 타임라인 추적을 캡처(브라우저 상의 콘솔 로그와 network 응답,실패 등도 모니터링이 가능)
  • Chrome 확장 프로그램을 테스트
  • mocha와 더불어 자동화 테스팅도 가능하다!

예제 코드들은 여기서 확인이 가능하다

https://github.com/puppeteer/puppeteer

 

GitHub - puppeteer/puppeteer: Headless Chrome Node.js API

Headless Chrome Node.js API. Contribute to puppeteer/puppeteer development by creating an account on GitHub.

github.com

 

이번에는 특정 사이트에서 메뉴를 클릭하여 전시회 정보를 갖고 와봤다.

 

 1) cheerio를 통해 html의 태그를 파싱하여 데이터를 갖고 온다. 
 2) puppeteer로 브라우저를 열고, 특정 태그의 데이터 갖고오기, 페이지 이동을 수행한다.


const express = require('express');
const puppeteer = require('puppeteer');
const router = express.Router();
router.use(express.json());
const cheerio = require('cheerio');
const url = 'http://ticket.interpark.com/TPGoodsList.asp?Ca=Eve&SubCa=Eve_O&tid4=Eve_O';


async function printcontent(content) {
    let trList = [];
    const $ = cheerio.load(content);
    const $bodyList = $("div.stit tbody").children("tr");
    let data = [];
    $bodyList.each(function (i, elem) {

        let date_text_location = $(this).find('td.Rkdate').text().replace(/[\n\t]/g, '').indexOf('20');
        let date = $(this).find('td.Rkdate').text().replace(/[\n\t]/g, '').substring(date_text_location);
        let sub_date = date.split("~");

        let start_date = sub_date[0];
        let end_date = sub_date[1];

        //위치랑 날짜가 같은 클래스 Rkdate를 사용하고 있어서 date를 추출할 때 값이 두개가 같이 불러옴
        // 20으로 시작하는 문자열의 시작점을 추출해서 그 문자열부터 출력하는 substring을 사용했는데... 더 효율적인 방법이 없을까?
        trList[i] = {
            title: $(this).find('td.RKtxt span.fw_bold a').text(),
            location: $(this).find('td.Rkdate a').text(),
            image_url: $(this).find('td.RKthumb a img').attr('src'),
            image_alt: $(this).find('td.RKthumb img').attr('alt'),
            start_date: start_date, // 불필요한 \n과 \t 제거
            end_date: end_date
        };
        data.push([trList[i]['title'], trList[i]['location'], trList[i]['image_url']
            , trList[i]['image_alt'], trList[i]['start_date'], trList[i]['end_date']]);//.filter(n => n.title);
    });

}

async function test() {

    const browser = await puppeteer.launch({ headless: false });
    const page = await browser.newPage();
    await page.goto(url);
    await page.waitFor(10000);

    const content = await page.content();
    await printcontent(content);
    let i = 2;
    for (i = 2; i < 6; i++) {

        let page_now = await page.$("div.box > dl.lt > dd:nth-child(" + i + ") a");
        //이동할 메뉴의 url 추출함
        let page_now_href = await page.evaluate(anchor => anchor.getAttribute('href'), page_now);
        //메뉴 이동 
        await page.goto(page_now_href);
        page.waitForNavigation();
        let page_now_content = await page.content();
        await page.waitFor(10000);
        //동작반복
        await printcontent(page_now_content);
    }
    browser.close();
}

test()
    .then(() => {
        console.log("success"); // Success!
    })
    .catch(e => { console.log(e); });

 

다음에는 퍼펫티어를 이용한 테스팅을 해봐야겠다.