본문 바로가기

BackEnd/Phantom.js

Phantom 모듈을 활용한 web scraping

1.   준비 사항

Node.js 환경에서 Scraping 을 진행하려면 phantom 모듈이 필요 합니다.

https://www.npmjs.com/package/phantom

이와 함께 서버 단 에서 제이쿼리 문법을 사용하여 DOM 을 제어 해야 함으로

Jquerygo 모듈도 같이 설치 합니다.

https://www.npmjs.com/package/jquerygo

2가지 모듈은 모두 해당 프로젝트에 local install 되어야 하며, 설치가 완료되었다면

Scraping 을 위한 준비 가 끝났습니다.


2.   Phantom 모듈을 활용한 Scraping

Scraping에 사용한 프로젝트의 구조는 Express angular 기반 구조로 작성 되었습니다.

web Scraping 할 대상 페이지 입니다.

http://www.funddoctor.co.kr/avn/vfund/vfund_list.jsp?page=1&fund_nm=&insur_cd=000&prod_cd=Z&insva_cd=Z&panm_cd=1&selFundType6=1&list_orderBy=A

해당 페이지에서 초록색 박스표시 영역에 있는 변액상품의 정보를 Scraping 하는 기술을 구현 해보겠습니다.

A.   Scraping Target DOM 구조 분석

크롬 브라우저의 개발자 도구에서 Scraping Target 을 선택했을 때 보여지는 HTML 의 구조 입니다. 해당페이지는 사용자 지정 없이 1페이지당 20개의 리스트를 보여주고 있기 때문에 전체 페이지 계산을 위해서 caption_wrap 클래스를 기준으로 하위에 있는 count 클래스에 전체 상품 수량을 가져와야 합니다.

그리고 테이블 클래스명인 dataTable 을 기준으로 tbody > tr > th 태그에 순차적으로 접근하여 데이터를 Scraping 합니다 

B.    Scraping Logic 구현

전체적인 실행 흐름은 서버에 요청이 왔을 때 page_info_extract 함수를 우선적으로 실행 합니다. Page_info_extract 함수가 하는 역할은 첫 번째 phantom 을 생성하여 갱신일자와 총 상품의 개수를 Node 서버 단으로 Scraping 하게 됩니다.

해당 페이지는 1페이지당 20개씩 보여지는 것이 고정되어 있기 때문에 전체 상품의 개수 나누기 20을 해서 소수점 올림을 하게 되면 두 번째 phantom 이 실행될 때 순회할 페이지의 개수를 동적으로 구할 수 있게 됩니다. urlArr 배열에 순회할 페이지들을 저장하고 첫 번째 phantom 은 종료 되며 next_page 함수를 실행하게 됩니다.  

next_page 함수는 urlArr 배열에 저장된 pageUrl 을 모두 순회 할 때까지 handle_page함수를 반복적으로 실행되며 file 함수에 값이 없을 경우 두 번째 phantom 은 종료됩니다.

Handle_page함수는 두 번째 phantom 을 반복적으로 실행시키는 함수로 한번에 한 페이지씩 변액보험 정보를 pages 배열에 저장하는 역할을 합니다.

두 번째 phantom 이 종료되면서 Scraping 한 데이터들이 html 상에 리스트화 되고, Node 에서 제공하는 가장 기본적인 fs 모듈을 이용해 txt 형태의 파일로 해당 데이터들을 저장하게 됩니다. (txt 파일을 읽어 올지 database 에 저장 할지 여부는 유연하게 선택하여 구현가능 합니다.)

서버 단에 구현된 Logic 은 다음과 같습니다.

exports.phantomAction =function(req,res){

   
var pages =
[];
   
var pages_info
;
   
var urlArr =
[];

   
phantom.create(function (ph
) {

       
function handle_page(file
) {

           
ph.createPage(function (page
) {
               
page.open(file, function
() {
                   
console.log(file
);
                   
page.evaluate(function
() {

                       
var vul_productName =
[];
                       
var vul_company =
[];
                       
var vul_rank =
[];
                       
var vul_type =
[];
                       
var vul_division =
[];
                       
var vul_obj =
{
                           
productList: vul_productName
,
                            
companyList: vul_company
,
                           
rankList: vul_rank
,
                           
typeList: vul_type
,
                           
divisionList:
vul_division
                       
};
                       
$('.dataTable tbody tr').each(function
() {

                           
vul_productName.push($(this).find('th:nth-child(1)').text().trim
());
                           
vul_company.push($(this).find('th:nth-child(2)').text
());
                           
vul_rank.push($(this).find('th:nth-child(3)').text
());
                           
vul_type.push($(this).find('th:nth-child(4)').text
());
                           
vul_division.push($(this).find('th:nth-child(5)').text
());
                        });
                       
return vul_obj
;

                    },
function (result
) {
                       
setTimeout(next_page(), 5
);
                       
pages.push(result
);
                    });
                });
            });
        }

       
function next_page
(){
           
var file = urlArr.shift
();
           
if(!file
){
               
ph.exit
();
               
//
파일 만들기
               
var data  = pages_info+"\n" + JSON.stringify(pages);
               
fs.writeFile('./async_test.txt',data, 'utf8', function (error
) {
                   
console.log('WRITE FILE COMPLETE'
);
                });

               
res.send(200,{pages:pages,pages_info:pages_info
});
            }
           
handle_page(file
);
        }

       
page_info_extract
();

       
function page_info_extract
() {
           
phantom.create(function (ph
) {
               
ph.createPage(function (page
) {
                   
page.open('http://www.funddoctor.co.kr/avn/vfund/vfund_list.jsp', function
() {
                       
page.evaluate(function
() {
                           
var page_info =
[];
                           
page_info.push($('.caption_wrap > .caption ').text
());
                           
page_info.push($('.caption_wrap > .count ').text().replace(/[^0-9]/g, ''
));
                           
return page_info
;
                        },
function (result
) {
                           
pages_info = result
;
                           
//0
번째 배열에는 갱신일자.
                           
console.log(pages_info[0]);
                           
//1
번째 배열에는 상품의 총 개수.
                            //
나누기 20 = 리스트 개수 고정

                           
var page_langth = Math.ceil(pages_info[1] / 20);
                           
for (var i = 1; i <= page_langth + 1; i++
) {
                               
urlArr.push("http://www.funddoctor.co.kr/avn/vfund/vfund_list.jsp?page=" + i + "&fund_nm=&insur_cd=000&prod_cd=Z&insva_cd=Z&panm_cd=1&selFundType6=1&list_orderBy=A"
);
                            }
                            
ph.exit
();
                           
next_page
();
                        });
                    });
                });
            });
        }

    });
};

 

C.    실행 결과

 

실행 결과 508개의 리스트를 Scraping 하였습니다.

폴더의 최상위 경로에 txt 파일도 만들어진 모습을 확인 할 수 있습니다.

     

3.   그밖에 사항

Node 에서 Web Scraping 을 구현하는 다른 기술로는 cheerio 가 있습니다.

https://www.npmjs.com/package/cheerio

Phantom 의 상위 개념인 Casper 도 있지만 별도의 독립된 서버를 운영 해야 하기 때문에 서버 실행 중 소켓 통신을 해야 하는 불편함이 존재 합니다.

Web Scraping 에 대한 다른 내용은 해당 blog를 참고 해주시기 바랍니다.

http://codenamu.org/2014/11/13/16385/

http://nodeqa.com/nodejs_ref/86

http://blog.saltfactory.net/node/web-scraping-using-with-node-and-phantomjs.html