2019. 06. 29 수정


1. 크롤링( Crawling )

크롤링(Crawling)이란 여러 웹 페이지에서 필요한 정보들을 골라내어 데이터를 수집하는 활동입니다.

예를 들어, 알라딘의 도서 목록을 데이터화 하고 싶은 경우, 네이버의 실시간 인기 검색어를 가져오고 싶은 경우 등으로 활용할 수 있습니다.


이번 글에서는 "네이버에서 제공하는 실시간 영화순위 목록"을 가져오는 크롤링을 해보도록 하겠습니다.





2. 준비 작업

Node.js에서 크롤링을 하기 위해서는 기본적으로 request와 cheerio 모듈이 필요합니다.

또한 크롤링을 하는 과정에서 인코딩 변환을 위해 iconv 모듈도 필요하므로,

총 3개의 모듈을 설치하도록 하겠습니다.

# npm install request cheerio iconv





3. request 모듈

( request 모듈의 깃헙 페이지는 여기를, 조금 더 자세하게 설명된 블로그는 여기를 참고해주세요. )


request 모듈의 사용 목적은 웹 페이지의 HTML문서를 그대로 가져오기 위함입니다.

이외에도 여러 기능을 제공하지만, request 모듈을 자세히 다루는 글이 아니므로 이번 예제에서 HTML 문서를 가져오기 위한 기능만 살펴보도록 하겠습니다.


request 모듈의 사용법을 직관적으로 이해할 수 있도록 바로 코드를 작성해보겠습니다.

아래의 예제는 "[Naver 영화]"의 메인 페이지 HTML 문서를 콘솔창에 출력하는 코드입니다.

/routes/index.js

const express = require('express');
const router = express.Router();

const request = require('request');

router.get("/crawlingTest", function(req, res, next){
let url = "http://movie.naver.com/movie/sdb/rank/rmovie.nhn";

request(url, function(error, response, body){
console.log(body)
});
})


위의 코드를 작성한 후, node.js 서버를 실행하고 브라우저에서 localhost:3000/crawlingTest를 접근하면, 위 사진과 같은 결과 화면을 볼 수 있습니다.


코드를 살펴보면 request( ) 함수를 호출할 때,

  • 첫 번째 매개변수로 url 경로를 ( 옵션을 추가할 수도 있습니다. )
  • 두 번째 매개변수로 콜백 함수를 작성합니다.
    • 콜백 함수의 두 번째 매개변수( response )는 request 모듈이 가져온 페이지에 대한 여러 정보를 객체로써 저장합니다.
    • 이와 달리 세 번째 매개변수( body )는 위의 사진과 같이 html문서를 그대로 보여준다는 점에서 차이가 있습니다.

그리고 사진을 보시면 네모( ㅁ ) 문자를 볼 수 있는데, 이 문자는 한글 데이터가 깨진 것이므로 인코딩 타입을 UTF-8로 변환해야 합니다.

이 부분은 뒤에서 iconv모듈을 사용할 때 다루도록 하겠습니다.





3. cheerio 모듈

( cheerio 모듈에 대한 깃헙 페이지는 여기를 참고해주세요. )


cheerio 모듈은 HTML 문서를 파싱(parsing)하여 필요한 정보만을 가져올 수 있도록 도와주는 모듈입니다.

위의 request 모듈로 전체 HTML문서를 가져와봤자, 너무 많은 데이터가 있기 때문에 원하는 정보를 얻을 수가 없습니다.

원하는 것은 영화의 제목이지, HTML문서 자체가 아니기 때문이죠.


그렇다면 전체 문서에서 특정 요소의 정보를 가져올 수 있어야 하는데, 이를 도와주는 것이 cheerio 모듈입니다.

위 링크의 깃헙 페이지에서 cheerio 모듈을 다음과 같이 소개하고 있습니다.



즉, cheerio 모듈을 사용하는 방법은 jQuery와 유사하다고 볼 수 있는데,

