개발일기장

Node.JS 웹 크롤링으로 Promise.all 성능 비교 (수정: 이거 다 틀렸음) 본문

node.js

Node.JS 웹 크롤링으로 Promise.all 성능 비교 (수정: 이거 다 틀렸음)

게슬 2022. 5. 18. 18:57
728x90

※ 위에내용 다 틀렸고 밑에 고친거 있음. 쭉 내리면 됨

 

https://tlqckd0.tistory.com/48

 

NodeJS로 웹 크롤링 하기. puppeteer, cheerio, 동적 크롤링

1. 왜 puppeteer를 사용하는지 https://www.npmjs.com/package/puppeteer puppeteer A high-level API to control headless Chrome over the DevTools Protocol. Latest version: 14.1.0, last published: 5 days..

tlqckd0.tistory.com

이거랑 이어지는 거임.

 

일단 내가 원하는 것은 아래 사진과 같은 방식인데

이런거

일단 게시판 페이지를 크롤링 한 다음에 각각의 게시글마다 들어가서 어떤 사용자가, 언제 댓글을 달았는지 확인하고 싶었음.

1. For await 문으로 실행

그래서 일단 정말 단순하게

1. 게시글 리스트를 크롤링 한다

2. 각각의 게시글마다 들어가서 댓글을 크롤링한다.

이런 방식으로 데이터를 가지고 왔음.

const processing = ({ page }) => {
    return new Promise(async (resolve, reject) => {
        try {
            const obj_post = post_list_object;
            obj_post.href =
                process.env.ROOT_HREF + obj_post.href + `?page=${page}`;

            // POST정보 가지고오기.
            let post_data_list = await crawler(obj_post);
            const post_list_length = post_data_list.length;

            //방법 1.
            for (let i = 0; i < post_list_length; i++) {
               const obj_reple = reple_list_object;
                obj_reple.href = process.env.ROOT_HREF + post_data_list[i].href;
                post_data_list[i].reple = await crawler(obj_reple);
            }

            resolve(post_data_list);
        } catch (err) {
            console.error(err);
            reject(err);
        }
    });
};

for문으로 게시글 하나씩 들어가서 크롤링을 하면서 await를 걸어버렸다.

하나씩

그러니깐 120초 넘게 걸리는 현상이 발생했음. 어짜피 게시판 페이지 하나가 30갠가 20개로 구성되어있어서 120초 동안 글 리젠이 안되면 상관이 없긴한데 이 방식으로 태스트하고 서버에 올려서 돌리기는 조금 부담스럽다고 생각했음.

 

처음에 100개 정도 되는 페이지를 다 읽으려면 12000초니깐 3시간 넘게 기다려야 하는데 그거는 좀 ..

 

2. Promise.all로 실행 (뭔가 이상함)

그래서 어짜피 crawl함수는 promise가 반환값이기 때문에 Promise.all로 한번에 실행시키는 방식으로 변경해봤음

const processing = ({ page }) => {
    return new Promise(async (resolve, reject) => {
        try {
            const obj_post = post_list_object;
            obj_post.href =
                process.env.ROOT_HREF + obj_post.href + `?page=${page}`;

            // POST정보 가지고오기.
            let post_data_list = await crawler(obj_post);
            const post_list_length = post_data_list.length;

            //방법 2.
            const reple_list = new Array(post_list_length);
            for (let i = 0; i < post_list_length; i++) {
                const obj_reple = reple_list_object;
                obj_reple.href = process.env.ROOT_HREF + post_data_list[i].href;
                reple_list[i] = crawler(obj_reple);
            }
            const result = await Promise.all(reple_list);
            for (let i = 0; i < post_list_length; i++) {
                post_data_list[i].reple = result[i];
            }

            resolve(post_data_list);
        } catch (err) {
            console.error(err);
            reject(err);
        }
    });
};

reple_list 에 크롤링해야할 정보를 promise 비동기를 걸어버린 다음에

그 밑에서 Promise.all로 다 잡아버리는 방식으로 했다.

한번에

120초 -> 27초로 1/5수준으로 속도가 줄어들었음.

 

3. 문제점

1. 그런대 for await로 하나씩 가지고 올 때에는 데이터를 정확히 다 크롤링 해왔는데

Promise all로 한번에 잡으니깐 데이터가 문제가 생겼음.

다 같은걸로 나옴.. 내가 어디서 잘못했지. 링크는 제대로 전달이 되었는데 ..

 

2. 이게 Chromium열어서 하나씩 가지고 오는거다 보니깐 listener를 늘려줘야 했음 열어야 할 페이지가 1 + 20개다 보니깐 오류가 났었던것 같다.

process.setMaxListeners(40);

 

근대 진짜 왜 다 같게 나오지 뭐가 문젠지 모르겠다.

