NodeJS로 웹 크롤링 해보기

남정호·2021년 1월 24일
1

NodeJS

목록 보기
2/2

크롤링을 할 때에는 당연히 파이썬을 사용하는 것이 더 편할 것이다. 하지만 나는 FE 개발자이고 파이썬은 print("heelo world") 밖에 할 줄 모르기 때문에 Node 를 이용한 간단한 크롤러를 만들어 보기로 했다.

준비물

크롤링하기 쉬운 사이트

난 크롤러를 아주 간단하고 구현하고 싶기 때문에, 목표 사이트가 크롤링 하기에 편해야 한다. 그 기준은 다음과 같다.

1. 로그인이 없어야 한다.

로그인이 있을 때에도 크롤링 할 수 있는 방법이야 있겠지만, 나는 싫다. 로그인 방식도 사이트마다 다르며 그거 뚫는 시간보다 대체 사이트 찾는게 빠를 수 있다.

2. URL 이 친절해야 한다.

우리는 보통 한 페이지의 정보만이 아니라 여러 페이지 정보를 얻고 싶어한다. 특정 검색어에 대한 결과를 1페이지부터 10페이지 까지 얻고 싶을 수도 있고, 날짜별 환율을 구하고 싶을 수도 있다.
그런데 만약 사이트의 URL이 REST 하지 않아서 페이지를 바꾸어도 URL이 변경되지 않는다면? URL에서 페이지를 유추할 수 없다면? 그럼 골치 아파진다. 클릭 이벤트를 동작 시켜서 다음 페이지 값을 가져오는 방법을 생각해볼 수 있겠지만, 글쎄. 이 것도 방법 찾다가 날 샐 것 같다. (방법이 없진 않을 것이다. 그런데 난 안 할거다.)

말로 하면 모호하니 예시를 보자.

불친절한 URL 예시1 - 페이지를 바꿔도 URL이 안 바뀜

1 페이지 URL: http://example/somePost
2 페이지 URL: http://example/somePost
3 페이지 URL: http://example/somePost

불친절한 URL 예시2 - URL에서 페이지를 유추할 수 없음

1 페이지 URL: http://example/somePost?someQuery=213ugiadsyhifgjash
2 페이지 URL: http://example/somePost?someQuery=askjldfh789asdhfiu
3 페이지 URL: http://example/somePost?someQuery=2390j9sdfh93

친절한 URL 예시

1 페이지 URL: http://example/somePost?page=1
2 페이지 URL: http://example/somePost?page=2
3 페이지 URL: http://example/somePost?page=3

Node

노드는 당연하고, 그거 말고 내가 사용할 패키지는 다음과 같다.

"dependencies": {
  "axios": "^0.21.1",
  "cheerio": "^1.0.0-rc.5",
  "date-fns": "^2.16.1",
  "exceljs": "^4.2.0"
}

axios: 페이지를 fetch 하기 위해 사용
cheerio: 가져온 페이지를 파싱(분석)하기 위해 사용
date-fns: 내가 날짜 별 데이터가 필요해서 설치한 Date 관리용 패키지
exceljs: 파싱된 데이터를 xls 파일로 저장하기 위해 사용

위에 두개만 필수고 마지막꺼는 내가 필요해서 깐거

크롤링 GO

난 특정 게임의 날짜별 매출 순위를 가져올 것이다. 요새 아이템 때문에 하고 있는 메이플 M 을 분석해보자.

사이트 찾기

폭풍 구글링 끝에 두 개의 사이트를 찾았다. 운 좋게도 좋은 예시와 안 좋은 예시가 둘 다 들어 있었다.

  1. http://www.gevolution.co.kr/rank/history
  2. https://www.mobileindex.com/app/rank

1. 의 경우 날짜를 바꾸어도 URL이 변경되지 않는다. 따라서 크롤링 하기에 아주 불편하다.
2. 의 경우 URL이 아주 친절하다.
https://www.mobileindex.com/app/get_rank_all?rt=r&mk=2&c=kr&t=game&rs=100&d=2020-01-19
ANDROID 인지 IOS 인지, 카테고리가 게임인지, 날짜는 언제인지 전부 알려준다. 너무 착하다.

코딩 시작

나의 소중한 주말이 슬슬 아까워서 코딩 과정은 git 레포로 대체한다.
https://github.com/njh7799/getMobileGameRank

페이지 요청하기

우선 axios를 이용하여 페이지 데이터를 가져온다. params는 어떤 날짜의 데이터를 가져올지와 OS의 종류만 결정해준다.

const cheerio = require("cheerio");
const axios = require("axios");

const baseUrl = "https://www.mobileindex.com/app/get_rank_all";

const defaultParams = {
  rt: "r", // 모름
  mk: 2, // 1이면 IOS, 2이면 ANDROID
  c: "kr", // 지역
  t: "game", // 카테고리: 게임
  rs: 100, // 한 번에 몇 개까지 보여줄지 결정
  d: "2020-01-19", // 날짜
};

async function fetchPage({ deviceValue, date }) {
  const response = await axios.get(baseUrl, {
    params: {
      ...defaultParams,
      mk: deviceValue,
      d: date,
    },
  });
  const { data } = response;
  return data;
}

파싱

현재 들고 있는 데이터는 html 파일의 더미이다. 이 값을 의미 있는 값으로 변환시키는 작업인 파싱을 진행해야한다. 데이터 파싱에는 cheerio 패키지를 사용했다.

...
async function fetchPage({ deviceValue, date }) {
  ...
}

async function parseData(pageData) {
  const $ = cheerio.load(pageData);
  const tbody = $("tbody")[0];
  return getTrs(tbody).map((tr) =>
    getTds(tr).map((td) => {
      return getName($(td));
    })
  );
}

function getTrs(tbody) {
  return tbody.children.filter((node) => {
    return node.name === "tr";
  });
}

function getTds(tr) {
  return tr.children
    .filter((node) => {
      return node.name === "td";
    })
    .slice(1);
}

function getName(td) {
  return td.find(".appname").text();
}

xls 파일로 변환

위의 파싱한 데이터를 내가 원하는 형태로 정리한 후 xls 파일로 저장하였다.
exceljs 패키지를 사용하였다. 코드는 너무 길기 때문에 github 참고.

후기

내 코드의 목표

혹시 코드를 보다가 헷갈릴가봐 첨언하자면, 내 코드의 목표는 날짜별 게임들의 순위 데이터 자체가 아니다. 난 특정 게임의 날짜별 순위를 얻는 것을 목표로 코드를 작성했다.

최적화

최적화를 전혀 하지 않았다. 여러 페이지에 요청을 동시에 보내면 훨씬 빠르게 동작하겠지만, 안타깝게도 페이지 요청 -> 파싱 -> 다음 페이지 요청 -> 파싱 의 구조로 되어 있어 엄청나게 느리다. 그럴일은 없겠지만 나중에 이 코드를 다시 쓸 일이 생긴다면 최적화도 추가해야겠다.

더 쉬운 길

사실 보안이 부실한 많은 사이트들은 네트워크 탭을 까보면 데이터의 API 주소가 공개 되어 있는 경우가 많다. 그런 사이트에서는 그냥 API를 직접 긁어오는 것도 방법이겠다. 하지만 정상적인 방법은 아니기 때문에 급할 때만 추천한다.

Ref

0개의 댓글