Programming/JS +

Node.js를 이용한 뉴스 기사 크롤링 방법

joah.k 2023. 2. 14. 16:20
728x90

 

Node.js 를 이용하여 뉴스 기사를 크롤링해서 웹 페이지에 가져오자!  

 

1. Node.js와 관련된 패키지 설치 


서버 구축을 위해 NPM을 통해 패키지를 설치한다. 

 

# NPM

NPM은  Node.js와 관련한 여러 패키지들을 설치하고 관리할 때 사용되는 패키지 매니저이다. 

이 때 npm 안에는 패키지들이 서로 의존되어 있어 하나의 문제가 발생하면 연쇄적으로 문제가 발생할 수 있다.
이를 관리하기 위해 package.json 파일로 관리하는데, 
npm을 설치하면 프로젝트 폴더 내에 package.json 이라는 파일이 새로 생성된다. 

터미널에 npm init 을 명령어를 입력해 npm을 쓸 수 있는 초기 환경을 셋팅한다. 

npm init -y

package.json 파일이 새로 생성되었다. 

(-y 옵션을 사용하면, npm init 명령어를 실행하는 과정에서 생성될 package.json 파일에 대해 물어보는 대화형 프로세스를 건너뛰고 기본값으로 자동으로 설정됨. 이렇게 하면 프로젝트를 더 빠르게 시작할 수 있다.) 

 

 

# NPM을 통한 패키지 설치

서버를 구축하기 위한 준비가 필요하다. 

서버를 쉽게 짤 수 있게 도와주는 npm의 express 라이브러리를 추가한다.

설치가 되면 다음과 같이 node_modules 폴더가 생성되는 것을 볼 수 있다.

npm install express

 

- 추가로 크롤링 기능을 위해 axioscheerio 라이브러리를 설치한다. 
- 나는 news.html 이라는 파일을 따로 만들어 관리하기로 해서 ejs를 추가로 설치하였다. ejs를 통해 HTML 파일에 js 코드를 삽입하여 동적인 HTML 을 생성할 수 있다.  

npm install axios cheerio ejs

관련된 설정들이 package.json 파일에 추가된 것을 확인할 수 있다. 

정리하자면.. express를 통해 서버 구성을 하고, 
axios와 cheerio 라이브러리를 사용하여 네이버 뉴스 페이지에서 기사 제목, 링크, 언론사 정보를 파싱하여 가져오고 
ejs 를 사용하여 해당 정보를 동적으로 렌더링 하여 사용자단에서 보여주는 웹 페이지를 구성할 계획이다. 

 

2.  뉴스 페이지 크롤러 구성   


# 가져올 데이터 선정

네이버 뉴스 내 '언론사별 가장 많이 본 뉴스' 정보를 가져오려 한다. 

자세한 영역 확인을 위해 크롬 개발자 도구를 이용, 해당 내용을 클릭하면 소스를 확인할 수 있다. 

 

먼저, 필요한 모듈을 불러오고 ejs를 사용하기 위한 설정을 추가한다. 

const express = require("express");
const axios = require("axios");
const cheerio = require("cheerio");
const app = express();
const port = 8090;

// news.ejs를 사용하기 위한 설정
app.set("view engine", "ejs");

 

그리고 뉴스 크롤링을 해와야 함. 
axios.get() 메서드를 통해 서버에 GET 요청을 보내야 한다. 

파라미터에는 요청을 보낼 URL인 네이버 뉴스 페이지를 전달한다. 

응답 데이터는 JSON 형식 문자열로 반환되므로 data 속성만 추출하여 변수 data에 할당한다. 

// 크롤링 할 대상 페이지의 JSON data 를 가져옴
const { data } = await axios.get(
      "https://news.naver.com/main/main.naver?mode=LSD&mid=shm&sid1=101"
    );
    
// 가져온 데이터를 Cheerio를 이용해 파싱, 보통 객채명을 $으로 설정
const $ = cheerio.load(data);

 

네이버 '언론사별 가장 많이 본 뉴스' 구성은 이렇게 id값이 '_rankingList0' 인 ul 태그 아래에 <li>가 속한 구조. 

 

<li> 태그를 자세히 보면 <div> 가 세 번 감싸져 있다. 

 

    // 뉴스 목록 중에 필요한 정보를 가져옴
    const ranking_news_arr = $("#_rankingList0 > li > div > div > div").toArray();

cheerio를 통해 HTML 문서에 접근, HTML 요소 중에서 _rankingList0이라는 ID를 가진 요소의 하위 요소들 중에서 li > div > div > div에 해당하는 모든 요소를 선택한다. 

그리고 toArray() 함수를 통해 선택된 HTML 요소들을 배열로 반환한다. 

