본문 바로가기
프로젝트

api를 이용한 스크래핑 성능 개선

by 해룸 2024. 4. 15.

퍼페티어를 이용해 스크래핑을 완성하였으나, 성능이 너무나 뒤떨어졌다.

 

데이터 스크래핑을 해야하는 페이지는 사용자에게 쾌적한 사용을 주기 위해서인지 스크롤을 해서 해당 페이지에 도착했을때 해당 데이터가 네트워크 탭을 통해 들어오게 된다. 이런 특성 때문에 퍼페티어상에서도 직접 스크롤을 해야하고, 리뷰 목록을 가져올때도, 아무튼 모든걸 하나하나 클릭을 해서 가져오는데에 비해 가져올 데이터는 많아서 느려도 너무 느렸다..

 

작품 데이터 상위 20개만 가져온다고 해도 먼저 랭킹페이지에서 20위까지의 상세페이지 링크를 긁어모으고, 하나하나 들어가서 원하는 데이터를 가져와야한다. 이 와중에 스크롤도 해야하고 리뷰버튼은 더보기 버튼의 css 선택자가 달랐다... 뭐 이런 문제는 차치해도 성능이 큰 문제였다. 저렇게만 가져와도 빠르면 5분, 중간에 걸리면 10분까지도 걸린것 같다. 

 

하루에 한번 플랫폼별 랭킹데이터가 필요했고, 각 플랫폼에서 100개씩 추가로 데이터를 넣어서 업데이트 해줘야하는데 이렇게는 도저히 안되서 차라리 api를 찾게 되었다.

 

api란

api(application programming interface)는 두 프로그램이 서로 대화하기 위한 방법을 정의한 것이다. 인증된 URL만 있으면 언제든지 필요한 데이터에 편리하게 접근할 수 있다. 예를 들어 우리가 사용하는 윈도우나 맥os 같은 운영체제는 문서 작성 프로그램이 디스크에 있는 파일을 읽고 쓸 수 있도록 api 를 제공한다. 또 기상청에서 제공하는 api를 사용하면 지역별, 실시간 날씨 정보를 얻을 수 있다.

네트워크 탭에서 api 찾기

 

웹 기반 api는 웹 서버와 웹 브라우저가 대화하는 방식과 비슷하다. http 프로토콜을 사용하지만 html을 주고받는것이 아니라 일반적으로 csv, json, xml같은 파일을 사용한다.

개발자 도구를 켜서 네트워크 탭에 들어간다. fetch/XHR 형식만 보도록 해서 새로고침하면 여러가지 데이터가 들어오는게 보이는데, 그 중에서 응답값에 내가 원하는 데이터가 있는지 살펴본다.

보통은 숨겨 놓는다고 하는데 나는 운이 좋게도 떡하니 데이터가 존재했다...

 

 

여기 있는 URL로 요청을 보내면 내가 원하는 데이터가 그대로 들어온다.. ^-^!!

만약 인증이 필요하거나 하다면 아래에 요청헤더값을 넣어주면 된다고 한다.

 

구현

export async function get60WebtoonRanking(type: TYPE, page: number) {
  try {
    const { data } = await axios({
      method: 'get',
      url: `https://ridibooks.com/_next/data/3.8.172-ecfb8c7/category/books/${type}.json?tab=books&category=${type}&page=${page}&order=review`,
    });
    const books = data.pageProps.dehydratedState.queries[2].state.data;

    const data60ranking = await Promise.all(
      books.map(async (bookData) => {
        const book = bookData.book;
        const bookId = book.bookId;
        const title = book.serial.title;
        const desc = book.introduction.description;
        const imageSmall = book.cover.small;
        const image = imageSmall.replace('/small', '');
        const isAdult = book.adultsOnly;
        const authorArr = book.authors;
        let authors = [];
        for (let i = 0; i < authorArr.length; i++) {
          const name = authorArr[i].name;
          const role = authorArr[i].role;
          const author = `${name}/${role}`;
          authors.push(author);
        }
        const categoryArr = book.categories;
        const category = [];
        for (let i = 0; i < categoryArr.length; i++) {
          category.push(categoryArr[i].name);
        }
        const url = `https://ridibooks.com/books/${bookId}`;
        const pubDate = book.publicationDate;
        let contentType;

        if (type == '1600') {
          contentType = ContentType.WEBTOON;
        } else {
          contentType = ContentType.WEBNOVEL;
        }

        let reviewList = [];

        const reviews10 = await getReviews10(bookId);
        const reviews20 = await getReviews20(bookId);
        reviewList = reviewList.concat(reviews10, reviews20);

        const platform = { ridibooks: url };
        return {
          contentType,
          bookId,
          platform,
          title,
          desc,
          image,
          isAdult,
          author: authors.join(', '),
          category: category.join(', '),
          url,
          pubDate,
          pReviews: reviewList,
        };
      }),
    );

    return data60ranking;
  } catch (error) {
    console.error('Error fetching data:', error);
  }
}

