본문 바로가기
기타

네이버 뉴스 크롤링 따라하기 - node / request / cheerio / iconv / 웹크롤러 만들기

by jaewooojung 2021. 3. 25.

네이버 뉴스 크롤링 따라 하기

자바스크립트, 노드 공부를 목적으로 하는 크롤링 포스트입니다.

 

1. 프로젝트 세팅

크롤러를 만들기 위해 npm 프로젝트를 생성해 줍니다.

mkdir news-crawler
cd news-crawler
npm init

 

request, cheerio, iconv 패키지 설치

npm install request cheerio iconv

 

루트 폴더에 index.js 파일을 생성해 줍니다

touch index.js

 

코드 편집기 실행

code .

 

 

index.js 파일 상단에 필요한 패키지 3가지를 불러옵니다

const request = require("request");
const cheerio = require("cheerio");
const iconv = require("iconv-lite");

 

getNews() 함수를 작성하고 실행환경 테스트를 위해 콘솔출력을 한번 해줍니다.

const request = require("request");
const cheerio = require("cheerio");
const iconv = require("iconv-lite");

const getNews = () => {
  console.log("getNews function");
};

getNews();

 

터미널에서 index 파일실행

node index.js

 

실행결과

콘솔출력이 잘 되었습니다.

 

2. 구현

크롤링 흐름은 다음과 같습니다.

  1. request 패키지로 html 가져오기
  2. iconv로 디코딩
  3. cheerio로 데이터 추출

 

2-1. request 패키지로 html 가져오기

우선 데이터를 추출할 네이버 뉴스 페이지 url을 확인합니다.

 

 

 

https://news.naver.com/

 

getNews() 함수에서 request 패키지를 사용합니다.

const getNews = () => {
  request(
    {
      url: "https://news.naver.com/",
      method: "GET"
    },
    (error, response, body) => {
      if (error) {
        console.error(error);
        return;
      }
      if (response.statusCode === 200) {
        console.log("response ok");
        // cheerio를 활용하여 body에서 데이터 추출
      }
    }
  );
};

 

옵션으로 url과 method를 전달해 주고 handler 함수에서 전달받은 응답을 처리해 줍니다.

 

중간 테스트

node index.js

 

실행결과

중간 테스트

정상적으로 콘솔 출력 되고 있습니다.

 

에러 테스트

url을 임의로 변경하여 에러를 발생시켜 봅니다.

url: "https://news.naver.com1212121212121212/"
node index.js

 

실행결과

에러 테스트

 

에러 발생되고, 에러 내용이 정상적으로 콘솔출력 되었습니다. url은 다시 수정해 줍니다.

 

크롤링할 부분은 우측의 '언론사별 가장 많이 본 뉴스' 탭입니다.

크롤링 할 부분

 

 

개발자 도구를 열고 목표 영역을 찾아봅니다.

 

 

DOM을 보면 id가 _rankingList0인 <ul> 태그를 찾으실 수 있습니다.

 

여기 하위에 있는 5개의 <li> 태그들이 타겟이 됩니다.

 

추출할 데이터는 <li> 태그 안에 있는

  • 뉴스글 url
  • 뉴스 제목
  • 작성자(언론사)

 

2-2. iconv로 디코딩

response ok인 상태에서 body를 출력해 봅니다.

const getNews = () => {
  request(
    {
      url: "https://news.naver.com/",
      method: "GET"
    },
    (error, response, body) => {
      if (error) {
        console.error(error);
        return;
      }
      if (response.statusCode === 200) {
        console.log("response ok");
        
        console.log(body);
      }
    }
  );
};

 

 

실행결과 일부

body 출력 결과

 

출력되는 <html> 안에 깨진 글자들이 다수 보입니다. 타겟 페이지의 charset이 utf-8이 아닌 경우 위와 같은 현상이 발생합니다.

 

네이버 뉴스 페이지의 <head> 태그를 확인해 봅니다.

charset euc-kr

 

charset이 euc-kr임을 확인하였습니다. euc-kr를 디코딩하기 위해 iconv 패키지를 사용합니다.

 

아래와 같이 bodyDecoded 변수에 디코딩된 결과를 저장합니다.

const bodyDecoded = iconv.decode(body, "euc-kr");		

 

const getNews = () => {
  request(
    {
      url: "https://news.naver.com/",
      method: "GET",
      encoding: null,
    },
    (error, response, body) => {
      if (error) {
        console.error(error);
        return;
      }
      if (response.statusCode === 200) {
        console.log("response ok");

        const bodyDecoded = iconv.decode(body, "euc-kr");

        console.log(bodyDecoded);
      }
    }
  );
};

 

디코딩된 bodyDecoded를 출력해 보면 한글이 정상적으로 출력됩니다.

디코딩 완료

 

 

2-3. cheerio로 데이터 추출

데이터에 접근하기 위해 bodyDecoded를 cheerio 모듈로 load 합니다.

const $ = cheerio.load(bodyDecoded);

 

