XML::LibXMLでHTML文書を扱う

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の方が少し速いようです。