PerlでDOMとXPathを用いた効果的なXML処理

いくつかの大規模なXMLプロジェクトの分析に基づき、このコラムでは、DOMの有効かつ効率的な使い方を検討します。開発者であり、著者であるTony Daruger氏が、DOMを堅固で使いやすくするための、一連の使用パターンと関数のライブラリーを紹介します。DOMは、XML文書を作成、処理、操作する、柔軟性があり強力な手段ですが、一方ではDOMには、使いにくく、コードの安定性を欠き障害を引き起こす可能性がある側面もあります。このコラムでは、落とし穴を避ける方法を提案します。Perlのコード・サンプルを用いて、そのテクニックを紹介します。

Parand Tony Darugar (tdarugar@yahoo.com), Head of architecture, Yahoo! Search Marketing Services

Parand Tony DarugarParand Tony Darugar氏は、Webサービスのソフトウェア・プラットフォームのプロバイダーである、VelociGen Inc. の共同創設者兼チーフ・ソフトウェア・アーキテクトです。氏は、e-business統合のハイパフォーマンス・システム、インテリジェントな分散アーキテクチャー、ニューラル・ネットワーク、人工知能などに関心があります。連絡先はtdarugar@velocigen.com です。



2001年 10月 01日

DOM (Document Object Model) は、XML文書のコンテンツ、構造、スタイルを動的にアクセス、更新するための、プラットフォームおよび言語に中立なインターフェースです。DOMでは、文書を表すためのインターフェースの標準セット、これらのオブジェクトの組み合わせ方の標準モデル、およびオブジェクトにアクセスして操作する方法の標準セットが定義されています。DOMは、W3C勧告なので、Web標準として公認されています。また、Perl、C、C++、Java、Tcl、Pythonなどの幅広い言語で実装することができます。

このコラムで紹介するように、DOMは、ストリーム・ベースのモデル (SAX) が不十分な場合においてXML処理の優れた選択肢となります。残念ながら、言語中立のインターフェースや、「すべてがノードである」という抽象的概念の使用など、この仕様の中には、使い方が難しく、不安定なコードを生成しかねない側面があります。これは特に、去年1年間に渡ってさまざまな開発者が開発した大規模なDOMプロジェクトの当社調査により明らかになりました。ここでは、共通の問題と、その対応策について説明します。

DOMの概要

DOMの仕様は、どのようなプログラム言語でも使えるように作られています。したがって、すべての言語で利用できる、共通で、コアの機能セットを使用することを目指しています。また、インターフェース定義において中立であることも、DOM仕様の目標です。このため、Perlのプログラマーは、Javaを利用する場合にDOMの知識を応用し、DOMを利用する場合にJavaの知識を応用することができます。

また、DOM仕様では、文書の各パーツを型と値からなるノードとして処理します。これにより、文書のすべての面を扱うことができる、すばらしい概念的なフレームワークが提供されます。たとえば、次のようなXMLフラグメントがあるとします。

<paragraph align="left">the <it>Italicized</it> portion.</paragraph>

これをDOM構造で表すと、次のようになります。

図1: XML文書のDOM表現
図1: XML文書のDOM表現

ツリーのDocumentElementTextAttr 部分は、それぞれDOM::Node です。

設計問題

DOMの言語中立という性質の欠点は、各プログラム言語で通常使用されている方法論とパターンを採用できないことです。たとえば、XMLノードの属性は、一意の名前と値のペアの集まりなので、Perlでは普通はハッシュとして表されます。しかし、DOMでは、ノードのセットとして表され、それぞれの値にアクセスするには、別々の関数呼び出しを使用します。プログラマーは、簡単なハッシュを使用できず、代わりに多くの新しいデータ構造とアクセス方法の使い方を覚えなければなりません。これらのちょっとした不便な点が積み重なって、通常とは異なるコーディング方法が出来上がり、その結果、コードの行数が増加します。また、プログラマーは、普段使用している処理方法に代わる、そのためのDOMの方法を覚えなければなりません。

