[첫화면으로]Perl/Web-Scraper

마지막으로 [b]

Web::Scraper

Cpan:Web::Scraper

웹페이지에서 특정 태그를 찾아 내용을 뽑아내고 하는 걸, /정규표현식을 쓰는게 아니라 태그의 이름과 속성 등을 명시해서 할 수 있게 해 준다.

Ruby 의 scrAPI[1] 라는 툴이 있었고 이걸 Perl에서도 비슷하게 만든 것 같은데, 문제는 Perl용 Web::Scraper 모듈의 perldoc 문서가 영 부실해서... 직접 삽질하고 코드 들여다보며 알아낸 것들 여기에 정리함.

1. perldoc Web::Scraper 정리
1.1. 이름
1.2. 개요
1.3. 설명
1.4. 메쏘드
1.4.1. scraper
1.4.2. scrape
1.4.3. process
1.5. 예문
1.6. 참조
2. 주인장 추가
2.1. 원하는 엘리멘트를 골라내는 법
2.2. 추출할 내용을 명시할 때
2.3. 수행속도 비교
3. 기타 & Comments

1. perldoc Web::Scraper 정리

Cpan:Web::Scraper 내용 요약 정리

1.1. 이름

Web::Scraper - HTML과 CSS 셀렉터 또는 XPath 표현식을 사용한 웹 정보추출1 도구 모음

1.2. 개요

  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") );

  # 결과물은 수집된 트윗들의 배열
  for my $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 }, ] }

1.3. 설명

Web::Scraper는 웹 추출 도구이며, Ruby의 Scrapi에서 영감을 얻어 만들어졌다.

1.4. 메쏘드

1.4.1. scraper

  $scraper = scraper { ... };

Web::Scraper 오브젝트를 생성한다. 이 오브젝트에는 scrape 메쏘드가 호출되었을 때 실행될 DSL 코드 블록을 담긴다.

1.4.2. scrape

  $res = $scraper->scrape(URI->new($uri));
  $res = $scraper->scrape($html_content);
  $res = $scraper->scrape(\$html_content);
  $res = $scraper->scrape($http_response);
  $res = $scraper->scrape($html_element);

URI, HTTP::Response, HTML::Tree, 텍스트 문자열로부터 HTML을 읽어들이고 DOM 오브젝트를 생성한 후, 콜백 코드를 실행하여 자료 구조를 구성한다.

URI 또는 HTTP::Response 오브젝트를 인자로 전달할 경우, Web::Scraper는 Content-Type 헤더와 META 태그를 조사하여 문서의 인코딩을 추측한다. 그 외의 경우는 HTML을 scrape메쏘드에 전달하기 전에 사용자가 유니코드로 디코드하여야 한다.

HTML을 스트링으로 전달할 경우, 추가적으로 base URL을 인자로 전달해 줌으로써 Web::Scraper가 문서 내의 상대 경로로 된 링크의 올바른 주소를 얻어낼 수 있다.

  $res = $scraper->scrape($html_content, "http://example.com/foo");

1.4.3. process

  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" };

1.5. 예문

모듈 배포본의 eg/ 디렉토리 안에 다수의 예문들이 있다.3

1.6. 참조

2. 주인장 추가

2.1. 원하는 엘리멘트를 골라내는 법

기본적으로 다음 두 가지 형태로 지정 가능

여기에 scrAPI에서는 (독자적인 건지 다른데서 만들어진 걸 가져왔는지 모르겠지만) 편의를 위해 CSS2의 Selector 패턴에 추가로 pseudo class가 몇 개 더 있던데, 이 중에 일부는 Web::Scraper 에서도 되고 또 일부는 안 됨.

다음의 내용은 [2]에는 없고 scrAPI cheat sheet[5]에는 있는 것들 중에 내가 확인한 것들. 보아하니 이건 Web::Scraper 모듈이 아니라 Cpan:HTML::Selector::XPath 모듈에서 지원하는지 여부에 따른 듯

2.2. 추출할 내용을 명시할 때

scrAPI[5] Web::Scraper 설명
:text "text" 또는 "content" 텍스트 부분만 추출, 태그들은 제거됨
:element (?5) "raw" 또는 "html" 태그까지 포함된 전체 내용을 추출

2.3. 수행속도 비교

Clien RSS를 만드는데 이 모듈을 사용하고 있는데, RSS를 새로 갱신할 때마다 실행 속도가 눈에 확 띌 정도로 느려서... 간단히 테스트를 해 보았다.

테스트 코드: 길어서 감춤 - 클릭

결과: (다섯 번 반복해 측정해봤는데 거의 비슷하게 나와서 하나만 옮김)

Benchmark: timing 200 iterations of RegExp, Scraper, TokeParser...
    RegExp:  3 wallclock secs ( 2.72 usr +  0.00 sys =  2.72 CPU) @ 73.53/s (n=200)
   Scraper: 34 wallclock secs (33.73 usr +  0.01 sys = 33.74 CPU) @  5.93/s (n=200)
TokeParser:  3 wallclock secs ( 3.00 usr +  0.00 sys =  3.00 CPU) @ 66.67/s (n=200)
             Rate    Scraper TokeParser     RegExp
Scraper    5.93/s         --       -91%       -92%
TokeParser 66.7/s      1025%         --        -9%
RegExp     73.5/s      1140%        10%         --

Web::Scraper는 정규표현식을 쓸 때에 비해 11배, HTML::TokeParser를 쓸 때에 비해 10배 느리다...; 오히려 정규표현식에 뒤지지 않는 TokeParser의 성능이 눈에 띈다.

물론 작업이 조금만 복잡해져도 ("**태그 안에 있는 xx태그 안에 있는 텍스트" 뭐 이런 것 들) 정규표현식으로 특정한 태그들을 찾아내고 내용을 추출하는 것은 무리일 거고... 차후에 웹사이트의 포맷이 바뀌거나 할 때 (그나마) 손쉽게 수정하려면 Scraper가 최선이긴 한데,처리할 html이 많을 때는 속도에서 많은 손해를 보니까 유의해야 할 듯.

3. 기타 & Comments

매번 html 전체 구조를 파싱하고 트리구조를 생성한 후 거기에서 원하는 노드를 찾아가는 형태이다보니, 정규표현식으로 한번에 원하는 곳을 집어내는 것에 비해서 매우 느리다. 웹페이지 전체를 가지고 처리하지 말고 일단 불필요한 부분을 제거한 후에 남은 내용을 가지고 처리하게 하는 것이 좋다.

-- Raymundo 2010-2-24 6:04 pm

html 자체에 오류가 있는 경우 (태그에 속성 이름이 잘못되었다거나, 따옴표나 괄호가 제대로 매치가 안 된다거나...) scrape()과정에서 die를 해 버리니 좀 불편하다. 추출은 못 하더라도 굳이 die까지는 안 해도 될 것 같은데.

-- Raymundo 2012-2-22 9:28 pm
이름:  
Homepage:
내용:
 


컴퓨터분류 Perl
각주:
1. "scraper"의 적당한 번역이 뭔지 몰라서...
2. 아마도 Domain specific language일 듯
3. 근데 이 예문들도 설명이 자세히 되어 있지는 않더라.
4. Dead link
5. 이게 이 용도가 맞는지 모르겠음

마지막 편집일: 2014-4-18 11:06 pm (변경사항 [d])
3493 hits | Permalink | 변경내역 보기 [h] | 페이지 소스 보기