目次


Perl 開発者のための XML

第 2 回 Perl を使用した高度な XML 解析手法

ツリー解析とイベント駆動型解析の考察

Comments

コンテンツシリーズ

このコンテンツは全#シリーズのパート#です: Perl 開発者のための XML

このシリーズの続きに乞うご期待。

このコンテンツはシリーズの一部分です:Perl 開発者のための XML

このシリーズの続きに乞うご期待。

はじめに

驚くほど広範な Perl アプリケーションで XML ツールとして選ばれているのは、連載の第 1 回 (「参考文献」を参照) で紹介した XML::Simple です。XML::Simple は、XML 入力ファイルを操作しやすい Perl データ構造に変換し、Perl データ構造を XML として書き込みます。ただし覚えておいてもらいたいのは、この手法は特定の状況では機能しないということです。

XML 文書の表現をメモリー内に作成し、それからかなり複雑な方法や予測不可能な方法でその表現を検索または変換するには、XML::Simple は最適ではありません。そんな場合に活躍するのは、ツリー解析です。一方、XML 文書がメモリー内に収まらなかったり、XML 文書の長さが不明なストリームの場合には XML::Simple. は使用できません。代わりに使用するのは、イベント駆動型パーサーです。始めはほとんどの人がイベント駆動型パーサーは使いづらいと感じますが、いったんこのスタイルの解析方法に慣れてしまえば、SAX がお気に入りのツールになるはずです。

この記事では、Perl を使ったこれらの 2 つの高度な XML 解析方法について説明します。

手順の開始

この記事の手順に従うには、オープン・ソースの Perl モジュールが必要です。ほとんどの場合、次のいずれかの方法で取得できます。まず、Windows をお使いの場合は ppm を使用します。オペレーティング・システムが UNIX® または Linux™ の場合は、CPAN にアクセスします (「参考文献」にリンクを記載)。リポジトリーがよくわからない場合は、連載第 1 回で紹介しているので参照してください。

リスト 1 に、UNIX/Linux にモジュールをインストールする方法を示します。これはルートとして実行し、システム上のすべてのアカウントでこれらのモジュールを使用できるようにします。これらのモジュールには依存関係がありますが、一部の依存関係はご使用のシステムに存在しない場合があります。cpan が正しく構成されていれば (follow=yes)、依存関係が自動的にインストールされます。

リスト 1. この記事で使用するモジュールを CPAN から取得する方法
$ perl -MCPAN -e shell
cpan> install XML::LibXML XML::SAX::Base XML::SAX::ExpatXS XML::SAX::Writer
cpan> quit

Windows でのインストール手順はもっと簡単です (リスト 2 を参照)。この場合も、管理アカウントで実行することをお勧めします。

リスト 2. PPM によるモジュールの取得方法
$ppm install XML::LibXML XML::SAX::Base XML::SAX::ExpatXS XML::SAX::Writer

ツリー解析

大抵のプログラマーは、XML をツリー構造で表示するとわかりやすいと感じるはずです。このような XML の表示は、長年に渡るプロセスの末、文書オブジェクト・モデル (DOM) として定型化されました。DOM Level 3 は、2002年に完成しています。

DOM は XML 文書を二重にリンクされたノードのツリーとして表現します。つまり、各レベルの最初の子がその親と兄弟にリンクされます。ツリーには主要なプログラミング言語の実装とともに大規模な関数セットが定義されます。

DOM ツリーをナビゲートするにはリンクを辿るという方法もありますが、一般的に XPath プロトコルを使用するほうが、プログラマーの時間を節約できます。XPath プロトコルはサブ言語で、これによってノードへのナビゲーション、ノード・セットの取得などが可能になります。

DOM 仕様自体へのリンク、そして DOM 仕様、XPath、関連プロトコルをわかりやすく紹介している資料については、「参考文献」を参照してください。

XML 文書を DOM ツリーに解析できる Perl モジュールは多数ありますが、なかでも最も優れたモジュールとして挙げられるのは、Petr Pajas の XML::LibXML です (「参考文献」を参照)。XML::LibXML は、DOM パーサー、XPath の部分的実装、そして SAX2 の実装が含まれる Gnome プロジェクトの多角的パッケージ、libxml2 をラップしています (以下に説明)。

リスト 3 は、連載第 1 回で操作した XML ファイルです (「参考文献」を参照)。第 1 回では、このファイルを XML::Simple で解析して Perl データ構造として表現を変更し、それから XML::Simple を使ってテキスト形式の XML に変換し直しました。

リスト 3. Rosie's Pet Shop、pets.xml
<?xml version='1.0'?>
<pets>
  <cat>
    <name>Madness</name>
    <dob>1 February 2004</dob>
    <price>150</price>
  </cat>
  <dog>
    <name>Maggie</name>
    <dob>12 October 2002</dob>
    <price>75</price>
    <owner>Rosie</owner>
  </dog>
  <cat>
    <name>Little</name>
    <dob>23 June 2006</dob>
    <price>25</price>
  </cat>
</pets>

このファイルを XML::LibXML で解析するのは簡単です リスト 4リスト 5 のプログラムからの出力を参照)。まず、単純な $parser->parse_file で、DOM モデルに対する XML ツリー構造を作成します。次に、ツリー内のノードにサブ要素を追加する単純な Perl サブルーチンを定義し、このサブルーチンを使って個別のペットを表すサブツリーを構成します。さらにこの addPet() サブルーチンで、いくつかの新しいペット (スナネズミとハムスター) を在庫に追加します。

リスト 4. Rosie's Pet Shop の在庫の XML::LibXML 解析
#!/usr/bin/perl -w
use strict;
use XML::LibXML;

my $parser = XML::LibXML->new;
my $doc    = $parser->parse_file('pets.xml')
                or die "can't parse Rosie's stock file: $@";
my $root = $doc->documentElement();
sub addSubElm($$$) {
    my ($pet, $name, $body) = @_;
    my $subElm = $pet->addNewChild('', $name);
    $subElm->addChild( $doc->createTextNode($body) );
}
sub addPet($$$$) {
    my ($type, $name, $dob, $price) = @_;
    # addNewChild is non-compliant; could use addSibling instead
    my $pet = $root->addNewChild('', $type);
    addSubElm ( $pet, 'name', $name );
    addSubElm ( $pet, 'dob',  $dob  );
    addSubElm ( $pet, 'price', $price );
}    
addPet('gerbil',  'nasty', '15 February 2006', '5');
addPet('hamster', 'boris', '5 July 2006',      '7.00');

my @nodeList = $doc->getElementsByTagName('price');
foreach my $priceNode (@nodeList) {
    my $curPrice = $priceNode->textContent;
    my $newPrice = sprintf "%6.2f", $curPrice * 1.2;
    my $parent = $priceNode->parentNode;
    my $newPriceNode = XML::LibXML::Element->new('price');
    $newPriceNode->addChild ( $doc->createTextNode( $newPrice ) );
    $parent->replaceChild ( $newPriceNode, $priceNode );
}
print $doc->toString(1);        # pretty print

次に DOM の手腕を発揮して、ツリー内の価格ノードへの参照リストを取得し、それぞれの価格を 20% 引き上げます。要素内では複数のテキスト・ノードでテキスト (価格) を表すことができるので、その場で価格を変更するのではなく、ノードから価格を取得して価格を引き上げ、形式を再設定してから元のノードを完全に置き換える方法が一番簡単です。これは当然、第 1 回の Perl コードでの変換より著しく複雑になります。

リスト 5. ツリー・パーサーの出力 (整理済み)
<?xml version="1.0"?>
<pets>
  <cat>
    <name>Madness</name> <dob>1 February 2004</dob> 
<price>180.00</price>
  </cat>
  <dog>
    <name>Maggie</name> <dob>12 October 2002</dob> <price> 
90.00</price>
    <owner>Rosie</owner>
  </dog>
  <cat>
    <name>Little</name> <dob>23 June 2006</dob> <price> 
30.00</price>
  </cat>
  <gerbil>
    <name>nasty</name><dob>15 February 2006</dob><price>  