すべてがノードであるという抽象的概念は、非常にすばらしいものですが、上記の属性ノードの例のように、コーディングがすっきりしないという状況を引き起こします。同じ状況は、XMLタグ内に入っている値にアクセスする場合にも起こります。XMLフラグメント、<tagname>Value</tagname> を考えましょう。おそらく、みなさんはtagname ノードでgetValue または同等のメソッドを呼び出せば、テキスト値にアクセスできる、と考えるでしょう。しかし、実際には、テキストはtagname ノード下の1つ以上の子ノードとして扱われます。したがって、テキスト値を取得するには、tagname の子を走査して、それらをストリングに組み上げることが必要です。これには、きちんとした理由があります。tagname には他の埋め込みXMLタグが入っている可能性があります。tagname に埋め込みXMLタグが入っている場合は、そのテキスト値を取得してもあまり意味がありません。しかしながら、この便利な関数がないことから、コーディング・エラーが頻繁に起きるのが現実です。

すべてがノードであるという抽象的概念に価値がない理由としては、存在するノード・タイプが多いこと、そしてアクセス方法に統一性がないことも挙げられます。たとえば、CharacterData ノードの値を設定する場合はinsertData メソッドを使用しますが、Attr (属性) ノードの値を設定する場合は、value フィールドに直接アクセスします。ノードごとに異なるインターフェースを表すことにより、メソッドの統一性と簡潔性が低くなり、学習曲線が高くなります。


共通のコーディング問題

いくつかの大規模なXMLプロジェクトを分析したところ、DOMの利用に関する共通の問題が浮かび上がりました。次に、そのうちのいくつかを紹介します。

コードの膨張

当社の調査対象であったプロジェクトのすべてで、何よりも重大な問題が姿を現しました。簡単なことを実行するのに多くのコード行を要した、ということです。ある例では、属性の値のチェックに16行のコードを使用していました。しかし、堅固さを高め、エラー処理を改善すれば、同じ作業を3行で実行できます。コード行数が増加した原因は、DOM APIの低レベルの性質、メソッドやプログラミング・パターンの間違った適用、そしてAPI全体の知識の欠如にありました。次に、これらの問題の状況を個別に取り上げます。

DOMを走査する

われわれが調査したコードで、最も一般的な作業は、DOMを走査することでした。次に、文書のconfig セクション下の "header"というノードを探すコードを要約して示します。

リスト1. セクション内でノードを探す場合のコード (要約)
$document_root  = $dom_document->getDocumentElement();
my $config_node = $document_root->getFirstChild();
foreach my $node ( $config_node->getChildNodes() ) {
if ( $node->getName() eq "header") {
# do something
}
}

文書はルートから走査し、まず最上位の要素を取得し、次にその最初の子 (config_node) を取得し、最後にconfig_node の子を個別に調べます。残念ながら、この方法は、かなり冗長であるだけでなく、障害が発生する危険性もあり、バグをはらんでいる場合もあります。

たとえば、このコードの2行目は、getFirstChild メソッドを使用して中間ノードを取得します。すでに、問題が発生する可能性がたくさんあります。ルート・ノードの最初の子は、実際にはユーザーが探しているconfig_node ではない場合があります。何も考えずに最初の子に従えば、タグの実際の名前を無視することになり、文書の間違ったパーツを探している可能性もあります。このようなエラーがよく発生するのは、XML文書でルート・ノードの後ろに空白文字や復帰 (CR) が含まれている場合です。ルート・ノードの最初の子は実際にはDOM::Text であって、目的のノードではありません。目的のノードに正しく移動するには、Text ノードではなく、探している名前を持つノードが見つかるまで、document_root の子ノードを調べる必要があります。

