node.js를 통한 업무자동화 - 크롤링 02 - axios, iconv-lite, cheerio

하이루·2021년 12월 15일
2

크롤링를 위해 외부에서 데이터를 가져오기

프론트단( Front-End )에서 API를 사용( request할 때 )할 때, Ajax를 사용한 적이 있습니다.
--> 서버에서 만든 기능이 담긴 주소를 호출 할 때 사용

반대로 백엔드단( Back-End ),
서버에서 외부에 있는 자료를 가져오거나 API 주소를 호출 할 때 어떻게 할까?
--> 여러 방법이 있겠지만, 이런 목적으로 axios 라이브러리를 사용


크롤링을 위한 준비

크롤링을 위한 도구인 iconv-lite와 cheerio를 다운 받는 코드

npm install axios cheerio iconv-lite -s

1. axios 라이브러리 --> 서버에서 외부 api에 request

--> 서버에서 외부에 있는 자료나 api 주소를 호출( request )하기 위해 사용하는 라이브러리

--> Ajax( 프론트엔드에서 api 주소를 호출( request )하는 도구 )와 동일한 기능을 서버에서 사용하는 도구

axois 사이트 : https://www.npmjs.com/package/axios

axios 사용법

Ajex와 사용법은 유사함

url: 대상 url (혹은 API )
method: API 호출 방식
responseType: 어떤 방식으로 데이터를 받을 것인지

여기서 url과 method만 적절히 바꿔 사용한다 생각

router폴더에 있는 goods.js에 추가

  const express = require("express");
  const Goods = require("../schemas/Goods");
  const Cart = require("../schemas/Cart");

  const cheerio = require("cheerio");
  const axios = require("axios");
  const iconv = require("iconv-lite");
  const url =
    "http://www.yes24.com/24/Category/BestSeller";

  const router = express.Router();

  router.get("/goods/add/crawling", async (req, res) => {

    try {
      await axios({
        url: url,
        method: "GET",
        responseType: "arraybuffer",
      }).then(async (html) => {

      });
      res.send({ result: "success", message: "크롤링이 완료 되었습니다." });

    } catch (error) {
      console.log(error)
      res.send({ result: "fail", message: "크롤링에 문제가 발생했습니다", error:error });
    }

  });
  
  

try-catch로 묶은 이유
1) 네트워크 상 API 호출 오류
2) 크롤링 할 웹사이트의 오류
등등의 오류발생 가능성 존재


해당 부분에서 다운받은 도구인 iconv-lite 와 cheerio 그리고 서버에서 외부 api에 호출하기 위한 라이브러리인 axios를 import함

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

해당 부분이 데이터를 크롤링 할 주소임

   const url =
    "http://www.yes24.com/24/Category/BestSeller";

해당 부분이 axios 라이브러리를 통해 서버에서 외부 api에 request하는 부분임

       await axios({
        url: url,
        method: "GET",
        responseType: "arraybuffer",
      }).then(async (html) => {

      });
      

--> 위에 설정해 놓은 url 주소에 request
--> get으로 request ----> ( 조회 )

responseType은 arraybuffer
--> responseType은 Json형태나 xml형태 등등으로 설정할 수 있음 --> 여기서 arraybuffer라는 건 html 형태라는 뜻

  즉, arraybuffer로 response를 받는다는 말은 해당 api에 request하여 response 받는 내용을 html코드 형태로 받을 것이라는 뜻

--> 그렇게 받은 response를 람다함수에 넣어, html이라는 매개변수에 대한 요소로 사용


2. iconv-lite --> 외부 api에서 response해준 데이터의 한글깨짐현상 해결

--> 크롤링하려는 html데이터는 한글이 깨지는 형태로 인코딩 되어 있을 가능성이 있음

--> 가져오는 사이트의 데이터 구조와 응답 방식에 따라 한글이 깨지기 쉽상

--> 따라서 한글이 꺠지는 것을 방지하기 위해 가지고 온 html코드를 정제하는 절차가 필요함

위의 코드의 goods.js에 들어가 라우팅한 부분의 코드를 다음과 같이 수정

  router.get("/goods/add/crawling", async (req, res) => {

    try {
      await axios({
        url: url,
        method: "GET",
        responseType: "arraybuffer",
      }).then(async (html) => {

        const content = iconv.decode(html.data, "EUC-KR").toString();

      });
      res.send({ result: "success", message: "크롤링이 완료 되었습니다." });

    } catch (error) {
      console.log(error)
      res.send({ result: "fail", message: "크롤링에 문제가 발생했습니다", error:error });
    }

  });
  