6.00</price>
  </gerbil>
  <hamster>
    <name>boris</name><dob>5 July 2006</dob><price>  
8.40</price>
  </hamster>
</pets>

従来のツリー・パーサーを使って XML を操作する場合は、これが一般的な方法です。ソースは XML 形式のテキストで、これが DOM ツリーに変換されます。ツリーをナビゲートするには、リンクを 1 つずつ辿ってノードをウォークするか、XPath のようなコマンドを使用して一連のノードへの参照を取得します。取得した参照を使用すれば、ノードを編集できます。それからツリーをディスクに再び書き込んだり、整然と表示したりすることができます。

規模が小さく単純なツリーの場合、エンジニアリングのコストという点では XML::Simple を使用するのが一般的に安上がりな方法です。ただし極めて複雑な XML 文書の場合には、XML::LibXML を使用した方が、getElementsByTagName のようなメソッドを使用できるので便利です。このメソッドは手作りの Perl と XML::Simple より実行するのに時間がかかるかもしれませんが、作成する必要もデバッグする必要もありません。

イベント・ベースの解析: SAX

SAX (Simple API for XML) の解析方法はまったく異なります。この方法では、最初にオーバーヘッドがかかります。SAX は文書を一連のイベントとして考えるため、それぞれのイベントに応答する方法を指定してあげなければならないためです。イベントには、start_document、end_document、start_element、end_element、そして characters などがあります。完全なイベント・リストについては、「参考文献」の「Perl SAX 2.1 Binding」を参照してください。いずれの文書についても、Perl プログラマーがイベント・タイプごとにハンドラー・メソッドを使用できるようにする必要があります。

この解析方法は、退屈な繰り返しのレシピのように思えますが、実際にはこの後すぐにわかるように絶好のツールになります。

XML::LibXML には SAX インターフェースがありますが、DOM パーサーであることに変わりはありません。つまり、文書全体をメモリーに読み込んでから、イベント指向のインターフェースを文書に提供します。このような XML::LibXMLが役立つことは多々ありますが、メモリー内に収まらない文書や Jabber/XMPP などの XML ストリームである文書は扱えません。そこで使用することになるのが、XML::SAX::ExpatXS です。このモジュールは James Clark の由緒正しい外部パーサーをラップするもので、安定性にも処理速度にも優れています。

例えば、連載第 1 回で使用したペット・ショップと同じような新しいペット・ショップがあるとします。リスト 6 は、このショップの在庫の一部です。

リスト 6. Lizzie's Petatorium、pets2.xml
<stock>
<item type="iguana" cost="124.42" location="stockroom" age="1"/>
<item type="pig" cost="15" location="floor" age="0.5"/>
<item type="parrot" cost="700" location="cage" age="6"/>
<item type="pig" cost="117.50" location="floor" age="3.2"/>
</stock>

上記を SAX2 で解析するには、パーサーが作成するイベントを処理するためのハンドラーが必要となります。最も単純なイベント・ハンドラーは、イベントごとにテキストを出力する書き出しプログラムです。リスト 7 のコードが、新しい XML を解析します。

リスト 7. pets2.xml の SAX 解析
#!/usr/bin/perl -w
#use strict;
use XML::SAX::ParserFactory;
use XML::SAX::Writer;
my $writer = XML::SAX::Writer->new;

$XML::SAX::ParserPackage = "XML::SAX::ExpatXS";
my $parser = XML::SAX::ParserFactory->parser(Handler => $writer);

eval { $parser->parse_file('pets2.xml') };
die "can't parse Lizzie's stock file: $@"   if $@;

この XML によって作成された出力が、リスト 8 です。

リスト 8. SAX パーサーの出力
<?xml version='1.0'?><stock>
  <item cost='124.42' location='stockroom' type='iguana' age='1' />
  <item cost='15' location='floor' type='pig' age='0.5' />
  <item cost='700' location='cage' type='parrot' age='6' />
  <item cost='117.50' location='floor' type='pig' age='3.2' />
</stock>

