PerlでXMLを処理するモジュールには大きく分けてDOMとSAXがあります。僕が借りているレンタルサーバーにたまたまSAXと、その関連のモジュールがインストールされていたので、今日はSAXを触ってみます。
XMLのファイルを全てメモリ上に展開するDOMとは違って、SAXではXMLをファイルの初めから読みながら(メモリをこまめに解放しながら)パースしていくらしい。サーバーにSAX関連のモジュールがインストールされている理由は、処理に負担がかかりにくいという理由なんだろうか。ちなみにSAXよりポピュラーなDOMはインストールされていなかった。SAXを使っくれ、というささやかな勧告かもしれません。
さてそのSAXですが、処理の方法には一種独特のものがあります。「
XMLファイルをメモリに全て展開せずに先頭から少しずつパースしていく」というイメージを頭に入れておくと、実際のコードに触れる際に悩まずに済むかもしれません。というのも、ActionScriptでXMLを取り回すのに慣れていた僕はSAXとの違いにかなり悩みました。ActionScriptでのXMLの処理は、おそらくDOMのようにメモリ上に展開して行うものだと思うので、ファイル内の各要素や属性にかなりランダムにアクセスが出来ました。メモリを食う分、使い勝手は良かったのに・・。ActionScriptはいったん忘れよっと。
さてSAX。
SAXのモジュールを使うよ、という一文を書いてあげます。
use XML::SAX;
次にパーサーを作ります。「パーサー」は構造分析担当者とでも訳せるかな。つまり読む人です。
my $parser = XML::SAX::ParserFactory->parser(Handler => MySAXHandler->new);
ここでいきなり僕は「?」でした。
XML::SAX::ParserFactoryは何かと言うと、PerlにインストールされているXMLパーサーの中から、最適なものを選ぶためのクラス。詳しくは以下。
CPAN http://search.cpan.org/~grantm/XML-SAX-0.16/SAX/ParserFactory.pm
つまりは、サーバーにおけるPerlの環境の多様性に対応するためにユーザーとSAXとの間にワンクッション置かれているんだな、きっと。「今日入ってる一番いいネタ下さい」というイメージだろうか。そしてそのXML::SAX::ParserFactoryがparserというメソッドを呼んでいて、その返り値(ここではおそらくパーサー本体への参照)を$parserに代入している、という流れ。
parserというメソッドに渡している引数を見てみると、
Handler => MySAXHandler->new
となっています。ここで「先頭から少しずつパースしていく」というイメージが重要になってきます。SAXではパーサーがXMLファイルを解析していくのにしたがって、「ここはドキュメントの先頭ですよ」とか、「ここから要素が始まります」とか、「さっきの要素はここまで」のように、ユーザーに逐次イベントを教えてくれるようです。つまりパーサーがXMLファイルの解析を実況解説して、ユーザーはそれらをよく聞きながら場合場合に備えた処理を記述する、という流れらしい。ユーザーが用意したコールバック関数でそれらのイベントを捕まえてやるんだけど、そのコールバック関数のセットをハンドラと呼ぶ。それで、パーサーを作成するタイミングでハンドラを渡す必要がある、とのこと。さて、パーサーのインスタンスを作っているところに戻ります。Handler => MySAXHandler->newはつまり、「ハンドラはこれです」と指定しているわけです。ハンドラの中身は、自分で用意したコールバック関数で、書式もおおよそ決まっています(詳細は後述)。コールバック関数を記述したMySAXHandlerというクラスを自分で作って、オブジェクトをnewで作ってあげます。
さて、ここからハンドラの中身についてです。別ファイルにMySAXHandler.pmという名前で書くことにします。
package MySAXHandler;
MySAXHandlerというパッケージを作ってやって、その中にコールバック関数を書いてあげればよいです。ですが、パースする際に必要な全てのコールバック関数を自分で書く必要はなくて、基本的には、雛形になるXML::SAX::Baseを継承してしまいます。
use base qw(XML::SAX::Base);
その上で、自分でカスタマイズする関数だけその下に書いてやればOK。特に重要だと思われるのがstart_element。これは、パーサーがXMLの要素に出くわしたときに実行される。つまり、この関数を書いてやらないとおそらく何も起こらずに処理を終えるはずです。では、オーバーライドする関数の中身を見てみます。
my ($self, $data) = @_;
引数を受け取ります。一番目はパーサー自体への参照なのでここでは使いません。2番目は要素のデータが全て格納されています。まず手始めに要素の名前を標準出力に出力してみます。
print "$data->{Name}";
要素のデータはハッシュになっています。次に属性。任意数の属性が格納されたハッシュをまず得ます。
my $hash_attributes = $data->{Attributes};
ちなみにここで得られるハッシュの「キーと値」ですが、キーは空で、値に個々の属性のデータが入っているようです。なので、値のみを配列に移します。
my @attributes = values %$hash_attributes;
そして、配列の要素それぞれ自体がさらにハッシュになっていて、例えば一番目の要素の名前は$attributes[0]->{Name}、値は$attributes[0]->{Value}で得られる。ここでは@attributesの中身を「要素名='値'」というフォーマットで全て出力してみます。
foreach $temp(@attributes){
print "$temp->{Name}='$temp->{Value}'<br />";
}
ハッシュのエントリーが1つしかないとき、charactersという関数が呼ばれます。これを利用してテキスト要素を捕まえることができます。Dataというキーを使ってテキストノードの文字列を得ることができます。「ハッシュのエントリーが1つしかないとき」とは、パーサーがテキストノードに出くわしたタイミングとイコールの関係ではなく、その他にもいろいろなタイミングで呼ばれる場合があるので、必要の無い場合に呼ばれたコールバックは無視する必要がありますね。出力結果を見ると、確かにテキストノード以外にもcharactersが呼ばれているのが確認できますね。
さて、ハンドラを設定して、パーサーを作成して、いよいよパースを開始してみます。パーサーにXMLファイルを指定してあげます。
$parser->parse_uri("foo.xml");
これはやけにあっさりとしてますね。
では以下、今回使用したソースファイルと出力結果です。
saxSample.cgi
#!/usr/bin/perl
use XML::SAX;
use MySAXHandler;
my $parser = XML::SAX::ParserFactory->parser(
Handler => MySAXHandler->new);
print "Content-type: text/html\n\n";
$parser->parse_uri("foo.xml");
MySAXHandler.pm
#!/usr/bin/perl
package MySAXHandler;
use base qw(XML::SAX::Base);
sub start_element {
my ($self, $data) = @_;
print "$data->{Name}<br />";
my $hash_attributes = $data->{Attributes};
my @attributes = values %$hash_attributes;
foreach $temp(@attributes){
print "$temp->{Name}='$temp->{Value}'<br />";
}
}
sub characters{
my ($self, $data) = @_;
if($data->{Data} ne ''){
print "Data:$data->{Data}<br />";
}
}
1;
foo.xml
<?xml version="1.0" encoding="UTF-8" ?>
<case>
<drink taste='sweet' alchol='no' caffeine='yes'>cola</drink>
<drink taste='sweet' alchol='no' caffeine='no'>sprite</drink>
<drink taste='bitter' alchol='yes' caffeine='no'>beer</drink>
</case>
出力結果
case
Data:
drink
caffeine='yes'
taste='sweet'
alchol='no'
Data:cola
Data:
drink
caffeine='no'
taste='sweet'
alchol='no'
Data:sprite
Data:
drink
caffeine='no'
taste='bitter'
alchol='yes'
Data:beer
Data: