[nodejs] 웹툰 플랫폼 랭킹 순위 크롤링 해보기 - 1

protomothis·2023년 1월 14일
0
post-thumbnail

시작하기

나의 지인은 얼마전부터 웹툰 출판업계에서 일을 하게 되었다.
회사에서 그럭저럭 적응을 잘 하는 줄 알았는데, 얼마 전 이런 이야기를 듣게 되었다.

"회사에서 내고 있는 작품들 포함해서 우리랑 연관된 각 웹툰 플랫폼의 랭킹 데이터가 필요해서 하루종일 수작업으로 엑셀에 적고있어...ㅠ"

그 이야기를 듣고 나서.. 그건 크롤링을 하면 해결될 이야기가 아닐까 해서 간단하게 만들어보기로 했다.
지인은 화색이 돌며 정말 고마워하드라.
그래서 나는 단기간에 아주 최소한의 기능만 만들어주고 수작업의 고통에서 벗어나게 해주고 싶었다.

익숙한 언어인 javascript로 크롤러를 만들기에는 nodejs가 적절하다. 크롤러를 만들어본 적은 없었지만.. 뭐 세상에 크롤링을 위한 적당한 라이브러리는 넘쳤기에 간단한 이해를 바탕으로 나는 다음과 같이 모델을 고려하였다.

처음 목표인 픽코마에서 받아올 모델이다.

// 픽코마 랭킹 메인에서 받아올 작품 한 단위의 모델
export interface ResponsePiccomaRankingData {
  imgSrc: string;
  ranking: number;
  title: string;
  author: string;
  likes: string;
}

/**
각 랭킹 메뉴, 총 3가지가 있는데 만화, 웹툰, 노벨을
지원하는 모양이다. route에 따라 나뉘므로 다음과 같이 정의하였음.
*/
export enum PiccomaRankingMenus {
  manga = 'K',
  smartToon = 'S',
  novel = 'N',
}

/**
상기 메뉴 중에서.. 타겟은 웹툰이 될 예정이었으므로,
smartToon의 카테고리를 모두 파악해 정리하였다.
*/
export enum PiccomaSmartToonRankingCategories {
  common = 0,
  love = 1,
  fantasy = 2,
  drama = 3,
  daliy = 4,
  action = 5,
  sport = 6,
  mistery = 7,
  noir = 9,
  gourmet = 10,
}

그 다음엔?

nodejs로 결정했으니 webapi를 만들기 적당한 expressjs를 기반으로 model, service, controller를 구성하기로 하였다.

픽코마 크롤링 서비스 구성하기

긁는 도구로는 puppeteer를 선정했다.
https://pptr.dev/
워낙 유명하므로 자세한 설명은 생략한다!

public async crawlData(piccoma: RequestPiccomaDto): Promise<ResponsePiccomaRankingData[]> {
    // puppeteer initialize
    const browser = await puppeteer.launch({
      headless: true,
    });

	const page = await browser.newPage();
    const url = `https://piccoma.com/web/ranking/${PiccomaRankingMenus[piccoma.menu]}/P/${PiccomaSmartToonRankingCategories[piccoma.category]}`;

try {
      const result: ResponsePiccomaRankingData[] = [];
      // 페이지 로드를 기다렸다가,
      await page.goto(url, { waitUntil: 'load' });
      // 랭킹 페이지가 로드되면 스크롤 여부에 따라 컨텐츠가 완전히 로드되지 않을 수도 있다.
      // 그래서 로드된 뒤로 스크롤 자동으로 내려주면서 자연스럽게 컨텐츠를 완전히 로드!
      const lastPosition = await scrollPageToBottom(page, {
        size: 500,
        delay: 250,
      });

      const content = await page.content();
      const $ = load(content);
      // 컨텐츠를 긁어올 곳을 셀렉터로 긁기
      const rankingList = $('#js_contentBody > section > ul > li');
      // 요청한 만큼만 긁기. 예를 들어 50개만 요청되면 50개만 짤라서 가져오기
      const cutLength: number = rankingList.length ? Math.min(rankingList.length, piccoma.length) : rankingList.length;

      // 각 셀렉터에서 ranking, title, author, likes, imgSrc를 가져오기
      rankingList.slice(0, cutLength).each((i, e) => {
        const ranking = Number($(e).find(`div.PCM-rankingProduct_rank > div.PCM-rankingProduct_rankNum`).text());
        const title = $(e).find(`div.PCM-l_rankingProduct_info > div > div.PCM-rankingProduct_tdata > div.PCM-rankingProduct_title > p`).text();
        const author = $(e).find(`div.PCM-l_rankingProduct_info > div > div.PCM-rankingProduct_tdata > div.PCM-rankingProduct_author > p`).text();
        const likes = $(e).find(`div.PCM-l_rankingProduct_info > div > div.PCM-rankingProduct_tdata > div.PCM-l_rankingProduct_like > div.PCM-rankingProduct_like > span`).text();
        const imgSrc = $(e).find(`div.PCM-l_productCoverImage > div > div > img`).attr('src');

        result.push({
          imgSrc,
          ranking,
          title,
          author,
          likes,
        });
      });
}

결과


데이터는 잘 가져오는 것 같다.
그런데 지인은 저런 rawData를 받고싶은 것이 아니라 엑셀파일을 받고 싶어하기 때문에.. 다음 편에서 엑셀에 데이터를 말아넣어 보도록 하자

profile
frontend dev, home server lover.

0개의 댓글