Web::Scraper
웹페이지에서 특정 태그를 찾아 내용을 뽑아내고 하는 걸, /정규표현식을 쓰는게 아니라 태그의 이름과 속성 등을 명시해서 할 수 있게 해 준다.
Ruby 의 scrAPI[1] 라는 툴이 있었고 이걸 Perl에서도 비슷하게 만든 것 같은데, 문제는 Perl용 Web::Scraper 모듈의 perldoc 문서가 영 부실해서... 직접 삽질하고 코드 들여다보며 알아낸 것들 여기에 정리함.
use URI;
use Web::Scraper;
# 먼저 스크래이퍼 블록을 생성my$tweets = scraper {
# 클래스가 "status"인 LI들을 분석, 'tweets' 배열에 저장# 각 트윗에 대해 동작하는 또다른 스크래이퍼가 중첩되어 있음
process "li.status", "tweets[]" => scraper {
# And, in that array, pull in the elementy with the class# "entry-content", "entry-date" and the link
process ".entry-content", body => 'TEXT';
process ".entry-date", when => 'TEXT';
process 'a[rel="bookmark"]', link => '@href';
};
};
my$res = $tweets->scrape( URI->new("http://twitter.com/miyagawa") );
# 결과물은 수집된 트윗들의 배열formy$tweet (@{$res->{tweets}}) {
print"$tweet->{body} $tweet->{when} (link: $tweet->{link})\n";
}
결과물의 자료구조는 (시각적으로) 다음과 같이 생겼다:
{ tweets => [ { body => $body, when => $date, link => $uri }, { body => $body, when => $date, link => $uri }, ] }
URI, HTTP::Response, HTML::Tree, 텍스트 문자열로부터 HTML을 읽어들이고 DOM 오브젝트를 생성한 후, 콜백 코드를 실행하여 자료 구조를 구성한다.
URI 또는 HTTP::Response 오브젝트를 인자로 전달할 경우, Web::Scraper는 Content-Type 헤더와 META 태그를 조사하여 문서의 인코딩을 추측한다. 그 외의 경우는 HTML을 scrape메쏘드에 전달하기 전에 사용자가 유니코드로 디코드하여야 한다.
HTML을 스트링으로 전달할 경우, 추가적으로 base URL을 인자로 전달해 줌으로써 Web::Scraper가 문서 내의 상대 경로로 된 링크의 올바른 주소를 얻어낼 수 있다.
scraper {
process "tag.class", key => 'TEXT';
process '//tag[contains(@foo, "bar")]', key2 => '@attr';
};
process는 CSS 셀렉터 또는 XPath 표현식을 사용하여 HTML로부터 일치하는 요소를 찾아내고, 그 요소의 텍스트나 속성값을 추출하여 저장한다.
# <span class="date">2008/12/21</span>
# date => "2008/12/21"
process ".date", date => 'TEXT';
# <div class="body"><a href="http://example.com/">foo</a></div>
# link => URI->new("http://example.com/")
process ".body > a", link => '@href';
# <div class="body"><a href="http://example.com/">foo</a></div>
# link => URI->new("http://example.com/"), text => "foo"
process ".body > a", link => '@href', text => 'TEXT';
# <ul><li>foo</li><li>bar</li></ul>
# list => [ "foo", "bar" ]
process "li", "list[]" => "TEXT";
# <ul><li id="1">foo</li><li id="2">bar</li></ul>
# list => [ { id => "1", text => "foo" }, { id => "2", text => "bar" } ];
process "li", "list[]" => { id => '@id', text => "TEXT" };
여기에 scrAPI에서는 (독자적인 건지 다른데서 만들어진 걸 가져왔는지 모르겠지만) 편의를 위해 CSS2의 Selector 패턴에 추가로 pseudo class가 몇 개 더 있던데, 이 중에 일부는 Web::Scraper 에서도 되고 또 일부는 안 됨.
다음의 내용은 [2]에는 없고 scrAPI cheat sheet[5]에는 있는 것들 중에 내가 확인한 것들. 보아하니 이건 Web::Scraper 모듈이 아니라 HTML::Selector::XPath 모듈에서 지원하는지 여부에 따른 듯
Web::Scraper는 정규표현식을 쓸 때에 비해 11배, HTML::TokeParser를 쓸 때에 비해 10배 느리다...; 오히려 정규표현식에 뒤지지 않는 TokeParser의 성능이 눈에 띈다.
물론 작업이 조금만 복잡해져도 ("**태그 안에 있는 xx태그 안에 있는 텍스트" 뭐 이런 것 들) 정규표현식으로 특정한 태그들을 찾아내고 내용을 추출하는 것은 무리일 거고... 차후에 웹사이트의 포맷이 바뀌거나 할 때 (그나마) 손쉽게 수정하려면 Scraper가 최선이긴 한데,처리할 html이 많을 때는 속도에서 많은 손해를 보니까 유의해야 할 듯.
매번 html 전체 구조를 파싱하고 트리구조를 생성한 후 거기에서 원하는 노드를 찾아가는 형태이다보니, 정규표현식으로 한번에 원하는 곳을 집어내는 것에 비해서 매우 느리다. 웹페이지 전체를 가지고 처리하지 말고 일단 불필요한 부분을 제거한 후에 남은 내용을 가지고 처리하게 하는 것이 좋다.