[Nest.js]09. puppeteer로 무한 스크롤링하기(2)

김지엽·2023년 10월 13일
0
post-custom-banner

1. 개요

저번 글에 이어서 이번에는 모든 웹툰의 id를 가져오는 함수를 구현한다. 저번보다 훨씬 쉽지만 주의해야 할 것은 완결 웹툰이다.

완결 웹툰은 양이 많기 때문에 네이버나 카카오 웹툰 페이지를 들어가면 한번에 모든 웹툰을 로딩 하지 않고 일부분만 로딩한다.

그리고 무한 스크롤링 기능이 구현되어 있어 모든 완결 웹툰을 크롤링 할려면 무한 스크롤링 동작을 직접 크롤링 과정에서 구현해야 한다.

무한 스크롤링이란 서버에서 한번에 많은 데이터를 받아올때 클라이언트에서 모든 정보를 한번에 보여주면서 사이트의 속도가 매우 느려지는 현상을 막기 위해서 데이터의 일부분만을 표시하고 스크롤을 할 때마다 더 많은 정보를 계속해서 표시해주는 기능이다.

2. 요일별 웹툰 크롤링 구현

- crawling 메서드

...

async crawlWeeklyWebtoonId(page: Page, day: CrawlDayOption, service: string): Promise<string[]> {
    const webtoonIds = service === "naver"
        ? await getKakaoWebtoonIdForDay(page, day)
        : await getNaverWebtoonIdForDay(page, day);

    return webtoonIds;
}

...

- kakao

import { Page } from "puppeteer";
import * as cheerio from "cheerio";
import { CrawlDayOption } from "src/types/webtoon.interface";
import {
    KAKAO_DAY_TRANSFORM,
    KAKAO_DAY_WEBTOONLIST_SELECTOR,
} from "src/constatns/crawling.constants";

export async function getKakaoWebtoonIdForDay(page: Page, option: CrawlDayOption): Promise<string[]> {
    const webtoonIdList: string[] = [];

    // 요일을 url에 들어갈 수 있게 바꿔줌
    const day = KAKAO_DAY_TRANSFORM.indexOf(option.day);
    await page.goto(`https://page.kakao.com/menu/10010/screen/52?tab_uid=${day}`);

    // 무한 스크롤링 방지
    while (true) {
        try {
            // 페이지 끝까지 스크롤을 하기 전 높이와 이후의 높이를 비교한다.
            const scrollHeight = "document.body.scrollHeight";
            let previousHeight = await page.evaluate(scrollHeight);
            await page.evaluate(`window.scrollTo(0, ${scrollHeight})`);
            await page.waitForFunction(`${scrollHeight} > ${previousHeight}`, {
                timeout: 5000,
            });
        } catch (e) {
            // 더이상 스크롤 높이가 증가하지 않아 오류가 발생하면 스크롤링을 멈춘다. 
            break;
        }
    }

    const content = await page.content();
    const $ = cheerio.load(content);

    // 웹툰 id 불러오기
    const rootElement = $(KAKAO_DAY_WEBTOONLIST_SELECTOR);
    rootElement.children().map((idx, element) => {
        const $data = cheerio.load(element);
        const id: string = $data("div a").attr("href").split("/")[2];
        webtoonIdList.push(id);
    });

    return webtoonIdList;
}

- naver

import { Page } from "puppeteer";
import * as cheerio from "cheerio";
import { CrawlDayOption } from "src/types/webtoon.interface";
import {
    KAKAO_DAY_TRANSFORM,
    NAVER_DAY_CLICK_SELECTOR,
    NAVER_DAY_TRANSFORM,
    NAVER_DAY_WEBTOONLIST_SELECTOR,
} from "src/constatns/crawling.constants";