다시 말하면 특정 HTML 요소에 접근하기 위해서 jQuery 셀럭터를 사용할 수 있다는 것입니다.


코드를 보는 것이 이해가 더 빠를 것이라 생각되므로, 예제를 통해 사용법을 살펴보도록 하겠습니다.

아래의 코드는 위의 request 모듈에서 사용했던 코드를 수정한 것입니다.

/routes/index.js

const express = require('express');
const router = express.Router();

const cheerio = require('cheerio');
const request = require('request');

router.get("/crawlingTest", function(req, res, next){
let url = "http://movie.naver.com/movie/sdb/rank/rmovie.nhn";

request(url, function(error, response, body){
let resultArr = [];

const $ = cheerio.load(body);
let colArr = $(".tit3")
for(let i = 0; i < colArr.length; i++){
resultArr.push(colArr[i].children[1].attribs.title)
}

res.json(resultArr)
});
})

module.exports = router;

request 모듈만 사용했을 때는 해당 페이지의 HTML 문서 전체를 불러올 수 있었습니다.

그런데 cheerio 모듈을 이용하면 그 중에서 필요한 정보들만 가져올 수 있습니다.


cheerio 모듈을 사용하면 서버단에서 jQuery 셀렉터를 사용할 수 있다고 했습니다.

그러기 위해서는 jQuery 기호인 $ 변수에 cheerio.load() 메서드를 호출한 값을 할당해야 합니다.

const $ = cheerio.load(body);

이 때, cheerio.load( ) 메서드의 인자로 request( url )의 실행 결과 데이터인 body를 넘겨줍니다.

그러면 이제 jQuery 셀렉터를 사용하여 HTML 요소에 접근할 수 있습니다.


영화 제목을 추출하기 위해서는 우선, DOM 구조가 어떻게 작성되어 있는지를 알아야 합니다.

[Naver 영화] 페이지에서 개발자도구를 실행 한 뒤, DOM 구조를 살펴보겠습니다.




영화 제목을 감싸고 있는 태그는 class 이름이 tit3인 div 태그이고, 영화 제목은 a태그의 title속성에 정의되어 있습니다.

따라서 위의 예제에서 아래의 코드를 작성하여 영화 제목을 배열에 담을 수 있었습니다.

let resultArr = [];

const $ = cheerio.load(body);
let colArr = $(".tit3")

for(let i = 0; i < colArr.length; i++){
resultArr.push(colArr[i].children[1].attribs.title)
}


그러나 결과화면을 보면 아래 사진과 같이 알아볼 수 없는 데이터가 출력됩니다.



"1987"이라는 영화제목이 출력된 것을 보아 크롤링은 잘 된 것 같은데, 한글이 깨졌습니다.

이를 해결 하기 위해, 다음으로 iconv 모듈을 사용하여 인코딩 타입을 변환하도록 하겠습니다.





4. iconv 모듈

( iconv 모듈에 대한 깃헙 페이지는 여기를 참고해주세요. )


iconv 모듈은 문자열의 인코딩을 변환시켜주는 모듈입니다.

그렇다면 현재 [Naver 영화]가 어떤 인코딩 방식을 사용하는지 알아보아야겠네요.

[Naver 영화] 웹 페이지의 개발자 도구를 실행한 다음 인코딩 타입을 확인해보도록 하겠습니다.



[Naver 영화]의 인코딩 방식이 MS949임을 확인할 수 있습니다.


그런데 iconv 깃헙 페이지를 보시면 지원하는 인코딩 타입에 MS949가 없습니다만,

MS949와 유사한 CP949를 지원하고 있기 때문에, CP949를 UTF-8로 변환하는 코드를 추가하여 라우터 함수를 다시 수정하도록 하겠습니다.



아래의 코드는 [Naver 영화]를 크롤링하는 최종 코드입니다.

/routes/index.js

const express = require('express');
const router = express.Router();