네이버 뉴스로 돌아가셔서 사이트의 DOM을 보시면 class명이 list_text_inner인 div에 필요한 데이터가 모두 담겨있습니다.

(두 개의 <a> 태그)

 

list_text_inner

 

 

ul#_rankingList > div.list_text_inner

ul#_rankingList0 안에는 div.list_text_inner가 총 5개가 있습니다. 이 5개의 div를 배열로 받아보겠습니다.

 

const list_text_inner_arr = $("#_rankingList0 > li > div > div > div").toArray();

 

 

 

list_text_inner_arr.length // 5

/*
<div class="list_text_inner">...</div>
<div class="list_text_inner">...</div>
<div class="list_text_inner">...</div>
<div class="list_text_inner">...</div>
<div class="list_text_inner">...</div>
*/

 

이 배열을 순회하면서 뉴스글 url, 뉴스제목, 작성자(언론사) 데이터를 추출해 봅니다.

 

순회 시마다 결괏값을 담을 빈 배열 result를 생성한 후 forEach문으로 순회하겠습니다.

if (response.statusCode === 200) {
  console.log("response ok");

  const bodyDecoded = iconv.decode(body, "euc-kr");

  const $ = cheerio.load(bodyDecoded);

  const list_text_inner_arr = $("#_rankingList0 > li > div > div > div").toArray();

  const result = [];
  list_text_inner_arr.forEach((div) => {
    // result에 1. 뉴스글 url / 2. 뉴스제목 / 3. 작성자(언론사) 저장
  });

}

 

첫 번째 <a> 태그에 접근(aFirst)

list_text_inner_arr.forEach((div) => {
  const aFirst = $(div).find("a").first();
});

 

첫 번째 <a> 태그에서 url, title 추출

list_text_inner_arr.forEach((div) => {
  const aFirst = $(div).find("a").first();
  
  const path = aFirst.attr("href"); // 첫번째 <a> 태그 href
  const url = `https://news.naver.com/${path}`; // 도메인을 붙인 url 주소
  const title = aFirst.text().trim(); // trim으로 공백제거

  console.log(url, title) // https://news.naver.com//main/ranking/read.nhn?mode=LSD&mid=shm&sid1=001&oid=437&aid=0000262268&rankingType=RANKING, "나발니, 못 걷는 수준…건강 급격히 악화"  
});

 

두 번째 <a> 태그에 접근(aLast), author 추출

list_text_inner_arr.forEach((div) => {
  const aFirst = $(div).find("a").first();
  
  const path = aFirst.attr("href"); // 첫번째 <a> 태그 href
  const url = `https://news.naver.com/${path}`; // 도메인을 붙인 url 주소
  const title = aFirst.text().trim(); // trim으로 공백제거

  console.log(url, title) // https://news.naver.com//main/ranking/read.nhn?mode=LSD&mid=shm&sid1=001&oid=437&aid=0000262268&rankingType=RANKING, "나발니, 못 걷는 수준…건강 급격히 악화"  

  const aLast = $(div).find("a").last();

  const author = aLast.text().trim();

  console.log(author); // JTBC
});

 

모든 데이터를 추출했으니 result 배열에 객체형태로 넣어주면 완성입니다.

const result = [];
list_text_inner_arr.forEach((div) => {
  const aFirst = $(div).find("a").first(); // 첫번째 <a> 태그
  const path = aFirst.attr("href"); // 첫번째 <a> 태그 url
  const url = `https://news.naver.com/${path}`; // 도메인을 붙인 url 주소
  const title = aFirst.text().trim();

  const aLast = $(div).find("a").last(); // <두번째(마지막) <a>태그
  const author = aLast.text().trim();
  result.push({
    url,
    title,
    author,
  });
});

 

완성 코드

const request = require("request");
const cheerio = require("cheerio");
const iconv = require("iconv-lite");

const getNews = () => {
  request(
  {
    url: "https://news.naver.com/",
    method: "GET",
    encoding: null,
  },
  (error, response, body) => {
    if (error) {
      console.error(error);
      return;
    }
    if (response.statusCode === 200) {
      console.log("response ok");
      const bodyDecoded = iconv.decode(body, "euc-kr");
      const $ = cheerio.load(bodyDecoded);

      const list_text_inner_arr = $(
        "#_rankingList0 > li > div > div > div"
      ).toArray();

      const result = [];
      list_text_inner_arr.forEach((div) => {
        const aFirst = $(div).find("a").first(); // 첫번째 <a> 태그
        const path = aFirst.attr("href"); // 첫번째 <a> 태그 url
        const url = `https://news.naver.com/${path}`; // 도메인을 붙인 url 주소
        const title = aFirst.text().trim();

        const aLast = $(div).find("a").last(); // <두번째(마지막) <a>태그
        const author = aLast.text().trim();
        result.push({
          url,
          title,
          author,
        });
      });
      console.log(result);
    }
  });
};

getNews();

 

3. 최종 실행 결과

최종 실행 결과

 

👏완성

수고하셨습니다!



        
답변을 생성하고 있어요.