ExpatXS を使用する際には、以下の点に注意してください。

  • すべてのツールが SAX または SAX2 のいずれかであること。組み合わせて使用することはできません。リスト 7 の XML::SAX::Writer の代わりに XML::Handler::YAWriter を使用すると、エラー・メッセージは受信しませんが、出力のほとんどがガーベッジになります。ExpatXS は SAX2 パーサーであるため、SAX2 書き出しプログラムと併せて使用する必要があります。
  • パーサー・エラーをチェックするには、パーサーを eval にラップしてから $! でなく $@ をテストすること。
  • ハンドラーは使用する前にセットアップすること。プログラマーは SAX パーサーをパイプライン (以下で詳しく説明するポイント) として左から右への方向で可視化するのに対し、初期化は右から左への方向で実行します。つまり、パイプラインは P > W のようになるため、初期化は逆に W から P の順に行わなければなりません。

ドライバーとフィルター

SAX の天才ぶりはここからが本番です。SAX は、パーサーが一連のイベントを生成して、それぞれのイベントをハンドラーに渡すというイベント・ストリームを定義します。パーサーまたはハンドラーのどちらか、あるいはその両方のように見える抽象モジュールを想像してみてください。このモジュールはパーサーのように SAX イベントを生成します。それと同時に、ハンドラーでもあります。このハンドラーは肩書きを変えてパーサーの役割を引き受け、イベントを次のハンドラーに渡すことで、あらゆる標準 SAX イベントを処理できます。つまり、SAX イベント・ハンドラーはイベントに渡すだけの一連のデフォルト・メソッドを定義します。これらのメソッドを処理するモジュールが、XML::SAX::Base です。

考えられるあらゆる SAX イベント・ハンドラーを定義するには、プログラマーが XML::SAX::Base を拡張して対象のメソッドをオーバーライドします。その他のイベントはただ渡されるだけとなります。このようなイベント・ハンドラーを連鎖させることで、UNIX コマンド・ラインとまったく同じようなパイプラインを作成できます。ハンドラーには明確に定義されたインターフェースがあり、ことのほか明確に定義されたコンテンツ、つまり XML があります。

さらに、パイプの両端で同じ方法を採ることができます。先頭部分にあるジェネレーターは、XML 文書を使用してイベントを生成する SAX2 パーサーです。実際には、このジェネレーターは、SAX イベントを生成するものであれば何にでもなることができます。例えば、データベース内のテーブルを読み込んで一連の SAX イベントを出力するモジュールを作成することも可能です (このモジュールは、XML::Generator::DBI として存在します)。

通常はパイプのもう一方の端が SAX イベントを使用して文書を出力します。XML::SAX::Writer は、まさにこれを行うモジュールです。ただし、データベースに書き込むのも同じく簡単です (XML::SAX::DBI)。

以上に説明した SAX の実力は、2 つの大きな利点をもたらします。第一に、イベント・ストリームを単純な方法で変換する SAX ハンドラーの開発が促進されます。実際その通りで、現在 SAX 2.1 バインディングを実装するオープン・ソースの Perl モジュールは何百もあります (「参考文献」を参照)。第二に、設計者は既存のハンドラーと連動してジョブを実行するための必要最低限の機能を提供するハンドラーを指定するという作業に専念できます。この 2 つが、安価なマシン・リソースを交換してプログラマーの貴重な時間を節約します。

XML::SAX::Base の詳細

Kip Hampton の XML::SAX::Base を使用してハンドラーを設計するのに必要なのは、2 つの単純なステップです。まず、ハンドラーが基本クラスを拡張するようにします。次に、プログラマーが必要に応じて基本メソッドをオーバーライドします。これで、イベントを中止することや、基本クラスでオーバーライドされたメソッドを呼び出すことが可能になります。ハンドラーが、オーラーライドしているモジュールに含まれるメソッドではなく、スーパークラスのメソッドを呼び出すことが肝心です (リスト 9 を参照)。

リスト 9. XML::SAX::Base の使用方法
package XyzHandler;
  use base qw(XML::SAX::Base); # extend it

  sub start_element {          # override methods as necessary
    my $self = shift;
    my $data = shift;          # parameter is a reference to a hash
    # transform/extract/copy data
    $self->SUPER::start_element($data);
  }

まとめ

3 部構成の連載第 2 回となるこの記事では、XML 解析の非常に複雑な世界を、要点を押さえて簡潔に説明しました。

まず始めに説明したのは、XML 文書をメモリー内でオブジェクトのツリーに変換する方法です。大抵のプログラマーにとって、この方法は自然に感じられるはずです。実際、データがメモリー内に収まる限り、いろいろな意味で便利な方法となります。

次に紹介したのは、SAX とイベント・ベースの解析です。XML 文書が非常に大きい場合や果てしないストリームである場合は、この手法を使用しなければなりません。この記事の説明でおわかりのように、このような条件に対処するために開発されたツールは、まったく別のプログラミング・スタイルに役立ちます。そのようなツールとして極めて豊富な機能を持っているのが、SAX パイプラインです。

次回の記事では、より複雑なアプリケーションで DOM と SAX 両方の解析方法を使用する方法を紹介します。


ダウンロード可能なリソース


関連トピック

  • Perl での参照の使用方法に関するチュートリアル: Perl 参照とネストされたデータ構造のすべての側面に関する完全な資料です。
  • Perl developers: Fill your XML toolbox」(Parand Darugar 著、developerWorks、2001年6月): Perl を使って XML を操作するための 20 の必須ツールおよびライブラリーの概要を紹介しています。
  • Effective XML processing with DOM and XPath in Perl」(Parand Darugar 著、developerWorks、2001年10月): DOM を効果的かつ効率的に利用する方法を説明しています。
  • High-order perl』(Mark Jason Dominus 著、2005年): Perl での関数型プログラミング手法、そして他の関数を変更および作成できる関数を作成する方法を解説した本です。
  • IBM XML 認証: XML や関連技術の IBM 認定開発者になる方法について調べてください。
  • XML Technical library: 広範な技術に関する記事とヒント、チュートリアル、標準、そして IBM レッドブックについては、developerWorks XML ゾーンを参照してください。
  • XML::LibXML は XML 文書解析用の最も優れた Perl モジュールの 1 つです。
  • Document Object Model (DOM) 仕様: プログラムとスクリプトがコンテンツ、構造、そして文書スタイルに動的にアクセスして更新することを可能にする、プラットフォームと言語に依存しないインターフェースの詳細を調べてください。
  • Perl SAX 2.1 バインディング: Perl モジュールで使用する SAX のバージョンを説明する資料を入手してください。
  • Perl: 最新バージョンを入手して実行してください。
  • 壮大な CPAN Perl ライブラリー: Comprehensive Perl Archive Network にアクセスしてください。この記事で触れたすべてのモジュールのリンクが掲載されています。
  • Windows 対応 PPM (Perl Package Manager): 一般的な Perl CPAN モジュール (Tk、DBI など) をインストール、アンインストール、アップグレード、あるいはその使用方法を管理できるツールを ActivePerl と一緒に入手してください。
  • Grant McLean の XML::Simple: 基礎 XML 解析モジュールの上位に位置する単純な API レイヤーとして XML::Simple モジュールを試してみてください。
  • XML 仕様: Extensible Markup Language (XML) の詳細を調べてください。
  • XPath 1.0: DOM ツリーをナビゲートするための言語の仕様を入手してください。
  • XSLT 1.0 仕様: XML 文書間での変換について学んでください。
  • Perl によるツリー・ベース XML のスクリプト作成」(Parand Darugar 著、developerWorks、2000年7月): Perl を使ったツリー・ベースの XML 解析を詳細に紹介しています。
  • IBM トライアル・ソフトウェア: developerWorks から直接ダウンロードできるトライアル・ソフトウェアで、次の開発プロジェクトを構築してください。

コメント

コメントを登録するにはサインインあるいは登録してください。

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=60
Zone=XML, Web development
ArticleID=249859
ArticleTitle=Perl 開発者のための XML: 第 2 回 Perl を使用した高度な XML 解析手法
publish-date=02062007