해당 부분이 달라짐

   await axios({
              url: url,
              method: "GET",
              responseType: "arraybuffer",
            }).then(async (html) => {

              const content = iconv.decode(html.data, "EUC-KR").toString();

            });
      

--> 이 부분의 람다함수 부분에 코드 하나가 추가됨 ( 데이터를 한글이 가능한 형태로 가공하기 위해 디코딩 후 인코딩 )

  1. 우리의 request에 대한 response는 html파라미터에 요소로 들어가며, 그 주내용은 data에 할당되어 있음

    --> 그래서 html.data로 그 내용을 불러오는 것

  2. 우리가 request하는 url의 서버는 EUC-KR의 형태로 이루어져있음
    ( 해당 url이 그렇다는 것 --> 크롤링하려는 주소마다 다를 수 있음 )

    --> EUC-KR 형태의 경우, 해당 형태로 response해준 데이터를 그대로 GET할 경우 한글이 깨져서 나옴

    --> 따라서 response 받은 EUC-KR형태의 데이터를 byte단위로 디코딩한 후,
    다시 toString으로 (String형태로) 재인코딩해주는 것으로 한글이 문제없이 나오게 만듬


3. cheerio --> html에서 원하는 데이터를 가져오기

cheerio를 사용하면 jQuery처럼 Node.js에서 HTML데이터를 다룰 수 있게 됨

jQuery란?

--> HTML의 요소들을 조작하는, 편리한 Javascript를 미리 작성해둔 라이브러리

--> HTML 코드중 특정 코드를 가르켜 조작하거나, 데이터를 가져 올 때 편리하게 사용할 수 있는 도구

jQuery의 사용 예시) 둘은 같은 내용의 코드

JavaScript

document.getElementById("element").style.display = "none";

jQuery 사용 --> 훨씬 직관적으로 html코드의 내용을 다룰 수 있음

$('#element').hide();

router폴더의 goods.js에 추가할 내용( 위의 코드에서 추가했던 내용 수정 )

    router.get("/goods/add/crawling", async (req, res) => {

      try {
        await axios({
          url: url,
          method: "GET",
          responseType: "arraybuffer",
        }).then(async (html) => {
                    const content = iconv.decode(html.data, "EUC-KR").toString();
                    const $ = cheerio.load(content);
              const list = $("ol li");
                    await list.each( async (i, tag) => {
                            let desc = $(tag).find("p.copy a").text() 
                    let image = $(tag).find("p.image a img").attr("src")
                    let title = $(tag).find("p.image a img").attr("alt")
                    let price = $(tag).find("p.price strong").text()
            })
        });
        res.send({ result: "success", message: "크롤링이 완료 되었습니다." });

      } catch (error) {
        console.log(error)
        res.send({ result: "fail", message: "크롤링에 문제가 발생했습니다", error:error });
      }
    });

수정된 부분

      ......
      }).then(async (html) => {
                  const content = iconv.decode(html.data, "EUC-KR").toString();
                  
                  const $ = cheerio.load(content);
            	  const list = $("ol li"); 
                  
                  await list.each( async (i, tag) => {
                  let desc = $(tag).find("p.copy a").text() 
                  let image = $(tag).find("p.image a img").attr("src")
                  let title = $(tag).find("p.image a img").attr("alt")
                  let price = $(tag).find("p.price strong").text()
          })
      });
      ......

이 부분에서 한글이 안깨지도록 인코딩된 데이터가 cheerio에 load됨

                  const $ = cheerio.load(content);	
                  
                  

--> 정제한 데이터를 cheerio의 load() 함수에 다음과 같이 넣으면

cheerio.load(content)

jQuery 처럼 사용할 수 있는 준비가 완료됨
--> $라는 변수에 담아서 태그 이름 또는 class, id 이름을 지칭하여 태그를 가르켜 데이터를 가져올 준비를 함

==> cheerio.load() 함수는 데이터를 HTML 코드로 변환하는 변환기 정도의 역할을 함


html코드에서 어느 부분을 가져올 지 확인

예를 들어)