자바스크립트 공부가 너무 부족한거 같다 ㅜㅜㅜㅜㅜㅜㅜㅜㅜㅜㅜㅜㅜㅜㅜㅜㅜㅜㅜㅜㅜㅜ

고쳐봐야겠다 ㅜㅜㅜㅜㅜㅜㅜㅜㅜㅜㅜㅜㅜㅜㅜㅜㅜㅜㅜㅜ

 

+ puppeteer쪽에 설정을 뭐 해주면 된다는거같은데 더 찾고 고쳐야 겠다.

 


4. 문제 해결(5월 20일)

1. for await문 속도라 120초 이상 걸리고 느렸던 이유

 

-> browser(Chromium)를 실행시키고, page를 열고 원하는 링크로 이동하는 작업 자체가 (3+a)초 정도 걸렸음. 실행시킨 브라우저와 페이지 연것을 재활용하지 않은것은 비효율적인 작업임. AWS 서버에 올렸으면 진짜 느렸을듯

 

-> 이거만 재활용 해주면 링크이동 + 데이터 가져오기 = 1.5초 정도 걸리는 작업.

 

2. Promise.All에서 데이터가 문제가 생겼던 이유 

-> 확인해보니 loop마지막 링크에서 가지고온 reple들의 값을 다 공유하는 것으로 봐선 page와 browser를 열었을때 내부적으로 마지막으로 열린 페이지의 데이터를 가지고 왔는데 이거 이유는 모르겠음.

 

5. 수정 코드

crawling과 parsing하는 부분을 나누고 한번 열었던 page를 재활용 + page에서 다 열릴때까지 기다리는 옵션 사용

crawl_process.js

const { crawler } = require('./crawler');
const { post_list_object, reple_list_object } = require('./crawl_obejct');
const puppeteer = require('puppeteer');

require('dotenv').config();
process.setMaxListeners(40);

const processing = ({ page_num }) => {
    return new Promise(async (resolve, reject) => {
        try {
        	//브라우저와 페이지는 한번 열고 끝까지 사용한다.
            const browser = await puppeteer.launch();            
            const page = await browser.newPage();
            
            const obj_post = post_list_object;
            obj_post.href =
                process.env.ROOT_HREF + obj_post.href + `?page=${page_num}`;

            // POST정보 가지고오기.
            
            await page.goto(obj_post.href);
            const content = await page.content();
            let post_data_list = crawler({
                crawl_object: obj_post,
                content,
            });

            const post_list_length = post_data_list.length;        

			
            for (let i = 0; i < post_list_length; i++) {
                let obj_reple = reple_list_object;
                obj_reple.href = process.env.ROOT_HREF + post_data_list[i].href;   
                
                // 페이지가 중복으로 열리는것을 방지하기 위해서 for await를 걸었지만 엄청 느려지지는 않았음.
                // page.waitForNavigation({waitUntil: 'networkidle2' }) 옵션을 사용하여 끝까지 기다려줌.
                
                await Promise.all([page.goto(obj_reple.href), page.waitForNavigation({waitUntil: 'networkidle2' })]) 
                const content = await page.content();
                post_data_list[i].reple = crawler({ crawl_object: obj_reple, content });
            }
            
			//다 사용한 페이지와 브라우저 닫기
            await page.close();
            await browser.close();
            resolve(post_data_list);
            
        } catch (err) {
            console.error(err);
            reject([]);
        }
    });
};

 

변경된  crawler.js (Promise를 사용할 이유가 없다.)

const cheerio = require('cheerio');

const crawler = ({ crawl_object, content }) => {
    const $ = cheerio.load(content);
    const result = [];
    $(crawl_object.select_path).each(function (idx, element) {
        const $data = cheerio.load(element);
        const return_data = {};
        crawl_object.items.forEach((item) => {
            if (item.text === true) {
                return_data[item.name] = $data(item.path).text();
            }

            if (item.href === true) {
                return_data[item.name] = $data(item.path).attr('href');
            }
        });

        if (crawl_object.type === 'post') {
			// 파싱 로직
        }

        if (crawl_object.type === 'reple') {
			// 파싱 로직
        }
    });
    return result;
};


module.exports = {
    crawler,
};

 

 

6. 하..

1. 라이브러리 사용할땐 좀 알아보고 사용하자

2. 재활용 할 수 있는것은 최대한 재활용하기

3. 모듈 하나는 하나의 열활만 -> 크롤링하기, 파싱하기 분리

4. Promise, async, await 남발하지 않기 -> NodeJS사용하는 이유가 비동기에서 비롯한 빠릿빠릿함인데 저거 쓰면 이유가 없어지는것 같다.(그런 느낌)

 

https://github.com/tlqckd0/web-crawling/tree/main/crawl

 

GitHub - tlqckd0/web-crawling: web-crawling & analysis

web-crawling & analysis. Contribute to tlqckd0/web-crawling development by creating an account on GitHub.

github.com

 

728x90
Comments