본문 바로가기
tech documents/node

[Cheerio],[Axios] 모듈을 사용한 웹 크롤링 - 2

by kimtahen 2020. 4. 1.
반응형

  이전 포스팅에서 책의 목차를 크롤링하여, 배열에 URL을 저장하는 작업까지 완료하였다. 현재까지의 코드는 아래와 같다.

/fetching.js

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

let $href = [];
axios.get(`https://thebook.io/080212`)
    .then(dataa => {
        const $ = cheerio.load(dataa.data);
        $('section.book-toc>ul>li>a').each((index, item)=>{$href.push(item.attribs.href)});
        console.log($href);
    });

$href 배열에는 아래와 같이 URL이 담겨 있다. 

 

이 URL에 대해 다시 axios 모듈로 요청을 보내서, 책의 내용을 크롤링 해야 한다. 하지만 무작정 for문이 이 $href 배열을 순회하며, 무작정 axios 요청을 보낼 수만은 없다. 왜냐하면 axios 모듈은 비동기로 요청과 작업이 이루어지는데, 이 때문에 크롤링이 끝나기도 전에 for문이 끝나버릴 수도 있기 때문이다. 따라서 axios get 요청에서 크롤링이 끝나기 전에 for문이 실행되는 것을 방지해야 한다.

 

예시로 아래와 같은 코드를 작성하고 실행해보자.

function fetch(i) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve(i);
        }, 3000);
    })
}

(async () => {
    for (let i = 0; i < 10; i++) {
        let data = await fetch(i);
        console.log(`for loop finished, data:${data}`);
    }
})();

이 코드에서는 for 문의 실행을 잠시 멈추기 위해 javascript의 async/await 문법을 사용한다. 

async 함수에는 await식이 포함될 수 있습니다. 이 식은 async 함수의 실행을 일시 중지하고 전달 된 Promise의 해결을 기다린 다음 async 함수의 실행을 다시 시작하고 완료후 값을 반환합니다.

async 함수와 await 식은 위와 같이 동작한다.

위 코드에서 fetch 함수는 3초후에 resolve 상태인 Promise를 데이터와 함께 반환한다. 그리고 for문의 멈춤 기능을 위하여 IIFE, 즉시 실행함수를 사용하였다.

 

IIFE 는

()();

 이와 같이 두개의 괄호가 쓰이는데, 앞의 괄호에는 함수를 넣어주면 되고, 두번째 괄호에는 앞의 함수에 입력되는 파라미터가 들어간다. 파라미터와 함께 앞의 괄호에 쓰인 함수는 호출없이 즉시 실행된다.

 

fetch 함수의 반환이 완료될때 까지 for 문을 멈추어야 하기 때문에 fetch함수를 await로 호출해야한다. 그리고 이 await는 async 함수의 작동을 일시적으로 멈추는 역할을 하기 때문에, async와 함께 쓰여야 한다. 그래서 IIFE 로 async 함수를 만들고, 그 안에 for문을 넣어서 즉시 실행되도록 하였다. 

 

직접 저 코드를 실행시켜보면 

이와 같이 3초마다 for 문이 한번씩 실행되는 것을 확인할 수 있다.

 

다시 본문으로 돌아와서 async/await을 이용해 axios 요청을 보내는 모드를 작성해보자. 일단 위의 예시에서 사용했던 fetching 함수 처럼 프로미스를 반환하는, axios를 통해 크롤링하는 함수를 작성한다.

function contentLoad(URL) {
    return new Promise((resolve, reject) => {
        axios.get(`https://thebook.io${URL}`)
            .then(res => {
                const $ = cheerio.load(res.data);
                bookContents += `<div style="border: 1px solid black;padding: 0 10px;">${$.html($('section#page_content'))}</div><br>`;
            })
            .then(() => {
                resolve();
            });
    })
}

여기서는 axios로 $href 의 URL에 요청을 하여, html을 가져오고 이를 cheerio 모듈로 로드한다. 이후 책 내용에 해당하는section#page_content 부분을 선택자로 선택한뒤, 이를 다시 html로 변환하고 약간의 html태그와 css style을 추가하여 bookContents 변수에 문자열로 연결한다. 다시 html로 변환하는 이유는, 실제로 Thebook 페이지 에서는 책 내용이 한 태그 안에 들어 있는 것이 아니라, 여러 태그들로 나뉘어져 있다. 그래서 따로따로 모두 가져오기 보다는 그냥 html 자체를 이용하는 편이 좋을 것 같아 이런 방식을 택했다.  

