본문 바로가기

BackEnd/Node.js

Puppeteer.js 를 이용한 웹 자동화

https://github.com/GoogleChrome/puppeteer

Puppeteer

Puppeteer는 node.js를 이용하여 웹 자동화를 도와주는 API이다.

정말 간단한 사용법에 비해, 엄청나게 파워풀한 기능들을 가지고 있다.

사용 브라우저의 경우 Chrome 시리즈인 Chromium를 이용해서 사용한다. 그렇기 때문에 서버에서 사용시에 굉장히 많은 메모리 소모가 되기 때문에, 여러 사람이 사용하기에는 적합한지는 생각해보아야 할 문제다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const puppeteer = require("puppeteer");
 
const doPuppeteer = async() => {
    const browser = await puppeteer.launch({
        headless : false
    });
    const page = await browser.newPage();
await page.waitFor(1000);
    await page.goto("https://www.naver.com");
    await page.waitFor(1000);
await page.goto("https://www.google.com");
    await page.close();
    await browser.close();
}
 
 
doPuppeteer();
 

일단 위의 코드를 한번 실행해보자.

제대로 작동된다면 브라우저 실행 -> 1초 대기 -> 네이버로 이동 -> 1초 대기 -> 구글로 이동 -> 페이지 닫힘 -> 브라우저 닫힘 순으로 작동할 것이다.

 

일단 puppeteer는 모든 메소드가 비동기적으로 작동한다. 그렇기 때문에 모든 경우에 await를 붙혀준다.

 

browser 에는 브라우저 객체가 저장되는데, 이 때 속성으로 headless를 false로 주었다.

headless는 브라우저를 실제로 띄워 작동 할지를 결정한다. 

 

이후에 브라우저 객체에 페이지를 추가하고 페이지 이동을 하고, 페이지를 닫는 식이다.

페이지는 당연하지만 여러 개를 띄울 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const doPuppeteer = async() => {
    const browser = await puppeteer.launch({
        headless : false
    });
    const [page, page2, page3] = await Promise.all([
        browser.newPage(),
        browser.newPage(),
        browser.newPage()
    ])
    await Promise.all([
        page.goto("https://www.naver.com"),
        page2.goto("https://www.google.com"),
        page3.goto("https://www.youtube.com"),
    ])
    await Promise.all([
        page.goto("https://www.tistory.com"),
        page2.goto("https://www.nexon.com"),
        page3.goto("https://github.io"),
    ])
    await page.close();
    await page2.close();
    await page3.close();
    await browser.close();
}
 

이 때 es6관련 문법 몇가지만 짚고 넘어가자.

Promise.all은 한번에 비동기 안의 여러가지 비동기 처리를 해야 할 경우에 사용된다.

파라미터는 배열로 받고 배열의 값으로는 실행할 메소드가 입력된다.

그리고 다음의 비구조 할당은 꽤 특이한데

1
2
3
4
5
6
 
 
 
    const [page, page2, page3] = await Promise.all([
        browser.newPage(),
        browser.newPage(),
        browser.newPage()
    ])
 
 

각각의 page에 새로운 페이지를 생성하기 위해 다음과 같이 작성되었다.

 

Crawling by Puppeteer

Puppeteer 역시 크롤링이 가능하다. axios를 사용하여 얻지는 않고, Puppetter의 자체 메소드를 이용해서 HTML를 얻고, DOM 태그 접근까지 한다 (axios와 cheerio의 역할을 모두 한다.)

 

csv 입 출력 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
const parse = require("csv-parse/lib/sync")
const fs = require("fs");
 
const csv = fs.readFileSync("csv/data.csv");
const stringify = require("csv-stringify/lib/sync");
const records = parse(csv.toString("utf-8"));
const puppeteer = require("puppeteer");
 