また、文書の構造が期待するものとは異なる可能性があることも無視しています。たとえば、document_root に子ノードがないと、config_nodeundef に設定され、3行目でエラーが発生します。したがって、文書内を正しく移動するためには、それぞれの子ノードを個別に調べて、名前が適切かどうかをチェックするだけでなく、各ステップごとに、それぞれのメソッド呼び出しが有効な値を戻すことを確認することが必要になります。どのような入力でも処理できる、堅固でエラーのないコードを作成するためには、細部に渡って十分に注意し、多くのコード行を書かなければなりません。

タグ内のテキスト値を取得する

DOM走査の次に一般的な作業は、タグに入っているテキスト値を取得することです。XMLフラグメント、<sometag>The Value</sometag>を考えましょう。sometag ノードに移動した後、そのテキスト値 (The Value) を取得するにはどうしたら良いでしょうか。直感的に考えると、次のようになります。

$sometag->getData();

お分かりのように、上記のコードは必要なアクションを実行しません。実際のテキストは1つ以上の子ノードとして格納されているため、sometag ノードでgetData または同等の関数を呼び出すことができません。次の方法の方が良いでしょう。

$sometag->getFirstChild()->getData();

ここでの問題は、値が実際には最初の子に入っていない可能性があることです。処理命令やその他の埋め込みノードがsometag 内にあったり、テキスト値が1つだけではなく複数の子ノードに含まれていることが考えられます。空白文字がテキスト・ノードとして表されることはよくあるので、$sometag->getFirstChild() 呼び出しで取得されるのは、タグとその値の間の復帰 (CR) だけの場合もあることを思い出しましょう。実際には、すべての子を走査して、Text というタイプのノードをチェックし、完全な値が見つかるまでその値を照合する必要があります。

getElementsByTagName

DOMインターフェースには、指定された名前を持つ子ノードを探すメソッドが組み込まれています。たとえば、次のメソッドを呼び出すとします。

my @results = $document_root->getElementsByTagName("name");

この場合は、文書内からname というタグの配列 (つまりNodeList) が戻されます。これが、前述の走査方法より便利であることは言うまでもありません。また、よく発生するバグの原因でもあります。

問題は、getElementsByTagName が文書を再帰的に走査して、一致するノードをすべて戻すことです。顧客情報、会社情報、製品情報を収めた文書があるとしましょう。これらの3つの項目すべてに、name タグが含まれている可能性があります。顧客名を探すためにgetElementsByTagName を呼び出しているのに、結果的に製品名や会社名も取得すると、プログラムは異常な振る舞いをすることになるでしょう。文書のサブツリーでこの関数を呼び出せば、リスクは軽減されます。しかし、XMLは柔軟性が高いため、操作対象のサブツリーが、期待どおりの構造であり、しかも探している名前と同じ名前の疑似子ノードを含まないようにすることは、かなり大変です。


DOMの効果的な使用

DOMの設計制約による制限がある中で、仕様を効果的にかつ効率良く使用するにはどのようにしたら良いでしょうか。次に、DOM使用の基本方針とガイドラインを紹介し、作業を簡単にする関数のライブラリーを作成します。

基本方針

次の基本方針に従えば、DOMは大幅に使いやすくなります。

  • 文書の走査にDOMを使用しない
  • ノードの検索や文書の走査には、可能な限りXPathを使用する
  • 高水準の関数のライブラリーを使用して、DOMを使いやすくする

これらの方針は、共通問題の調査から直接導き出したものです。前述のように、DOMの走査はエラーの主要原因です。しかしながら、最もよく使う必須機能の1つでもあります。それでは、DOMを使わずに文書を走査するにはどうしたら良いでしょうか。

XPath

XPathは、文書のパーツのアドレッシング、検索、マッチングを行うための言語です。W3C勧告なので、標準として公認されており、ほとんどの言語とXMLパッケージに実装されています。つまり、DOMパッケージでも、直接またはアドオンを介してXPathをサポートできるわけです。

XPathは、文書を走査、検索するすばらしい手段です。ファイル・システムやURLで使用されているものと同等のパス表記を使用して、文書のパーツの指定やマッチングを行います。たとえば、XPath:/x/y/z は、文書のxというルート・ノードの下の、yというノードの下に存在する、zというノードを検索します。このステートメントは、指定されたパス構造に一致するすべてのノードを戻します。