cheerio로 로드한 html이 담긴 변수인 $의 .html 메서드를 이용하면 DOM tree를 다시 html로 변환할 수 있다. 

axios는 기본적으로 프로미스를 반환하기 때문에 다음에 할 작업을 .then () 으로 이어갈 수 있다. bookContents 에 저장하는 작업이 끝나면, .then(()=>{resolve();}) resolve 함수를 호출하여 resolve 상태의 프로미스를 반환한다.

 

(async () => {
    for (let i = 0; i < $href.length; i++) {
        await contentLoad($href[i]);
    }
})();

즉시 실행 함수를 사용하여, 위의 예시처럼 await 키워드를 활용해서 contentLoad 함수를 실행시킨다. $href 배열 전체를 순회해야 되기 때문에 i를 $href의 index로 설정하여 contentLoad 함수에 $href[i]를 인자로 넣어준다.

 

여기까지의 전체 코드는 아래와 같이 된다.

./fetching.js

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

let $href = [];
let bookContents = "";
axios.get(`https://thebook.io/080212`)
    .then(dataa => {
        const $ = cheerio.load(dataa.data);
        $('section.book-toc>ul>li>a').each((index, item) => {
            $href.push(item.attribs.href)
        });

        function contentLoad(URL) {
            return new Promise((resolve, reject) => {
                axios.get(`https://thebook.io${URL}`)
                    .then(res => {
                        const $ = cheerio.load(res.data);
                        bookContents += `<div style="border: 1px solid black;padding: 0 10px;">${$.html($('section#page_content'))}</div><br>`;
                    })
                    .then(() => {
                        resolve();
                    });
            })
        }

        (async () => {
            for (let i = 0; i < $href.length; i++) {
                await contentLoad($href[i]);
            }
        })();

    });

이제 bookContents는 Thebook 에서 제공하는 책의 전체 주제와 소주제를 가지고 있는 html 문자열이 되었다.

 

fetching.js 로 이 코드를 작성한 이유는, 모듈로써 활용하기 위함이었다. 따라서 module.exports 를 사용하여, 크롤링을 하는 함수를 내보낼 수 있도록 만들어 주어야 한다. 함수로 만들어서 bookContents를 반환해야하는데, bookContents 또한 비동기로 작성되어서 아무값도 없는 bookContents 가 리턴될 가능성이 있다. 따라서 아래와 같이 코드를 작성한다.

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

module.exports = (booknum) =>{
    let $href = [];
    let bookContents = "";
    return axios.get(`https://thebook.io/${booknum}`)
        .then(dataa=>{
            const $ = cheerio.load(dataa.data);
            $('section.book-toc>ul>li>a').each((index, item)=>{$href.push(item.attribs.href)});

            function contentLoad(URL){
                return new Promise((resolve, reject)=>{
                    axios.get(`https://thebook.io${URL}`)
                        .then(res=>{
                            const $ = cheerio.load(res.data);
                            bookContents += `<div style="border: 1px solid black;padding: 0 10px;">${$.html($('section#page_content'))}</div><br>`;
                        })
                        .then(()=>{
                            resolve();
                        });
                })
            }

            return (async ()=>{
                for(let i = 0; i<$href.length; i++){
                    await contentLoad($href[i]);
                }
                return Promise.resolve(bookContents);
            })();

        })
};

일단 module.exports 로 함수를 넘겨준다. 그런데 이 함수의 리턴 값 자체를 axios 모듈로 설정해서, .then 으로 크롤링 후의 작업을 실행할 수 있도록 해주었다. axios.get() 다음 .then( ) 메서드에서 리턴값자체를 즉시실행함수로 설정해준다. 즉시실행함수에서 return 은 for문이 다 실행되고 리턴이 되는, 즉 동기식으로 작동하기 때문에, bookContents 가 모두 크롤링되어 저장된 후 리턴한다는 것은 보장된다. 결론적으로 Promise.resolve 에 데이터를 넣어서 리턴을 해주는데, 이 리턴값은 다시 리턴되고, 결국 함수를 실행하는 쪽(fetching.js을 모듈로 가져와서 사용하는 쪽)에서 .then 메서드를 붙여 쓸때 데이터로 가져올 수 있도록 한다.

 

반응형

댓글