const doPuppeteer = async() => {
    try{
        const result = [];
        const browser = await puppeteer.launch({
            headless : false
        });
        await Promise.all(records.map(async (r,i) => {
            try{
                const page = await browser.newPage();
                await page.goto(r[1]);
                const scoreEl = await page.$('.score.score_left .star_score');
                if (scoreEl){
                    const text = await page.evaluate(tag => tag.textContent, scoreEl);
                    result[i] = [r[0],r[1],text.trim()];
                }
                await page.close();
            }catch(e){
                console.log(e)
            }
        }))
        await browser.close();
        const str = stringify(result);
        fs.writeFileSync('csv/result.csv',str);
    }catch(e){
        console.log(e);
    }
}
 

csv 파일을 읽어 url을 puppeteer에 입력하여 크롤링을 한 후에, 다시 csv파일에 결과를 출력하는 식이다.

1. 2. csv 파일을 읽어들이기 위해서는 fs.readFileSynccsv-parse

5.작성하기 위해서는 csv-stringify 를 이용한다.

Promise.all과 map을 이용하여 비동기적 반복을 하였다.

 

19. 21 .태그의 접근은 해당 페이지로 이동 후 .$ 메소드를 이용하고, 그 중 text만 가져오기 위해서 evaluate 메소드를 이용하였다.

 

22. 가져온 데이터를 csv에 입력하기 위해서는 값을 2차원 배열로 만들어 주어야 한다( 가져올 땐, 문자들이 2차원 배열로 되었다.)

 

30. 입력된 2차원 배열을 stringify로 포멧시켜 준후 writeFileSync를 이용하여 입력하여 준다.

 

위의 코드에선 태그를 접근한 후 evaluate로 text를 가져왔는데, DOM 접근 후 evalue에서 리턴받는 방법도 있다.

 

아래 코드는 15번째 줄 부터 다른 예시이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
await Promise.all(records.map(async (r,i) => {
            try{
                const page = await browser.newPage();
                await page.goto(r[1]);
                const text = await page.evaluate(()=>{ //evaluate의 callback의 값이 text
                    const score = document.querySelector('.score.score_left .star_score');
                    if(score){
                        return score.textContent;
                    }
                });
                if(text)
                    result[i] = [r[0], r[1], text.trim()]
                await page.close();
            }catch(e){
                console.log(e)
            }
        }))
 

xlsx 입 출력 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
const xlsx = require("xlsx");
const add_to_sheet = require("./add_to_sheet.js");
const puppeteer = require("puppeteer");
 
const workbook = xlsx.readFile('xlsx/data.xlsx');
const ws = workbook.Sheets.시트명;
const records = xlsx.utils.sheet_to_json(ws);
 
const doPuppeteer = async() => {
    try{
        const browser = await puppeteer.launch({
            headless : false
        });
        const page = await browser.newPage();
        await page.setUserAgent("브라우저의 navigator.userAgent의 결과값");
        add_to_sheet(ws, 'C1','s',"평점");
        for(const [i,r] of records.entries()){
                await page.goto(r.링크);
                const text = await page.evaluate(()=>{
                    const score = document.querySelector('.score.score_left .star_score');
                    if(score){
                        return score.textContent;
                    }
                });
                if(text){
                    console.log(text);
                    const newCell = 'C' + (i + 2);
                    add_to_sheet(ws,newCell,'n',parseFloat(text.trim()))
                    await page.waitFor(1000);
                }
        }
        await page.close();
        await browser.close();
        xlsx.writeFile(workbook, 'xlsx/result2.xlsx')
    }catch(e){
        console.log(e);
    }
}
 
doPuppeteer();
 

파일이 바뀐거 변한 점이 있다.

15. setUserAgent는 브라우저의 타입을 속인다. Chrominum을 사용하기 때문에 크롤링이 막힐 때가 있기 때문에 사용된다.

 

이외에는 xlsx로 접근 한점, add_to_sheet의 사용정도를 봐두면 될꺼 같다.