우리가 사용할 html에서 우리가 가져올 데이터는 ol 태그 안에 li 태그 내부에 들어 있음

--> 따라서

이 부분에서 ol안의 li 태그 전부를 가져와 모든 정보를 list라는 변수에 담고

     const list = $("ol li");
     
     
     
--> 이 부분에 대해서 jQuery와 같은 문법이 들어있는데, 아래에 정리해놓음
--> cheerio가 js에서도 jQuery처럼 손쉽게 html을 조작하게 도와주는 도구이기 떄문

이 부분에서 각각의 li 태그들을 리스트의 요소 하나하나를 검색하듯이 반복문을 돌려 tag에서 꺼내 사용하는 것 까지 준비

      await list.each( async (i, tag) => {
      
      
 --> 여기서 Jquery each 함수는 map 함수와 동일한 기능       
   즉, 리스트의 요소 하나하나를 마지막 요소까지 꺼내 확인 할 수 있는 반복문

Jquery each

$.each(object, function(index, item){ });


이후 이부분에서 each 반복문이 반복되는 동안 각각의 데이터에 대해 세부내용을 나눠서 저장

  await list.each( async (i, tag) => {
                    let desc = $(tag).find("p.copy a").text() 
                    let image = $(tag).find("p.image a img").attr("src")
                    let title = $(tag).find("p.image a img").attr("alt")
                    let price = $(tag).find("p.price strong").text()
            })

--> each 의 람다함수 부분에 (i, tag)는 map의 람다함수부분의 (i, value)라고 생각하면 된다.

--> 이 코드의 find함수의 요소를 보면 "p.copy a" 라는 식으로 되어 있는데, 이것은 jQuery형식으로 우리가 원하는 특정 태그에 접근해나가는 방식이다.
( 아래의 jQuery형식의 태그 접근 참고 )

        (우리가 파일특정에 쓰는 절대주소처럼, 일종의 jQuery방식의 태그주소라고 보면된다.)

--> 그리고 코드 뒤쪽에 text(), attr("src"), attr("alt") 등이 있는데, 이것은 해당 태그에서 어느 부분을
가져올지 명시하는 함수들이다.
( 아래의 jQuery형식의 태그 접근 참고 )


jQuery형식의 태그 접근

우리가 절대주소를 사용하여 base/image/a.jpg와 같은 방식으로 파일을 특정하는 것처럼
jQuery는 자신의 방식으로 tag사이에서 특정 tag를 특정한다.
아래에 예시가 있다.

위의 코드의 경우)

"p.copy a" --> <p>태그중에 copy라는 이름( class="copy"인 것 )을 가진 태그를 찾은 뒤, 그 태그 안에 <a>태그 라는 뜻이다.

"p.image a img" --> <p>태그중에 image라는 이름( class="image"인 것 )을 가진 태그를 찾은 뒤, 그 태그 안에 있는 <a>태그 안에 있는 <img>태그라는 뜻이다.

"p.price strong" --> <p>태그중에 price라는 이름( class="price"인 것 )을 가진 태그를 찾은 뒤, 그 태그 안에 <strong>태그 라는 뜻이다.


--> 이후 해당 태그를 find함수로 특정한 후ㅡ, text(), attr("src"), attr("alt")등등이 뒤에 사용되어 있는데, 이것은
우리가 특정한 태그에서 해당 부분을 가져오라는 뜻이다.

    즉, text()는 해당 태그의 택스트 부분의 내용 가져오라는 뜻이다.
    그리고 attr("src")는 해당 태그의 src속성( css영역 ) 부분의 내용을 가져오라는 뜻이다.
    그리고 attr("alt")는 해당 태그의 alt속성( css영역 ) 부분의 내용을 가져오라는 뜻이다.
    

예를 들어)
아래 사진에서
1. 하이라이트된 <a> 태그를 특정한 뒤 .text()를 하면 --> "주린이가 가장 알고 싶은 최다질문 TOP77"을 반환한다.
2. 하이라이트된 <a> 태그를 특정한 뒤 .attr("href")를 하면 --> "/Product/Goods/96644794"를 반환한다.


이렇게 크롤링해 온 data를 mongoDB에 추가

