ある書籍の監訳作業をやっていて Perl と XML に関するところを読んでたら、ある疑問が沸々と。
RSS や OPML、changes.xml といった XML ドキュメントからある特定の要素だけを抜き出す処理はよくやります。例えば RSS に記載された permalink の URL を抽出して、それらを巡回などなど。
この特定の要素のみ抜き出すという処理の方法はいわゆる TMTOWTDI というやつで、いろいろやり方があります。正規表現、XML::Parser、XML::LibXML + XPath などなど。で、速度的にはどんなもんなんだろう? と気になったのでベンチマークを取ってみます。
処理内容は、あらかじめ保存しておいた RSS ファイル (FeedBack で '加藤ローサ' の RSS) から、各エントリの permalink の URL を抜き出すというもの。この処理を、
1. 正規表現でぶっこ抜き
2. XML::Simple でパースして得られるデータ構造から取得
3. XML::RSS でパースして取得
4. XML::LibXML の DOM に対する XPath インタフェースで取得
の 4 とおりで行って、Benchmark モジュールで比較します。
予想では正規表現が一番速く、次に XML::LibXML、そして XML::Parser を内部で使用している 2. と 3. は同じくらいといったところ。
で、以下のスクリプトを書いて実行してみました。
#!/usr/local/bin/perl use strict; use warnings; use Benchmark; use FileHandle; use XML::LibXML; use XML::RSS; use XML::Simple; my $rss_file = shift or die "usage $0\n"; my $fh = FileHandle->new($rss_file) or die "cannot open $rss_file: $!"; local $/; # slurp mode our $content = $fh->getline; $fh->close; Benchmark::timethese(1000, { 'regexp' => \&with_regexp, 'XML::Simple' => \&with_xml_simple, 'XML::RSS' => \&with_xml_rss, 'XML::LibXML' => \&with_xml_libxml, }); sub with_regexp { my $pattern = "<item .*?>.*?<link>(.*?)</link>.*?</item>"; my @links = ($content =~ m/$pattern/smg); } sub with_xml_simple { my @links = (); my $parser = XML::Simple->new; my $data = $parser->XMLin($content, ForceArray => 1); for my $item (@{$data->{item}}) { push @links, $item->{link}->[0]; } } sub with_xml_rss { my @links = (); my $rss = XML::RSS->new; $rss->parse($content); for my $item (@{$rss->{items}}) { push @links, $item->{link}; } } sub with_xml_libxml { my @links =(); my $parser = XML::LibXML->new; my $doc = $parser->parse_string($content); my @nodes = $doc->findnodes( "//*[local-name()='item']/*[local-name()='link']/text()" ); for my $node (@nodes) { # print $node->nodeValue, "\n"; push @links, $node->nodeValue; } }
1 〜 4 の処理をそれぞれサブルーチンに分けて、Benchmark モジュールの timethese 関数に渡しています。サブルーチンの実行回数はそれぞれ 1000 回で。すべてのサブルーチンの処理結果は同じです。
あんま関係ないですが、省略された名前空間を持つエレメントを XPath で指定するのって結構めんどくさいですね。はまりました。@IT 会議室にて解答を発見。*[local-name()='item']とかなんとかやる必要ありでちょっと嫌んな感じです。
さて、ベンチマーク実行。
[naoya@judy XML-Bench]$ perl rss.pl sample.rdf
Benchmark: timing 1000 iterations of XML::LibXML, XML::RSS, XML::Simple, regexp...
XML::LibXML: 2 wallclock secs ( 1.77 usr + 0.00 sys = 1.77 CPU) @ 564.97/s (n=1000)
XML::RSS: 93 wallclock secs (91.89 usr + 0.06 sys = 91.95 CPU) @ 10.88/s (n=1000)
XML::Simple: 372 wallclock secs (358.27 usr + 0.12 sys = 358.39 CPU) @ 2.79/s (n=1000)
regexp: 0 wallclock secs ( 0.13 usr + 0.00 sys = 0.13 CPU) @ 7692.31/s (n=1000)
(warning: too few iterations for a reliable count)
圧倒的に正規表現が速い。「1000 回じゃ信頼性のある結果にはなりませんよ」とまで言われるします。XML::LibXML も思ってたよりかなり速いです。XML::RSS や XML::Simple よりも圧倒的。一方、同じ XML::Parser の XML::RSS と XML::Simple ですが、予想してたより結構差がつきました。4倍ぐらい。XML::Simple の解析ルーチンがかなり柔軟性に富んでるので、それなりのオーバーヘッドがあるんでしょうね。
正規表現が速いのは分かりましたが、汎用性やコードの保守性という点も考慮するとこの類の処理を大量に行うプログラムでは XML::LibXML + XPath がバランスが取れてて良さそうです。逆に Quick Hack なら正規表現かな。
ここまで差が付くとは思わなかったので、ベンチしてみて良かったです。
ちなみに使用したマシンのスペックは Pentium 4 2.80GHz (Hyper Threading 有効)のメモリが 1GB といったものです。