const cheerio = require('cheerio');
const request = require('request');
const Iconv = require('iconv').Iconv;
const iconv = new Iconv('CP949', 'utf-8//translit//ignore');


router.get("/crawlingTest", function(req, res, next){
let url = "http://movie.naver.com/movie/sdb/rank/rmovie.nhn";

request({url, encoding: null}, function(error, response, body){
let htmlDoc = iconv.convert(body).toString();
let resultArr = [];

const $ = cheerio.load(htmlDoc);
let colArr = $(".tit3")
for(let i = 0; i < colArr.length; i++){
resultArr.push(colArr[i].children[1].attribs.title)
}

res.json(resultArr)
});
})

module.exports = router;


iconv 모듈을 사용하여 인코딩 타입을 UTF-8로 변환하니까 제대로 한글 데이터가 출력된 것을 확인할 수 있습니다.

예제에서 인코딩을 바꾼 코드는 다음과 같습니다.

const Iconv = require('iconv').Iconv;
const iconv = new Iconv('CP949', 'utf-8//translit//ignore');

const htmlDoc = iconv.convert(body).toString();

인코딩을 변환하는 메서드는 convert( )이고, 매개변수로 변환할 문자열 buffer로 넘겨주면 됩니다.


Iconv 객체를 생성할 때 넘겨주는 파라미터인 translit ignore 옵션에 대해서는 다음과 같이 소개하고 있습니다.



  • TRANSLIT 옵션
    • 대체 가능한 문자가 있는 경우 해당 문자로 대체할 수 있게 한다.
  • IGNORE 옵션
    • 지원하지 않는 문자의 경우 해당 부분을 무시한다.


예제에서 또 살펴볼 것은 request 모듈로 웹 페이지의 HTML문서를 불러올 때 String이기 때문에,

buffer형태로 받아오기 위해서는 request함수를 호출할 때 첫 번째 인자로 url 경로 뿐만 아니라 encoding 옵션을 추가했습니다.

