[Node.js] Crawling (feat. Cheerio)

29816 단어 node.jsnode.js

Crawling (크롤링)

웹 사이트를 접속해, 원하는 데이터를 추출하는 것

  • Crawling(기어가다, 기다) 이라는 단어처럼, 크롤링의 행위가 마치 웹 사이트를 기어다니며 정보를 수집하는 것과 비슷하다고 표현되기 시작하면서 생겨난 것으로 추측된다.
  • 주로 업무 자동화나 많은 양의 데이터를 효율적으로 수집하기 위해 Crawling 방법을 사용한다.
  • Crawling은 Python이 유명하지만, JavaScript나 다른 언어로도 가능하다.
  • Crawling을 수행하는 프로그램을 'Crawler(크롤러)' 라고 부를 수 있다.
  • 해당 글에서는 JavaScript를 통해 [YES24] 베스트 셀러' 페이지 데이터를 Crawling 해보도록 한다.

Crawling 준비

1. 패키지 설치

  • 이번 실습에서는 아래 3개의 라이브러리를 통해 Crawling을 실습할 예정이다.
  1. axios: HTTP 요청을 할 수 있도록 해주는 패키지
  2. cheerio: 웹 페이지에 있는 데이터를 쉽게 가공할 수 있도록 도와주는 패키지
  3. iconv-lite: 특정 웹 페이지에서는 한자, 한글, 이모지와 같은 유니코드 문자열이 깨질수 있는데, 문자가 깨지지 않도록 후처리해주는 패키지
  • 프로젝트를 생성 후, 프로젝트 폴더에서 아래 명령어를 통해 패키지를 설치한다.
// init
// -y 명령어를 통해, 모두 자동으로 동의하도록 해준다.
npm init -y

// install packages
// 띄어쓰기를 통해 여러 Package를 한번에 설치한다.
npm i axios cheerio iconv-lite
  • package.json 파일에 아래 이미지처럼 생성되었다면, 설치가 성공한 것이다.

2. 구조 확인하기

(1) 베스트 셀러 책 수 확인하기

  • 우선, 베스트 셀러에서 가져와야 할 책 목록이 몇 개인지부터 살펴본다. 아래 이미지에서 확인할 수 있듯, 총 40권의 책 목록이 제공되고 있다.

(2) selector로 위치 확인하기

  • 그럼, 이제 해당 책들을 어떻게 구성하여 보여주고 있는지 html 태그를 확인해보도록 한다. '개발자도구'를 열어 1위에 해당하는 책 영역을 살펴보면, <li>태그로 감싸져있는 것을 볼 수 있고 해당 태그의 클래스는 1부터 40까지 존재한다. 즉, 1위부터 40위까지의 책이 <li class="num00">의 형태로 존재하고 있음을 확인할 수 있다.

  • 40권의 책에 대한 모든 데이터를 확인하기 위해서 번호 하나씩 모두 확인하는 방법도 있겠지만, 위에서 살펴본 규칙을 통해 한번에 데이터를 가져올 수도 있을 것이다.

  • 우선, 1위 책에 대한 html 태그에서 우클릭 후, copy > copy selector 를 클릭해보자. 그리고, 해당 영역에서 cmd+f 키를 눌러, 검색창에 copy된 내용을 붙여넣기하면 아래와 같이 하나의 결과가 찾아짐을 확인할 수 있다.

  • css selector를 살펴보면 li.num1 이라는 것으로 검색을 하고 있는데, 이 것은 <li> 태그 중에서 num1 이라는 클래스를 가진 항목을 찾는다는 의미이다. 우리는 num1부터 num40까지의 값을 모두 가져와야하기 때문에, .num1을 제거하고 다시 검색해보자.

  • 아래 이미지와 같이 총 40개의 검색 결과가 나온 것을 볼 수 있다. 여기서 화살표를 아래로 내려보면, 우리가 원하는대로 1위부터 40위까지의 책 영역이 선택되는 것을 확인할 수 있다.

  • 1위부터 40위까지의 책 데이터가 어떤 html 속성으로 구성되어있는지 확인했으므로, 손쉽게 가져올 수 있게 되었다. 이제, 직접 Crawling을 해 볼 차례이다.


