Node.js 환경에서 Scraping 을 진행하려면 phantom 모듈이 필요 합니다.
https://www.npmjs.com/package/phantom
이와 함께 서버 단 에서 제이쿼리 문법을 사용하여 DOM 을 제어 해야 함으로
Jquerygo 모듈도 같이 설치 합니다.
https://www.npmjs.com/package/jquerygo
2가지 모듈은 모두 해당 프로젝트에 local install 되어야 하며, 설치가 완료되었다면
Scraping 을 위한 준비 가 끝났습니다.
Scraping에 사용한 프로젝트의 구조는 Express 와 angular 기반 구조로 작성 되었습니다.
web Scraping 할 대상 페이지 입니다.
해당 페이지에서 초록색 박스표시 영역에 있는 변액상품의 정보를 Scraping 하는 기술을 구현 해보겠습니다.
크롬 브라우저의 개발자 도구에서 Scraping Target 을 선택했을 때 보여지는 HTML 의 구조 입니다. 해당페이지는 사용자 지정 없이 1페이지당 20개의 리스트를 보여주고 있기 때문에 전체 페이지 계산을 위해서 caption_wrap 클래스를 기준으로 하위에 있는 count 클래스에 전체 상품 수량을 가져와야 합니다.
그리고 테이블 클래스명인 dataTable 을 기준으로 tbody > tr > th 태그에 순차적으로 접근하여 데이터를 Scraping 합니다.
전체적인 실행 흐름은 서버에 요청이 왔을 때 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();
});
});
});
});
}
});
};
실행 결과 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