request({url, encoding: null}, function(error, response, body){
...
}

encoding옵션의 값을 null로 하면 buffer로 받아올 수 있습니다.





이상으로 [Naver 영화]의 실시간 영화 랭킹을 node에서 크롤링해보았습니다.

node에서 크롤링을 하기 위해서는 request, cheerio 모듈이 필요했고, 인코딩을 위해 iconv 모듈이 필요했습니다.

  • request 모듈
    • URL에 해당하는 HTML 문서를 통째로 가져오는 역할
  • cheerio 모듈
    • 가져온 HTML문서 중 필요로 하는 정보를 추출하는 역할을 하며, jQuery방식의 셀렉터를 사용합니다.
  • iconv 모듈
    • 인코딩 문제 해결

또한 본문에서 언급하지는 않았지만 iconv 모듈을 사용하여 인코딩 타입을 변환할 필요없이,

cheerio-httpcli 모듈을 사용하면 cheerio 모듈과 함께 jQuery 방식의 셀렉터 사용뿐만 아니라 웹 페이지의 문자 타입을 자동으로 변환할 수 있습니다. cheerio-httpcli 깃헙 페이지는 여기를 참고해주세요 !



[ 참고 ] 

https://medium.com/@jinro4/%EA%B0%9C%EB%B0%9C-iconv%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%9C-euc-kr-%EB%AC%B8%EC%84%9C-%ED%8C%8C%EC%8B%B1%ED%95%98%EA%B8%B0-95caeebb9678

댓글 펼치기 👇
  1. Favicon of https://honeystorage.tistory.com Websterking 2019.01.06 00:13 신고

    유익하네요. 따라해봐야겠습니다.

  2. Favicon of https://honeystorage.tistory.com Websterking 2019.01.06 12:55 신고

    예제 따라해 보았습니다 ^^ 훌륭하네요 . 앞으로도 좋은 포스팅 부탁드립니다.

  3. jindu 2019.01.12 21:02

    위에 친절하게 알려주셔서 따라해보았습니다
    예제 따라해 본 결과 서버를 실행하고 브라우저에서 열면
    ["������","�ָԿ� ���� 2: ���ͳ� ������
    이 값은 출력이됩니다
    하지만 인코딩변환 해주는 코드를 추가하게 되면
    TypeError: Router.use() require a middleware function but got a Object 문구를 시작으로 오류문구가 쫙뜨네요
    뭐가 문제인지 알려주실수 있으신가요?

    그리고 웹브라우저에서 결과화면을 출력하지않고
    cmd에서 node index.js 를 입력했을 때 바로 결과화면이 출력되게 하려면
    console.log 를 이용해서 소스코드만 바꾸면 가능한 일인가요?

    • Favicon of https://victorydntmd.tistory.com victolee 우르르응 2019.01.13 13:01 신고

      언급해주신 에러는 /routes/index.js 파일 제일 하단에
      module.exports = router;
      코드가 빠진 것이 아닌가 생각이 됩니다!
      ( 본문에 내용 추가 했습니다. )

      cmd 창에서 결과를 보고 싶으시면,
      이 글 처음 예제에서 사용했던 것처럼 console.log()를 호출하시면 됩니다~

  4. 2019.03.02 14:24

    비밀댓글입니다

    • Favicon of https://victorydntmd.tistory.com victolee 우르르응 2019.03.04 21:20 신고

      안녕하세요~

      문의주신 부분이 명확하지 않아서, 이해가 잘 안되네요 ㅎㅎ...

      1. 말씀 주신대로 인코딩 문제는 iconv로 해결할 수 있을 것 같습니다.

      2. 로컬 개발 환경이라면, 코드를 실행한 것이 node.js 서버를 실행한 것과 같습니다. 서버 접근을 어떻게 한다는게 무슨 의미인지 잘 모르겠습니다.

      3. index.php가 무엇을 의미하는지,
      Content-Type으로 무엇을 궁금해 하시는지 파악하기 힘듭니다.
      Content-Type:application/octet-stream이 궁금하시다면 이 링크를 참고해보세요~ (https://developer.mozilla.org/ko/docs/Web/HTTP/Basics_of_HTTP/MIME_types#%EC%A0%95%ED%99%95%ED%95%9C_MIME_%ED%83%80%EC%9E%85_%EC%84%A4%EC%A0%95%EC%9D%98_%EC%A4%91%EC%9A%94%EC%84%B1)


      제가 실력이 부족해서 명확하게 말해주지 않으면 뭐가 궁금한건지 잘 몰라요....ㅎㅎ 다시 질문해주시면 같이 찾아볼게요.

  5. Favicon of https://jj100.tistory.com jjwhite 2019.06.29 09:28 신고

    유용한 글 감사합니다.
    본 예제를 실행 해 보려는데

    "서버를 실행하고 브라우저에서 localhost:3000/crawlingTest 를 요청하여 접근하면,"

    부분에서 "서버" 가 웹서버를 의미하나요? 작성된 코드를 의미하나요?

  6. Favicon of https://hanorange.tistory.com hanorange 2019.08.16 14:49 신고

    좋은 글 감사합니다. 혹시 여러페이지 동시 크롤링 방법에 대해 알 수 있을까요?

    • Favicon of https://victorydntmd.tistory.com victolee 우르르응 2019.08.25 12:09 신고

      글쎄요~ 해보지 않아서 잘 모르겠네요.

      클러스터링 및 병렬실행을 통해 해결 할 수 있을 것 같아요.
      https://www.slideshare.net/JoenggyuLenKim/nodejs-crawling

      관련 라이브러리도 있는것 같은데, 한 번 참고해보시면 좋을 것 같네요.
      https://github.com/apifytech/apify-js

  7. kevino 2019.11.12 04:48

    https://band.us/ 네이버밴드는 왜안되나유ㅠ