원하는 데이터 Crawling 하기

1. axios로 데이터 받아오기

  • 우선, axios를 통해 GET 요청을 보낸 후 화면을 구성하는 데이터들을 받아온다.
// index.js

const axios = require('axios');

// axios는 Promise를 반환하기 때문에 then, catch를 통해 chaining 할 수 있다.
axios({
    // 크롤링을 원하는 페이지 URL
    url: 'http://www.yes24.com/24/Category/BestSeller',
    method: 'GET',
})
    // 성공했을 경우
    .then(response => {
        console.log(response.data);
    })
    // 실패했을 경우
    .catch(err => {
        console.error(err);
    });
  • 위와 같이 axios 코드를 실행하면, 아래 이미지처럼 데이터를 받아온다. 하지만, 한글이 깨져보이는 것을 확인할 수 있다.

2. iconv-lite으로 Decoding하기

  • iconv-lite 모듈은 만약 '영문으로만 된 사이트' 이거나, 최근에 만들어진 사이트라면 사용할 필요가 없다. 조금 오래된 웹 사이트 중, Encoding 형식이 'EUC-KR'과 같은 형식일 경우에만 사용하면 되는 모듈이기 때문에, 크롤링 하고자 하는 웹 사이트를 잘 확인해서 사용하면 된다.

  • YES24 웹 사이트의 경우 charset이 UTF-8이 아닌 EUC-KR로 설정되어 있다. 따라서, axios로 데이터를 받아오면 한글이 깨지는 현상이 나타난다. 실제로 YES24 페이지에서 개발자 도구를 통해 확인해보면, charset=euc-kr로 설정되어있는 것을 볼 수 있다.

  • 즉, EUC-KR을 UTF-8로 Decoding 하기 위해 iconv-lite 패키지를 사용하는 것이다.

  • iconv-lite는 ArrayBuffer라는 형식을 받는데, axios에서 해당 형식으로 데이터를 받을 수 있다.

    1. axios에서 responseType: 'arraybuffer' 로 지정해준다.
    2. 성공했을 경우의 response.data를 iconv를 통해 decode 해준다.
    3. 만약 정상적으로 데이터가 출력되지 않는다면, .toString() 메서드를 통해 string으로 변환한다.
const axios = require('axios');
const iconv = require('iconv-lite');

axios({
    // 크롤링을 원하는 페이지 URL
    url: 'http://www.yes24.com/24/Category/BestSeller',
    method: 'GET',
    responseType: 'arraybuffer',
})
    // 성공했을 경우
    .then(response => {
        // 만약 content가 정상적으로 출력되지 않는다면, arraybuffer 타입으로 되어있기 때문일 수 있다.
        // 현재는 string으로 반환되지만, 만약 다르게 출력된다면 뒤에 .toString() 메서드를 호출하면 된다.
        const content = iconv.decode(response.data, 'EUC-KR');
        console.log(content);
    })
    // 실패했을 경우
    .catch(err => {
        console.error(err);
    });
  • 이제 한글이 잘 나오는 것을 확인할 수 있다.

3. cheerio 모듈로 데이터 추출

(1) cheerio 기본 사용법

  • 우선, cheerio 모듈을 import하고, 우리가 axios로 받아온 데이터 소스들을 cheerio에 로드 시켜준다. 아래 예시를 통해 간단하게 살펴보자.
const cheerio = require('cheerio');
...