axios를 이용해 해당 url에 get 요청을 보내주고 json 데이터를 내가 원하는 형식으로 정리한다.

 

export async function getReviews10(booksId) {
  let offset = 0;
  const { data } = await axios({
    method: 'get',
    url: `https://ridibooks.com/books/load-reviews?book_id=${booksId}&order=like&offset=0&buyer_only=true`,
  });

  const $ = cheerio.load(data);

  const reviews = [];

  $('ul > li.review_list').each((index, element) => {
    const writer = $(element)
      .find('.list_left.js_review_info_wrapper > div > p > span.reviewer_id')
      .text()
      .trim();
    const content = $(element)
      .find('.list_right.js_review_wrapper > p')
      .text()
      .trim();
    const likeCount = $(element)
      .find(
        '.list_right.js_review_wrapper > div.review_status > div > button.rui_button_white_25.like_button.js_like_button > span > span.rui_button_text > span.like_count.js_like_count',
      )
      .text()
      .trim();
    const isSpoiler = $(element)
      .find(
        '.list_right.js_review_wrapper > div.rui_full_alert_4.spoiler_alert.js_spoiler_alert > article > p',
      )
      .text()
      .trim();
    const dateString = $(element)
      .find('.list_left.js_review_info_wrapper > div > div > div')
      .text()
      .trim();
    const trimmedDateString = dateString.replace(/\.$/, '');
    const parts = trimmedDateString.split('.');

    const yyyy = parts[0];
    const mm = parts[1];
    const dd = parts[2];

    const isoDateString = `${yyyy}-${mm}-${dd}`;

    const createdDate = new Date(isoDateString);

    console.log(trimmedDateString, isoDateString, createdDate);

    if (likeCount === '') {
      console.log('Skipping review due to empty likeCount.');
      return; // 현재 반복을 중지하고 다음 반복으로 넘어갑니다.
    }

    reviews.push({
      writer,
      content,
      likeCount,
      isSpoiler: isSpoiler ? true : false,
      createDate: createdDate,
    });
  });

  return reviews;
}

리뷰데이터는 html api여서 cheerio를 병행해서 사용했다.

 

 async save60Db(data: WebContents[]) {
    try {
      const createContentDtos = data.map((content) => {
        const webContent = new WebContents();

        webContent.title = content.title;
        webContent.desc = content.desc;
        webContent.image = content.image;
        webContent.author = content.author;
        webContent.category = content.category;
        webContent.isAdult = content.isAdult;
        webContent.platform = content.platform;
        webContent.pubDate = new Date(content.pubDate);
        // webContent.keyword = JSON.stringify(content.keyword);
        // webContent.rank = content.rank;
        webContent.contentType = ContentType.WEBTOON;

        if (content.pReviews.length !== 0) {
          webContent.pReviews = content.pReviews;
        }

        return webContent;
      });

      // DB에 저장
      await this.contentRepository.save(createContentDtos);
    } catch (err) {
      throw err;
    }
  }

가져온 데이터를 db에 저장한다!

 

//crawler.service.ts
@Cron('0 17 * * *') //오후 다섯시 예약
  async createRidibooks() {
    const startTime = new Date().getTime();

    const currPage = 0;

    try {
      // 일간랭킹;
      await this.rankUpdet();

      console.log('start!');
      const rankingRnovels = await get20BestRanking(GENRE.R);
      await this.save20Db(rankingRnovels);
      console.log('done!');
      ... 생략

스케쥴러로 시간을 예약해두면 정해진 시간에 작업을 실행한다.

 

이렇게 변경한 코드로 작업을 실행하면, 총 400개 작품 데이터와 작품당 20개의 리뷰데이터(총 8000개)를 가져오는데 걸리는 시간은 이렇다.

😂😂😂😂😂😂😂😂😂

너무너무 아름다운 로딩시간...

밤샘해서 한 보람이 있었다....!!!!!

'프로젝트' 카테고리의 다른 글

Nest.js, SSE(server sent event)를 이용해 알림띄우기(해결안됨)  (0) 2024.04.22
퍼펫티어를 이용한 크롤링(2)  (0) 2024.04.11
퍼펫티어를 이용한 크롤링(1)  (0) 2024.04.11
최종프로젝트  (0) 2024.04.09
db 선택  (0) 2024.03.27