XML::LibXML大好きな者が以下述べてみます。
しかし、XML::Liberalを除けば、XML::*なモジュールはX抜きのHTMLを食ってくれない....
と、404 Blog Not Found:perl - HTMLをXMLとして扱うで書かれていたのですが、XML::LibXML、というかその基となるCライブラリlibxml2はHTMLパーサも備えているので、直にHTMLを扱うこともできます。
ただ元がXMLパーサなだけに、少しでもHTML文書に壊れた部分があると解析エラーを起こして停止してしまいますが、幸いなことにそのエラーから回復するモードも備えています。以下サンプルを。
use strict;
use warnings;
use XML::LibXML;
my $parser = XML::LibXML->new();
$parser->recover_silently(1);
my $doc = $parser->parse_html_file('http://blog.livedoor.jp/dankogai/');
print $doc->toString;
ここで使った $parser->parse_html_file()
、およびXML文書用の $parser->parse_file()
の両メソッドですが、上記の通り(名前の印象に反して)ファイル名だけでなくURLを渡すことも可能です。なのでLWPモジュールを使わなくても、XML::LibXMLだけでネットワーク上のXML/HTMLファイルを取得して解析することができてしまいます。perldoc XML::LibXML::Parserによれば、"for parsing files, this function is the fastest choice
" でもあるそうです。
また、XML::LibXML のいいところはDOMのAPIを使えることにありますが、その上にXPathも使えるのがさらに便利なところです。
# 目的のノード集合を配列に取得
@nodes = $node->findnodes( $xpath );
# XPathの指すオブジェクトの値を取得
$value = $node->findvalue( $xpath );
試しに、404 Blog Not Found:perl - HTMLをXMLとして扱うにあった、タイトル取得処理のベンチマークをDOMとXPathの両方で取ってみました。
use strict;
use warnings;
use Benchmark qw(cmpthese timethese);
use HTML::Entities;
use LWP::UserAgent;
use XML::LibXML;
use HTML::DOM;
my $uri = shift || die;
my $res = LWP::UserAgent->new->get($uri);
die $res->status_line unless $res->is_success;
my $content = $res->decoded_content;
my $raw_content = $res->content;
#use Data::Dumper;print Dumper($content);die;
#use Data::Dumper;print Dumper($raw_content);die;
my $xml = XML::LibXML->new();
$xml->recover_silently(1);
my $dom = HTML::DOM->new();
sub title_regexp{
my $str = shift;
$str =~ m{<title>((?>[^<]+))}msi;
decode_entities($1);
}
sub title_xmldom {
my $doc = $xml->parse_html_string(shift);
$doc->getElementsByTagName('title')->shift->firstChild->toString;
}
sub title_xpath {
my $doc = $xml->parse_html_string(shift);
$doc->findvalue('/html/head/title');
}
sub title_html {
$dom->open();
$dom->write(shift);
$dom->close();
$dom->getElementsByTagName('title')->[0]->innerHTML;
}
my $title = title_regexp($content);
warn qq("$title");
cmpthese timethese(0, {
'HTML::DOM' => sub { $title eq title_html($content) or die },
libxml_dom => sub { $title eq title_xmldom($raw_content) or die },
libxml_xpath => sub { $title eq title_xpath($raw_content) or die },
regexp => sub { $title eq title_regexp($content) or die },
});
libxml2では文字コードの変換も行うので、$parser->parse_string()
にはPerl内部文字列ではなく生のままの文字列を渡しています。ちなみに findvalue()
の結果はPerl内部文字列になります。
$ perl a.pl http://blog.livedoor.jp/dankogai/archives/51179647.html "404 Blog Not Found:perl - HTMLをXMLとして扱う" at b.pl line 46. Benchmark: running HTML::DOM, libxml_dom, libxml_xpath, regexp for at least 3 CPU seconds... HTML::DOM: 4 wallclock secs ( 3.94 usr + 0.00 sys = 3.94 CPU) @ 1.02/s (n=4) libxml_dom: 3 wallclock secs ( 3.16 usr + 0.00 sys = 3.16 CPU) @ 21.86/s (n=69) libxml_xpath: 4 wallclock secs ( 3.11 usr + 0.00 sys = 3.11 CPU) @ 28.30/s (n=88) regexp: 4 wallclock secs ( 3.14 usr + 0.00 sys = 3.14 CPU) @ 2137.58/s (n=6712) Rate HTML::DOM libxml_dom libxml_xpath regexp HTML::DOM 1.02/s -- -95% -96% -100% libxml_dom 21.9/s 2052% -- -23% -99% libxml_xpath 28.3/s 2687% 30% -- -99% regexp 2138/s 210345% 9680% 7452% --
DOMよりはXPathの方が少し速いようです。