const $ = cheerio.load('<h1 class="title">WebSite</h1>');
$('h1.title').text();  // 'WebSite'
  • 위 코드를 조금 살펴보자. cheerio.load() 메서드 안에 html 소스들을 넣고, 그 안에서 찾을 수 있도록 $ 변수에 할당해준다. $()로 표기하는 방식은 JQuery에서 주로 사용하는 형식인데, 크롤링을 할 때에는 이러한 형식을 많이 사용한다.

  • 위에서 보면, $('h1.title').text()의 결과로 'WebSite'가 나온 것을 볼 수 있다. 그렇다면 왜 이렇게 되는 것인지, 소스를 뜯어보면 아래와 같다.

    $(): 태그와 클래스를 통해 어떤 데이터의 위치를 선택
    h1.title: h1 태그 중, 클래스가 title 인 것을 의미함
    text(): 해당 태그에서, 'text'를 가져옴

(2) 베스트 셀러 제목 가져오기

  • 위의 방법대로 응용해서, 우리가 하고자 하는 YES24 베스트 셀러 40위까지의 제목을 가져와보도록 하자. 위에서 살펴본 방법대로, 개발자 도구에서 copy selector를 통해 '제목'의 위치를 가져온다.

  • 제목의 위치는 #bestList > ol > li.num1 > p:nth-child(3) > a 로 확인이 되는데, 마찬가지로 위의 방법대로 li.num1에서 num1을 제외하면 40개의 제목이 모두 확인되는 것을 알 수 있다. 이를 이용해서, 1위~40위 까지의 제목을 가져와보도록 한다.

const axios = require('axios');
const iconv = require('iconv-lite');
const cheerio = require('cheerio');

axios({
    // 크롤링을 원하는 페이지 URL
    url: 'http://www.yes24.com/24/Category/BestSeller',
    method: 'GET',
    responseType: 'arraybuffer',
})
    // 성공했을 경우
    .then(response => {
        // 만약 content가 정상적으로 출력되지 않는다면, arraybuffer 타입으로 되어있기 때문일 수 있다.
        // 현재는 string으로 반환되지만, 만약 다르게 출력된다면 뒤에 .toString() 메서드를 호출하면 된다.
        const content = iconv.decode(response.data, 'EUC-KR');
        const $ = cheerio.load(content);
        const titles = $('#bestList > ol > li > p:nth-child(3) > a').text();
        console.log(titles);
    })
    // 실패했을 경우
    .catch(err => {
        console.error(err);
    });
  • 위 코드를 실행해보면, 아래와 같이 1위부터 40위까지의 책 제목이 쭉 나열되어있는 것을 볼 수 있다.

(3) each()로 여러 항목 가져오기

  • 이렇게 여러 항목들을 효과적으로 가져오기 위해서, cheerio에서 제공하는 'each' 메서드를 사용할 수 있다. cheerio의 each 메서드에 대한 사용법은, cheerio 공식문서 링크에서 확인할 수 있다.
const content = iconv.decode(response.data, 'EUC-KR');
const $ = cheerio.load(content);
const titleSelector = '#bestList > ol > li > p:nth-child(3) > a';
$(titleSelector).each((i, elem) => {
    const title = $(elem).text();
    console.log(i + 1, title);
});
  • 위와같이 코드를 작성해주면, 아래의 결과를 확인할 수 있다. index는 0부터 시작하므로, 순위를 나타내기 위해 +1로 출력했고, $(elem).text()를 통해 제목을 가져왔다.