export async function getNaverWebtoonIdForDay(page: Page, option: CrawlDayOption): Promise<string[]> {
    const webtoonIdList: string[] = [];

    // 요일을 url에 들어갈 수 있게 바꿔줌
    const day =  NAVER_DAY_TRANSFORM[KAKAO_DAY_TRANSFORM.indexOf(option.day)];
    await page.goto(`https://comic.naver.com/webtoon?tab=${day}`);
    await page.waitForSelector(NAVER_DAY_WEBTOONLIST_SELECTOR);

    // 무한 스크롤링 방지
    while (true) {
        try {
            // 네이버 자동 스크롤링 방지 뚫기 (다른 페이지를 갔다가 다시 돌아온다.)
            await page.click(NAVER_DAY_CLICK_SELECTOR);
            await page.waitForSelector(NAVER_DAY_WEBTOONLIST_SELECTOR);

            await page.goBack();
            await page.waitForSelector(NAVER_DAY_WEBTOONLIST_SELECTOR, { timeout: 30000 });

            // 스크롤
            const scrollHeight = 'document.body.scrollHeight';
            const previousHeight = await page.evaluate(scrollHeight);
            await page.evaluate(`window.scrollTo(0, ${scrollHeight})`);
            await page.waitForFunction(`${scrollHeight} > ${previousHeight}`, {
                timeout: 30000
            });

            if (parseInt(previousHeight as string) > 80000) {
                break;
            }
            console.log(previousHeight);
        } catch (e) {
            break;
        }
    }

    const content = await page.content();
    const $ = cheerio.load(content);

    // 웹툰 id 불러오기
    const rootElement = $(NAVER_DAY_WEBTOONLIST_SELECTOR);
    rootElement.children().map((idx, element) => {
        const $data = cheerio.load(element);
        const reg = new RegExp(/[0-9]+/, "g");
        const webtoonId = $data("a").attr("href").match(reg)[0];
        webtoonIdList.push(webtoonId);
    });

    return webtoonIdList;
}

문제점

- 네이버의 무한 스크롤링 방지

위의 코드를 보면 카카오와 네이버의 무한 스크롤링을 방지하는 코드가 약간 차이가 있다. 그 이유는 네이버에서 매크로 방지를 위해 무한 스크롤링을 자동으로 못하게 막아 놓았기 때문이다.

따라서 카카오에서의 코드와 같이 일반적인 무한 스크롤링 동작을 구현하면 네이버에서는 스크롤이 내려가지 않는다. 그렇다면 어떻게 해결했을까?

직접 네이버 페이지를 이리저리 조작하면서 알아낸 것은 완결웹툰 페이지에서 스크롤을 하고 웹툰 데이터를 추가로 불러온 뒤 월요웹툰이나 화요웹툰 등 다른 웹툰 페이지를 갔다가 다시 완결 웹툰으로 돌아와도 이전에 스크롤을 통해 불러온 데이터는 유지된다는 것이다.

이를 이용해서 스크롤을 한번 하고 다른 웹툰 페이지를 갔다가 다시 돌아오고를 반복해 무한 스크롤링 동작이 계속되도록 구현했다. 해당 코드는 다음과 같다.

// 네이버 자동 스크롤링 방지 뚫기 (다른 페이지를 갔다가 다시 돌아온다.)
await page.click(NAVER_DAY_CLICK_SELECTOR);
await page.waitForSelector(NAVER_DAY_WEBTOONLIST_SELECTOR);

await page.goBack();
await page.waitForSelector(NAVER_DAY_WEBTOONLIST_SELECTOR, { timeout: 30000 });

글을 마치며

크롤링을 리팩토링 하면서 느낀 것은 코드를 작성할때 추후를 생각하면서 개발해야 한다는 것이다.

리팩토링 전 코드에서는 정말 매번 기능이 추가되거나 생각대로 안되는 것이 있을때 수정을 하는 것이 너무 귀찮고 번거로우며 오래 걸렸다.

리팩토링을 한 후의 코드를 이전의 코드와 비교하면서 내가 저 때보다는 성장했구나 느끼며 앞으로의 프로젝트에서는 미래를 생각하는 코드를 작성해야겠다고 다짐한다.

참고

https://pptr.dev/api/puppeteer.puppeteernode - puppeteer 공식 문서
https://cheerio.js.org/docs/api - cheerio 공식 문서

profile
욕심 많은 개발자
post-custom-banner

0개의 댓글