文書の構造と、ノードの値およびその属性の両方を組み合わせれば、さらに複雑なマッチングが可能です。ステートメント/x/y/* は、親 がxであるノードyの下にあるすべてのノードを戻します。/x/y[@name='a'] は、親がxであり、値がaname という属性を持つ、yというすべてのノードと一致します。

XPathとその使い方の詳細は、このコラムでは扱いません。参考文献に詳しいチュートリアルのリンクをリストしてあります。XPathを学習すれば、XML文書の処理はより一層簡単になります。


関数のライブラリー

DOMプロジェクトの調査で驚いた点の1つが、コードをコピー・アンド・ペーストする量の多さでした。あるファイルのコードの一部を他の多くのファイルにコピー・アンド・ペーストして、似たような機能を実装させていました。優れたプログラミング知識のある経験豊富なデベロッパーが、ヘルパー・ライブラリーを作成せずに、どうしてコピー・アンド・ペースト方法を盛んに使用していたのでしょうか。その理由は、ほとんどのプログラマーにDOMの知識がなく、必要な作業を実行してくれるコードで最初に目にしたものに飛びついてしまうことにあります。DOMのスキルにあまり自信がないので、ヘルパー・ライブラリーを構成する基本的な関数を作成できないのです。

ヘルパー・ライブラリーを作成、使用すれば、定められた機能を実装するのは非常に簡単です。必要なのは、ちょっとした訓練だけです。次に、初歩的な基本ヘルパー関数を紹介します。

getValue

XML文書を扱う場合に最もよく実行されるアクションは、指定されたノードの値の検索です。前述のように、これは、文書を走査して必要なノードを探し出す点、そしてノードの値を取得する点のどちらにおいても難しいかもしれません。走査は、XPathを使用すれば簡易化でき、値の取得は、一度コーディングすれば、それを再利用することができます。getValue 関数の実装には、2つの低レベル関数のヘルパーであるfindNode を使用しました。リスト2に示すように、このヘルパーは、指定されたXPath式およびgetTextContents に一致する最初のノードを探し出して戻し、getTextContents が、渡されたノードの下にテキスト・ノードの値の連結を反復せずに戻します。

リスト2. getValueの例
sub getTextContents {
my ($node, $strip)= @_;
my $contents;
if (! $node ) { return; }
for my $child ($node->getChildNodes()) {
if ( ! is_element_node($child) ) {
$contents .= $child->getData();
}
}
if ($strip) {
$contents =~ s/^\s+//;
$contents =~ s/\s+$//;
}
return $contents;
}
sub findNode {
my ($node, $xpath) = @_;
if (! defined($node) || ! defined($xpath) )
{
return undef;
}
my $match = ($node->xql($xpath))[0];
if (! $match )
{
return undef;
}
return $match;
}
sub getValue {
my ($node, $xpath) = @_;
my $match = findNode( $node, $xpath );
if (! defined($match) )
{
return undef;
}
return getTextContents( $match );
}

getValue は、検索を開始するノードと、検索対象のノードを指定するXPathステートメントの両方を引き渡して呼び出します。この関数は、指定されたXPathと一致する最初のノードを探し、そのテキスト値を取り出します。

setValue

リスト3に示すように、もう1つの一般的なアクションは、ノードの値を希望する値に設定することです。

リスト3. ノードの値の設定
sub sub setValue {
my ($node, $xpath, $value) = @_;
my $match = findNode( $node, $xpath );
if (! defined($match) )
{
return undef;
}
foreach my $child ( $match->getChildNodes() ) {
$match->removeChild ($child);
}
$match->addText($value);
return $match;
}

この関数は、開始ノードとXPathステートメント (getValue とまったく同じです)、そして一致するノードの値の設定値であるストリングを取ります。findNode を使用して希望するノードを探し、その子をすべて削除して (その中に含まれるテキストと他の要素をすべて削除して)、そのノードのテキストの内容を、引き渡されたストリングに設定します。

appendNode

プログラムには、XML文書に含まれている値を検索して変更するものと、ノードを追加/削除することで文書そのものの構造を変更するものがあります。このヘルパー関数は、リスト4に示すように、文書へのノードの追加を簡易化します。

リスト4. ノードの追加
sub appendNode {
my ($doc, $nodename, $xpath, $value) = @_;
if (! defined($nodename) || ($nodename eq "") ) {
return undef;
}
my $match = findNode( $doc, $xpath );
if (! defined($match) )
{
return undef;
}
my $newnode;
eval {
$newnode = $doc->createElement( $nodename );
};
if ($@ || (! defined($newnode) )) {
return undef;
}
$match->appendChild( $newnode );
if ( defined($value) ) {
$newnode->addText($value);
}
return $newnode;
}

この関数のパラメーターは、DOM文書、追加するノードの名前、そのノードの追加先となるノード(つまり、新しいノードの親ノード) を指定するXPathステートメント、そしてノードのテキスト値 (任意) です。新しいノードは、指定された親ノードに付加され、その値は、引き渡されたストリングに設定されます。

copySubTree

文書のセクションを同じ文書内の別の場所や別の文書にコピーする作業は、頻繁に行われることではありませんでしたが、以前は混乱の原因でした。しかし、そのために創意工夫に富んださまさまなコピー手順が生まれました。リスト5に示すように、実際には、実装はかなり簡単です。

リスト5. 文書のセクションのコピー
sub copySubTree
{
my ($sourcenode, $destnode) = @_;
my $copy_node =  $sourcenode->cloneNode(1);
if ( $sourcenode->getOwnerDocument() ne $destnode->getOwnerDocument() ) {
$copy_node->setOwnerDocument( $destnode->getOwnerDocument() );
}
$destnode->appendChild($copy_node);
return $copy_node;
}

この関数は、ソース・ノードを取得し、それを子として宛先ノードの下にコピーします。宛先ノードは別の文書に入っている場合もあるので、その場合はサブツリーが文書間でコピーされます。


結論

DOMを使ってXML文書を操作するのは、難しくわかりにくいという不満が聞かれます。しかし、実際には、いくつかの簡単な方針に従うことで、使いやすいシステムを作成できる非常に効果的なベースにもなります。DOMは、すでにほとんどのプラットフォームに実装され、最適化されているので、複雑なプロセスでXML文書の検索や操作を行わなければならないアプリケーションにとっては格好の選択肢です。

参考文献

コメント

developerWorks: サイン・イン

必須フィールドは(*)で示されます。


IBM ID が必要ですか?
IBM IDをお忘れですか?


パスワードをお忘れですか?
パスワードの変更

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む

 


お客様が developerWorks に初めてサインインすると、お客様のプロフィールが作成されます。会社名を非表示とする選択を行わない限り、プロフィール内の情報(名前、国/地域や会社名)は公開され、投稿するコンテンツと一緒に表示されますが、いつでもこれらの情報を更新できます。

送信されたすべての情報は安全です。

ディスプレイ・ネームを選択してください



developerWorks に初めてサインインするとプロフィールが作成されますので、その際にディスプレイ・ネームを選択する必要があります。ディスプレイ・ネームは、お客様が developerWorks に投稿するコンテンツと一緒に表示されます。

ディスプレイ・ネームは、3文字から31文字の範囲で指定し、かつ developerWorks コミュニティーでユニークである必要があります。また、プライバシー上の理由でお客様の電子メール・アドレスは使用しないでください。

必須フィールドは(*)で示されます。

3文字から31文字の範囲で指定し

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む

 


送信されたすべての情報は安全です。


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=60
Zone=XML
ArticleID=240113
ArticleTitle=PerlでDOMとXPathを用いた効果的なXML処理
publish-date=10012001