news 라는 배열에 데이터를 담을건데, forEach() 함수를 이용, 배열의 각 요소마다 콜백 함수를 실행한다. 

앞서 cheerio를 통해 담은 정보를 'ranking_news_arr'에 담았으니 

배열 각 요소에 대해 콜백 함수가 실행된다.

// 결과값을 담을 빈 배열을 생성
    const news = [];
    
    ranking_news_arr.forEach((div) => {
        // 타이틀, 링크
        const aFirst = $(div).find("a").first(); //div 안에서 처음 나타나는 'a' 태그 선택
        const title = aFirst.text().trim(); 
        const path = aFirst.attr("href"); //path 변수에 저장하고 
        const url = `https://news.naver.com/${path}`; //path를 이용, 뉴스기사 전체 url 저장 
        
        // 언론사        
        const aLast = $(div).find("a").last(); // div 내 가장 마지막에 나타나는 'a'태그 선택
        const author = aLast.text().trim(); //문자열 앞뒤 공백 제거
        
        console.log(url, title, author);
        
        // 배열에 객체 형태로 넣어줌
      news.push({
            url,title,author,
        });
    });
      return news;

 

3.  서버 라우팅 및 구동


express 를 이용해 서버를 구동하려 한다. 

/news 경로에 접근 시 getNews 함수를 호출하여 추출한 뉴스 데이터를 렌더링 하는 것이 목적. 

- async/await 를 사용하여 getNews 함수의 반환값을 가져온다 => async 함수의 await 키워드는 getNews() 함수가 끝나기 전까지 const article = await getNews(); 코드 이후의 코드 실행을 멈추고 기다리다가, getNews() 함수가 성공하면 그제서야 article 변수에 반환값이 할당되고 이후 코드가 실행된다. 

- getNews() 함수를 통해 반환한 데이터를 article 객체의 형태로 ejs 템플릿 파일에 전송 

- 오류 발생 시 에러 코드 처리 

- app. listen 함수를 사용해 웹 서비스를 구동한다. 

// news 페이지 라우팅
app.get("/news", async (req, res) => {
    try {
      const article = await getNews(); // getNews 함수에서 반환하는 값으로 변수명 수정
      res.render("news", { article }); // article 객체를 전달
    } catch (error) {
      console.error("error handling response:", error);
      res.status(500).send("서버 에러 발생");
    }
  });
  

// 서버 구동
app.listen(port, () => {
  console.log(`Server running on port ${port}`);
});

 

그리고 보낸 정보를 받아서 클라이언트단에 보여줄 news.ejs 는 다음과 같다. 

getNews 함수에서 반환한 news 배열을 받아 HTML 문서를 동적으로 생성할건데, 

article 배열의 각 요소 'item'을 순회하여 title, link, author 정보를 가져와 HTML 태그에 삽입된다. 

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <title>뉴스</title>
  </head>
  <body>
    <h1>언론사별 가장 많이 본 뉴스 리스트</h1>
    <table>
      <thead>
        <tr>
          <th>기사제목</th>
          <th>기사링크</th>
          <th>언론사</th>
        </tr>
      </thead>
      <tbody>
        <% article.forEach((item) => { %>
          <tr>
            <td><%- item.title %></td>
            <td><%- item.url %></td>
            <td><%- item.author %></td>
          </tr>
        <% }); %>        
      </tbody>
    </table>
  </body>
</html>

 

서버 구동 후 http://localhost:8090/news 에 들어가보면...^^

글씨가 깨져있는 것을 볼 수 있었다.. 

 

네이버 뉴스의 소스코드를 확인해보면 

chartset 이 'euc-kr' 사이트라 크롤링한 데이터를 처리하는데 디코딩 문제가 생긴 것이다. 

 

4.  디코딩 처리 


디코딩 처리를 하기 위해 iconv 패키지를 추가 설치해야 한다.  

--- npm install iconv 

그리고  app.js 파일 상단에 const iconv = require('iconv-lite'); 추가 

그리고 getNews() 함수 부분에 decoded 변수를 추가해 cheerio로 파싱하기 전에 디코드 처리를 한다.

 

 

그럼 다음과 같이 한글이 잘 출력되는 것을 볼 수 있다! 

 

 

그런데 또 문제점을 발견. 

기사링크 보니까 https:// 가 반복되어있다.
url 변수를 만들 때 https://news.naver.com/${path} 라고 했는데, path에는 이미 https://가 포함된 전체 URL이 들어가 있기 때문에 이중으로 https://가 들어가는 문제가 발생한 것..! 

변수를 그냥 path로 수정하니까 제대로 링크가 나왔다. 끝 ! 

728x90