웹에서 뭔가 필요한 자료를 찾을 때가 종종 있다. 한 번만 찾고 마는 경우라면 간단한데 정기 간행되는 기사나 웹툰(Webcomics) 같은 연재되는 컨텐트를 보려고 하면 일일이 눌러서 봐야 하기 때문에 흐름이 끊기는 게 싫기도 하고 광고가 페이지마다 뜨는 것도 마음에 안든다. 그래서 웹 스크레이핑(Web scraping)으로 필요한 컨텐트만 콕 집어서 한번에 가져온 다음 여유있게 보는 것이 편리할 것이다.

그래서 작성해봤다. 웹페이지의 컨텐트를 이미지로 저장하거나 텍스트 파일로 저장할 수 있는 자바스크립트 프로그램!

오늘의 프로그램 요리를 위한 첫번째 재료는 PhantomJS 되겠다.애플 사파리(Safari) 및 구글 크롬(Chrome) 웹브라우저의 렌더링 엔진인 WebKit을 활용한 “Headless” 브라우저로서 자바스크립트(JavaScript)을 사용해 UI 없이 명령행으로 웹페이지를 조작할 수 있다. 모질라 파이어폭스(Firefox)의 엔진인 Gecko 기반의 SlimerJS도 마찬가지 방식이다. 이 글에서는 PhantomJS를 사용해보기로 한다.

먼저 PhantomJS에서 기본적으로 제공되는 예제 중에 웹 페이지를 캡처해 저장하는 스크립트가 있다.

var page = require('webpage').create();
page.open('http://github.com/', function() {
page.render('github.png');
phantom.exit();
});

위 코드를 보면 PhantomJS의 외부 모듈 중 하나인 webpage 모듈을 로딩한 다음 page 개체를 만든다. 그리고는 그 페이지 개체를 사용하여 http://github.com 싸이트의 화면을 캡처한 후 종료한다.

이것이 일반적인 PhantomJS의 사용 방식이다. 웹 스크레이핑을 위해서는 연속적으로 웹 페이지를 찾아들어갈 수 있도록 반복되는 루틴을 추가하면 될 것이다.

오늘의 프로그램 요리 두 번째 재료로 웹 스크레이프를 할 대상 웹사이트를 준비한다. 여기서는 영어 그래픽 소설인 Delilah Dirk and the Turkish Lieutenant를 스크레이프해보겠다. 우선 이 만화에서 어떤 주소로부터 시작할지 정해야 한다. 첫 페이지인 http://www.delilahdirk.com/ddattl/ch0-000A-000B.html부터 스크레이프하기로 하고 그 페이지를 연 후 소스를 보자.

캡처할 부분을 HTML 소스에서 찾기
캡처할 부분을 HTML 소스에서 찾기

위는 구글 크롬에서 개발자 도구를 사용해 소스를 보면서 원하는 컨텐트가 어떤 부분인지 찾아내는 것이다. 파란색으로 강조 표시된 줄이 뒤의 웹페이지에서 흐릿하게 나타나는 것을 볼 수 있다. 다른 브라우저나 도구를 사용해도 상관 없다. 어떤 부분이 원하는 영역인지 찾은 후 그 영역을 구별할 수 있는 CSS 선택자를 만들어내야 한다. 여기서는 이미지를 감싸고 있는 div의 id가 있으므로 #leftpage img와 같이 선택할 수 있을 것이다.

이 페이지에는 만화가 두 쪽씩 들어있으므로 #rightpage img도 선택해야 한다.

다음으로는 이 웹페이지를 본 후 어디로 가는지를 찾아내야 한다. 위 그림에서 보면 아주 간편하게 #snextbutton a 태그 안의 href 값을 가져오기만 하면 된다.

그런데 우리나라의 웹페이지를 보면 위와 같이 간단히 HTML 개체에서 값을 가져오지 못하는 경우가 있다. 자바스크립트를 사용해서 동적으로 처리하거나 어딘가 숨겨놓은 경우다. 이런 경우는 CSS 선택자만으로는 안되고 정규식 같은 것을 사용해서 문자열을 추출하는 방식으로 처리해야 할 것이다.

일단은 위와 같이 간단한 조건으로 웹 컨텐트를 캡처할 수 있는 프로그램을 아래와 같이 만들어봤다.

/*
* phantomjs를 활용한 웹 스크레이핑 0.1, 2013-11-30
* http://start.goodtime.co.kr
* (c) 2013 이동련
* MIT 라이선스
*/


/********** 설정 영역. 아래 설정 값들을 적절히 수정해야 함 ***********/

// 시작 웹페이지의 URL. 이 주소부터 시작해서 컨텐트를 스크레이핑한다.
var url = 'http://www.delilahdirk.com/ddattl/ch0-000A-000B.html';

// 웹페이지에서 어떤 컨텐트를 스크레이핑할지 지정하는 CSS 선택자
var contentSelectors = ['#leftpage img', '#rightpage img'];

// 다음 웹페이지 링크를 찾을 수 있는 CSS 선택자
var nextLinkSelector = '#snextbutton a';

// 최대로 가져올 웹페이지 수
var maxPages = 5;

// 가져온 컨텐트 캡처 이미지를 저장할 폴더
var saveTo = './captures';

/********** 이 이하는 스크레이핑 실행 코드 ***************/
var index = 0;
var webpage = require('webpage');
var page = webpage.create();

page.open(url, scrape);

function scrape(status) {
if (status != 'success') {
console.log('웹페이지 열기 오류: ' + url);
phantom.exit();
}

console.log(++index + ': ' + url);

for (var i = 0; i < contentSelectors.length;) {
var clipRect = page.evaluate(function (selector) {
var o = document.querySelector(selector);
return o ? o.getBoundingClientRect() : null;
}, contentSelectors[i]);

++i;
if (clipRect) {
page.clipRect = clipRect;
page.render(saveTo + '/' + index + '-' + i + '.png');
}
}

url = page.evaluate(function(selector) {
var o = document.querySelector(selector);
return o ? o.href : null;
}, nextLinkSelector);

if (index >= maxPages || !url) {
phantom.exit();
}

page.open(url, scrape);
}

위의 스크립트는 원하는 컨텐트를 이미지로 저장하는 것인데 텍스트를 추출해 텍스트 파일로 저장할 경우는 파일 API를 사용한 처리도 필요하고 조금 더 복잡해진다.

이제 위 스크립트를 scrape.js라는 파일명으로 저장했다면 다음과 같이 실행함으로써 captures 폴더에 png 이미지들이 저장된다.

phantomjs.exe scrape.js

PhantomJS는 이러한 사용 외에도 웹 기능 테스트라든가 HTTP 네트웍 모니터링 등 활용도가 다양하다. 간단하게나마 위와 같이 웹 스크레이핑에 있어서도 아주 훌륭하다. 다양한 활용 방법이 앞으로도 계속 나오지 않을까 생각된다.

웹스크래핑