4. 순위별로 더 많은 데이터 추출하기

  • 지금까지는 하나의 항목(제목)을 가져오는 방법을 살펴보았다. 하지만 위의 방법으로는 각 순위별 책의 '가격, 제목, 설명, 책 이미지' 등을 모두 가져오기는 쉽지 않아보인다. 따라서, 각 순위별 책의 항목들을 가져오는 방법을 실습해보도록 한다.

  • 우선, 영역이 어떻게 나누어져 있는지 한번 확인해보도록 한다. 아래 이미지처럼, 순위에 맞는 책에 대한 설명, 제목, 저자, 출판사, 가격 등이 모두 하나의 html 태그 안에 포함되어있는 것처럼 보인다.

  • 이제 개발자 도구에서 책 하나에 대한 태그를 선택해서 열어보자. 예상대로, 위에서 확인해 본 항목들이 확인된다.

  • 이제 우리는 이것을 이용해서 '설명', '제목', '가격', '이미지'를 가져와보도록 하겠다. 이전 코드에서는 1위~40위까지 '제목' 태그만 each() 를 통해 반복했다면, 이제는 '책 한 권'에 대한 태그를 반복하면서 우리가 원하는 각각의 항목들을 출력해보면 될 것이다.

  • 먼저, 1위~40위까지는 #bestList > ol > li 임을 위에서 살펴보았다. 이 중에서, 원하는 각 항목에 접근하는 방법을 살펴보도록 하자. 개발자 도구를 통해 '설명'을 클릭하면, 아래처럼 해당 태그로 이동하는 것을 확인할 수 있다. (어떻게 클릭하는지 모르겠다면, 개발자 도구 창의 좌측 상단 '네모와 마우스'가 있는 이미지' 쪽을 클릭하거나, cmd + shift + c 를 입력 후 마우스로 클릭해보면 영역 선택이 된다.)

  • 태그 좌측의 화살표를 이용해서 항목을 펼쳐보면, 해당 코드는 <li> 하위에 있는 <p class="copy"> 태그의 하위에 <a href=....>{설명}</a>으로 위치해있다는 것을 알 수 있다. 이 것을 selector로 선택하는 방법을 알아보도록 한다. 아래의 규칙만 알면, 어렵지 않게 선택할 수 있다.

#: id
.: class
>: 하위 태그
attr("attribute name"): 속성명에 대한 속성

  • 우리가 위에서 살펴본 1위~40위까지 각 책에 대한 selector는 #bestList > ol > li 였다. 즉, id가 'bestList'인 것의 하위에 있는 ol 태그를 선택한 후, 그 하위에 있는 li 태그를 찾는 것이다. 그럼, 우리가 가져와야하는 '설명'은 #bestList > ol > li > p.copy > a가 되고, 그 것의 text를 가져와야 하므로 .text() 메서드를 호출하면 된다.
  • 예상대로 설명만 잘 가져오는 것을 확인했으므로, 이제 각 항목을 어떻게 가져오는지 대충 감을 잡았다. 그럼, 1위~40위까지의 selector를 each()로 반복하면서, 각각의 값을 가져와보도록 하겠다. 하위 selector를 찾기 위해서는 .find() 메서드를 사용하면 아주 간단하게 가져올 수 있다. (여기서부터는 selector를 찾는 설명은 생략한다.)

  • #bestList > ol > li를 통해 각 책에 대한 코드를 반복하면서, 원하는 항목인 제목, 설명, 가격, 이미지 주소를 크롤링 해보았다. 위 설명과 함께 아래의 이미지와 코드를 참고하면 바로 이해를 할 수 있을 것이다.

// 최종 코드
// index.js

const axios = require('axios');
const iconv = require('iconv-lite');
const cheerio = require('cheerio');

axios({
    // 크롤링을 원하는 페이지 URL
    url: 'http://www.yes24.com/24/Category/BestSeller',
    method: 'GET',
    responseType: 'arraybuffer',
})
    // 성공했을 경우
    .then(response => {
        // 만약 content가 정상적으로 출력되지 않는다면, arraybuffer 타입으로 되어있기 때문일 수 있다.
        // 현재는 string으로 반환되지만, 만약 다르게 출력된다면 뒤에 .toString() 메서드를 호출하면 된다.
        const content = iconv.decode(response.data, 'EUC-KR');
        const $ = cheerio.load(content);

        // 1위~40위까지의 책들에 대한 selector
        const booksSelector = '#bestList > ol > li';
        $(booksSelector).each((i, elem) => {
            const title = $(elem).find('p:nth-child(3) > a').text();
            const description = $(elem).find('p.copy > a').text();
            const price = $(elem).find('p.price > strong').text();
            const imgUrl = $(elem).find('p.image > a > img').attr('src');
            console.log(i + 1, {
                title,
                description,
                price,
                imgUrl,
            });
        });
    })
    // 실패했을 경우
    .catch(err => {
        console.error(err);
    });

좋은 웹페이지 즐겨찾기