前回ではSAXを使ってXMLをパースし、データがどのように読み取られるかを試しました。では、属性を変更して、処理したデータをファイルに出力してみましょう。XML処理に関しては、既に様々な種類のPerlモジュールが開発されていて、Filterを使用して柔軟にデータを処理することが可能です。
ところが!
僕が借りているレンタルサーバーには、XMLのデータ出力に関連するモジュールがインストールされていない・・・。なぜ・・・!?きっと理由はあるのでしょう。CPANなどからモジュールファイルを入手して、適当なフォルダにアップロードすればモジュールを利用できると思うのですが、ここではしません。なぜかというと、かなりいろいろなファイルをダウンロードする必要があるから。しかも、XMLの特定の「要素」のある「属性」を編集するだけなら個人のちからで書けそうです。あえて車輪の再開発をしてみて、SAXとXMLの動作に対する理解を深めてみたりして。
ではプログラムの作成に取り掛かりましょう。まずは、全体像から。やりたいことは、ある特定の名前の要素を探し出して、その要素内の属性を変更します。そして、変更したデータをファイルに出力します。まずパーサーを利用して目当ての要素を探し出しましょう。
例えば以下のようなXMLを例にします。
pen要素のcolor属性をblackからredに変えます。
<?xml version="1.0" encoding="UTF-8" ?>
<bag>
<book size='a4' / >
<pen color='black' / >
</bag>
ステップその1として、pen要素を探しましょう。「探す」というか、もっと正確に表現すると、XMLファイルがパースされる際に実行されるコールバックを利用して、パーサーがpen要素に遭遇した瞬間を捕まえる、というイメージです。パーサーが要素に遭遇すると、start_elementというコールバックが実行されるので、そのサブルーチンの中で要素の名前がpenであるかどうかを調べてみましょう。コールバックを書くファイルは、パーサーに渡すハンドラオブジェクト(コールバックを記述した任意のクラスのインタンス)の中です。全ソースはページ下部にあります。
sub start_element {
my ($self, $data) = @_;
if($data->{Name} eq 'pen'){print 'pen found';}
}
サブルーチン内の一行目で引数を受け取ります。第一引数はハンドラ自体へのリファレンスで、ここでは使用しません。第二引数は、要素の情報がいろいろと詰まっているハッシュです。二行目で、pen要素かどうかをチェックしています。要素の名前は$data->{Name}で知ることが出来るので、それがpenかどうかをifで判断して結果が真の場合、pen foundと出力しています。
ではステップ2。要素がpenであった場合に、colorという属性があるかどうかを調べてみましょう。この部分は、属性の情報がハッシュの入れ子になっていてちょっとややこしいです。
sub start_element {
my ($self, $data) = @_;
if($data->{Name} eq 'pen'){
my $hash_attributes = $data->{Attributes};
my @attributes = values %$hash_attributes;
foreach $temp(@attributes){
if($temp->{Name} eq 'color'){print 'color found';}
}
}
}
一行目から三行目までは、pen要素かどうかをチェックするための部分で、先ほど述べました。四行目、要素内の属性を得ます。$data->{Attributes}を$hash_attributesに代入します。この$hash_attributesはハッシュで、「キー」が空、「値」が各属性の情報です。まず$hash_attributesの値のみを配列にコピーしましょう。
my @attributes = values %$hash_attributes;
では、今配列にコピーした属性を調べて、color属性を見つけます。配列を構成するデータをforeachで一つずつ見てみます。$tempに各属性の情報が格納されます。そしてこの$tempはハッシュになっています。ちょっと込み入ってきました・・・。属性の名前は、$temp->{Name}で得ることが出来ます。なので、それがcolorであるかどうかをやっとifで判断することができます。ふうー・・・。
以上で、特定の要素の、特定の属性がパーサーに引っかかるタイミングを捕まえることができるようになりました。ではここから、最終目的である「データの変更・出力」を考えていきます。パーサーがXMLをパースするというのは、単に解析の状況に応じてコールバックを呼ぶというだけなので、メモリ上にはXMLのデータは残りません。だからいくら巨大なファイルを解析したとしても、サーバーには負担はかからないんですね。SAXの特徴でした。XMLを変更してファイルの出力するには、解析と平行して、最終的に得たいデータを一時ファイルに逐次出力してやるのがよいでしょうか。ここではその手法でいくとしましょう。
今回の単純なXMLの例の場合、ファイル出力に必要なポイントを列挙すると以下の感じです。
・まずドキュメントが始まったら、一時ファイルをオープンしてXML宣言を出力する。
・パーサーが要素に遭遇したら、一時ファイルにも同じ名前で要素を出力、属性がある場合はそれらも出力する。
・要素の終わりをコールバックで捕まえて、タグを閉じる。
・ドキュメントの終わりに到達したら、一時ファイルをクローズ。
ではコードを書いてみます。必要なコールバックは、start_document、start_element、end_element、end_documentの4つです。ではまずstart_documentから。ファイルのロック(flock)は記述していませんが、必要に応じて追加する必要があります。
sub start_document(){
$filename = 'temp.xml';
if(open(FP,"+>$filename")){
print FP "<?xml version='1.0' encoding='UTF-8' ?>";
}else{
exit;
}
}
temp.xmlという名前で出力先である一時ファイルを書き込みモードでオープンします。
end_documentでは、一時ファイルをクローズするだけです。
sub end_document(){
close(FP);
}
start_elementは、pen要素のcolor属性の場合のみデータを変更して出力して、それ以外の場合はパースした内容をそのまま一時ファイルに出力します。
sub start_element {
my ($self, $data) = @_;
print FP '<';
print FP $data->{Name};
my $hash_attributes = $data->{Attributes};
my @attributes = values %$hash_attributes;
foreach $temp(@attributes){
print FP ' ';
print FP $temp->{Name};
print FP '=\'';
if($data->{Name} eq 'pen' && $temp->{Name} eq 'color'){
print FP 'red';
}else{
print FP $temp->{Value};
}
}
print FP '\'';
print FP '>';
}
14行目がpen要素のcolor属性だった場合の処理、16行目がそれ以外の場合。pen要素のcolor属性だった場合、redという値に変更します。「それ以外」の場合は、パースされた情報をそのまま一時ファイルに出力します。
sub end_element(){
my ($self, $data) = @_;
print FP '</';
print FP $data->{Name};
print FP '>';
}
end_elementでは単純にタグを閉じるだけです。例えば</pen>という形式で一時ファイルに出力されます。
必要なコールバックは以上の4つです。これで今回の目的である「pen要素のcolor属性をblackからredに変える」ということができます。パーサーを作成して、パースを開始するとtemp.xmlという変更後のファイルが作成されます。以下各ソースファイルです。
foo.xml
<?xml version="1.0" encoding="UTF-8" ?>
<bag>
<book size='a4' / >
<pen color='black' / >
</bag>
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_document(){
$filename = 'temp.xml';
if(open(FP,"+>$filename")){
print FP "<?xml version='1.0' encoding='UTF-8' ?>";
}else{
exit;
}
}
sub end_document(){
close(FP);
}
sub start_element {
my ($self, $data) = @_;
print FP '<';
print FP $data->{Name};
if($data->{Name} eq 'pen'){
print FP ' ';
print FP $temp->{Name};
print FP '=\'';
my $hash_attributes = $data->{Attributes};
my @attributes = values %$hash_attributes;
foreach $temp(@attributes){
print FP ' ';
print FP $temp->{Name};
print FP '=\'';
if($data->{Name} eq 'pen' && $temp->{Name} eq 'color'){
print FP 'red';
}else{
print FP $temp->{Value};
}
print FP '\'';
}
print FP '>';
}
}
sub end_element(){
my ($self, $data) = @_;
print FP '</';
print FP $data->{Name};
print FP '>';
}
1;