HTML::TreeBuilder + CSSセレクタがいい感じな件

先日 PerlでCSSセレクタHTML::Selector::XPath がいい感じであると思ったわけですが、CSS セレクタだけじゃなく何気に HTML::TreeBuilder::XPath とのコンボがすげーイイ!ということにいまさら気づきました。

HTML::TreeBuilder::XPath で findnodes するとツリー状に連なった HTML::Element なデータ構造が返ってくるんですが、HTML::Element は API をかなりいろいろ持ってて、これをうまく使ってやるとスクレイピングを自然な感じで書けます。

例えばはてなダイアリーの任意のページから、本文部分だけをスクレイピングしたいと思ったときにキーワードリンクが邪魔だったりするわけですが、とりあえず HTML::Selector::XPath で div.section をぶっこ抜いて取れた HTML::Element に as_text を呼べばキーワードリンクとかを無視してテキストにできます。

つまり

# $tree はダイアリーの HTML をパーズした HTML::TreeBuilder::XPath
$tree->findnodes(selector_to_xpath('div.section'))->shift->as_text;

で OK と。これを素で書こうとすると div.section を正規表現でぶっこぬいたあとタグっぽいものを正規表現で削除して...というコードになって汚くなりがち。Selector::XPath + TreeBuilder::XPath なら正規表現は一行も書かずに同じことができます。

単にテキストにするだけだとあんまりありがたみがないですが、例えば HTML としてぶっこぬきつつ

なんて時は?

HTML::Element の look_down で、div.section 要素にぶら下がってる子要素でいらないものを探しつつ、delete や replace_with_content を呼んでやります。こんな感じで css セレクタで抽出したエレメントをいじって変形させることができます。

$_->delete for $section->look_down(_tag => 'h3');
$_->delete for $section->look_down(_tag  => 'p', class => 'sectionfooter');
$_->replace_with_content for $section->look_down(_tag  => 'a', class => 'keyword');

という具合。

スクレイピングの入り口が CSS セレクタで簡単になるだけじゃなく、スクレイピングして取ってきた要素を更に HTML::TreeBuilder が構築するデータ構造でいろいろといじれるので正規表現レスでだいたいのことができるという。こりゃ英和。

#!/usr/local/bin/perl
use strict;
use warnings;

use URI::Fetch;
use HTML::Selector::XPath qw/selector_to_xpath/;
use HTML::TreeBuilder::XPath;
use HTML::Entities qw/decode_entities/;

my $uri = shift or die "$0 <uri>";
my $res = URI::Fetch->fetch($uri) or die URI::Fetch->errstr;
my $section = $res->tree->select_by_css('div.section')->shift;

my $html = format_for($section)->as_XML;
decode_entities($html);
print $html, "\n";

sub format_for {
    my $section = shift;
    $_->delete for $section->look_down(_tag => 'h3');
    $_->delete for $section->look_down(_tag  => 'p', class => 'sectionfooter');
    $_->replace_with_content for $section->look_down(_tag  => 'a', class => 'keyword');
    $section;
}

sub URI::Fetch::Response::tree {
    my $res = shift;
    my $tree = HTML::TreeBuilder::XPath->new;
    $tree->parse($res->content);
    $tree->eof;
    $tree;
}

sub HTML::TreeBuilder::XPath::select_by_css {
    my ($tree, $css) = @_;
    $tree->findnodes(selector_to_xpath($css));
}

HTML::TreeBuilder が星5個なのがよくわかりました。気づくのおせーよ、という感じですが。

HTML::TreeBuilder を積極的に活用する場面っていうのはこれまであんまりなかったんですが、HTML::Selector::XPathCSS セレクタでツリーをゲットできるようになって入り口の障壁がとっても下がった感じで、今後はむちゃくちゃ使いそうな予感でございます。