router폴더에 goods.js부분에서 수정한 코드

      router.get("/goods/add/crawling", async (req, res) => {
        try {
          //크롤링 대상 웹사이트 HTML 가져오기
          await axios({
            url: url,
            method: "GET",
            responseType: "arraybuffer",
          }).then(async (html) => {
              //크롤링 코드
            const content = iconv.decode(html.data, "EUC-KR").toString();
            const $ = cheerio.load(content);
            const list = $("ol li");

            await list.each( async (i, tag) => {
              let desc = $(tag).find("p.copy a").text() 
              let image = $(tag).find("p.image a img").attr("src")
              let title = $(tag).find("p.image a img").attr("alt")
              let price = $(tag).find("p.price strong").text()

              if(desc && image && title && price){
                price = price.slice(0,-1).replace(/(,)/g, "")
                let date = new Date()
                let goodsId = date.getTime()
                await Goods.create({
                  goodsId:goodsId,
                  name:title,
                  thumbnailUrl:image,
                  category:"도서",
                  price:price
                })
              }

            });
          })
          res.send({ result: "success", message: "크롤링이 완료 되었습니다." });

        } catch (error) {
          //실패 할 경우 코드
          res.send({ result: "fail", message: "크롤링에 문제가 발생했습니다", error:error });
        }
      });
      

이 부분이 추가됨

    if(desc && image && title && price){
            price = price.slice(0,-1).replace(/(,)/g, "")
            let date = new Date()
            let goodsId = date.getTime()

            await Goods.create({
              goodsId:goodsId,
              name:title,
              thumbnailUrl:image,
              category:"도서",
              price:price
            })
          }
          

해당 부분에서 추출한 데이터가 내가 원하는 데이터가 맞는 지 확인한다.

     if(desc && image && title && price){
     
     --> 반복문을 돌리는 중에 이 데이터가 내가 원하는 데이터가 맞다면 해당 변수들에 모두 값이 할당 되었을 것
     --> 변수에 값이 할당되지 못했다는 것은 해당 <li>태그에 데이터가 없다는 뜻이기 떄문이다.

위에서 데이터 크롤링한 것의 예시)

--> 데이터를 가져온 모습을 보면 안가져와도 되는 정보가 비어있는 태그의 데이터도 가지고 왔음을 확인할 수 있음
--> 따라서 위와 같은 확인이 필요한 것


해당 부분에서 mongoDB에 스키마에 맞게 데이터를 가공해 줌

            price = price.slice(0,-1).replace(/(,)/g, "")
            let date = new Date()
            let goodsId = date.getTime()
            
            

--> 아래의 그림들을 보면,
크롤링해서 들어온 데이터는 제목(a), 이미지url(b), 설명(c), 가격(d)이다.

그리고 스키마에는 goodsId, name, thunbnailUrl, category, price가 있다.

여기서 각각
1. goodsId = 없음 ------> 문제(데이터가 없음)
2. name = 제목(a),
3. thunbnailUrl = 이미지url(b),
4. category = "도서" 로 고정,
5. price = 가격(d) ------> 문제(데이터의 형태)

으로 넣는다고 했을 때, 2가지를 가공해줘야한다.

  1. 먼저 goods스키마의 price의 경우 "type : Number"로 되어 있기 때문에 숫자를 넣어줘야하는데,
    아래 그림의 예시처럼 들어온 데이터를 보면 "16,200원" 이라는 문자열 형태로 되어있기 때문에 이를 가공하여 온전한 숫자로 만들어줘야한다.
  2. goodsId의 경우, 우리가 따로 고유한 정보로 만들어줘야한다.

따라서 이 부분에서 가격(d) 데이터의 "원"을 제거해주고, ","도 있다면 제거해준다.

		price = price.slice(0,-1).replace(/(,)/g, "")
        

그리고 이 부분에서 데이터가 들어온 현재 시간을 고유ID로 사용하여 goodsId에 넣을 준비를 한다. ( 절대 일치할 수 없기 때문 )

            let date = new Date()
            let goodsId = date.getTime()                
            

아래의 그림은 크롤링한 데이터를 넣을 mongoDB의 스키마임

그리고 데이터는 아래와 같은 형식으로 크롤링 되어 들어왔음


이 부분에서 스키마에 맞게 가공한 데이터를 mongoDB에 삽입한다.

    await Goods.create({
                      goodsId:goodsId,
                      name:title,
                      thumbnailUrl:image,
                      category:"도서",
                      price:price
                    })






   
profile
ㅎㅎ

0개의 댓글