JavaでDOMとXPathを使って効果的にXMLを処理する

数多くのプロジェクトを分析して得られたアドバイスおよびコードの提案

この記事では、いくつかの大規模なXMLプロジェクトを分析した結果に基づいて、JavaでDOMを効果的に使用する方法について解説します。DOMはXML文書を作成、処理、操作するための柔軟かつ強力な手段となりますが、その反面、扱いにくく、脆弱でバグの多いコードになってしまう可能性もはらんでいます。DOMを堅固で使いやすくするためのJavaの使用パターンと関数のライブラリーを、著者Parand Tony Darugerがご紹介します。

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年 12月 01日

The Document Object Model (文書オブジェクト・モデル、DOM) は、プラットフォームや言語に依存せずにXML文書のコンテンツ、構造、およびスタイルに動的にアクセスして更新するためのW3C公認の標準です。DOMは文書を表記するインターフェースの標準セット、および文書にアクセスして操作するインターフェースの標準セットを定義しています。DOMは幅広いサポートを得て普及し、Java、Perl、C、C++、VB、Tcl、およびPythonなどのさまざまな言語で実装されています。

この記事で説明するように、ストリーム・ベースのモデル (たとえばSAX) が効果的でないケースでは、DOMが有力な選択肢です。残念ながら、言語に依存しないインターフェースや、「あらゆるものがノード」式の抽象化といった特徴のためにこの仕様は扱いにくく、脆弱なコードが作り出される傾向があります。過去1年間に多数の開発者によって構築されたいくつかのDOMプロジェクトを最近分析した結果、この傾向がとくに顕著に見られます。以下では、よく見られる問題と、その解決策を示します。

The Document Object Model

DOM仕様は、どんなプログラム言語でも使用できるように意図されています。このため、すべての言語で利用できる機能からなる共通のコア・セットを使用します。また、DOM仕様はインターフェース定義においても非依存性を保ちます。これによって、JavaプログラマーはVisual BasicやPerlで作業するときにもDOMの知識を活かすことができます (その逆も当てはまります)。

さらに、この仕様は文書のあらゆる部分を、(タイプおよび値を持つ) ノードとして扱います。こうして、文書のすべての面を扱うエレガントな概念上のフレームワークとなっています。たとえば、次のようなXMLの一部

<paragraph align="left">the <it>Italicized</it> portion.</paragraph>
図1: XML文書のDOM表記
図1: XML文書のDOM表記

ツリーの中のDocumentElementText、およびAttr の各部は、DOMではすべてNode です。

エレガントな抽象化には代価が伴います。
<tagname>Value</tagname>
というXMLの一部を考えてみてください。このテキスト値 (Value) は通常のJavaString オブジェクトで表記され、単純なgetValue 呼び出しによってアクセスできると読者は考えるかもしれません。実際には、テキストはtagname ノードの下の1つまたは複数の子Node として扱われます。したがって、テキスト値を取得するには、tagname の子を走査して、それぞれの子の値を並べてストリングにする必要があります。これにはもっともな理由があります。tagname の中には他の組み込みXML要素が含まれる可能性があり、その場合、このテキスト値を取得することにはあまり意味がありません。しかし現実の世界では、テキスト値を取得することに意味がある80 %のケースで利用できるような便利な機能が存在しないため、コーディング・エラーが頻繁に発生しています。

設計上の問題

DOMの言語非依存性のマイナス面は、それぞれのプログラム言語が通常使用する方法論およびパターンを使用できないことです。たとえば新しいElement を作成するには、開発者はおなじみのJavaのnew 構文を使用する代わりに、ファクトリー・コンストラクター・メソッドを使わなければなりません。Node の集合は通常のList オブジェクトやIterator オブジェクトとしてではなく、NodeList として扱われます。こうした細かな手間がいくつも存在するため、コーディング手法は風変わりになってコードも長くなります。プログラマーたちは直感的に学ぶのではなく、DOM流のやり方を学習しなければなりません。

DOMは「あらゆるものがノード」式の抽象化を採用しています。つまり、DocumentElementAttr といったXML文書のほとんどの部分は、Node インターフェースを継承 (extend) します。このため概念的にエレガントであるだけでなく、DOMのそれぞれの個別の実装は標準インターフェースを介してクラスを設定できるようになっています。中間的なラッパー・クラスの使用によるパフォーマンス低下はありません。

多数のノード・タイプが存在し、それらのタイプにアクセスするメソッドが統一されていないため、「あらゆるものがノード」式の抽象化の価値は多少下がります。たとえば、CharacterData (文字データ) ノードの値を設定するにはinsertData メソッドを使用しますが、Attr (属性) ノードの値はsetValue メソッドを使って設定されます。異なるノードごとに異なるインターフェースを使用している分だけ、このモデルのエレガントさと統一性は低くなり、学習カーブが増大します。


JDOM

JDOMはDOM APIをJava用に調整しようとするものであり、より自然で使いやすいインターフェースとなっています。DOM構成の言語非依存的な性質が扱いにくいことを認識して、JDOMではネイティブのJava表記およびオブジェクトを使うことを意図し、汎用タスク用の便利な機能をいくつか提供しています。

たとえば、JDOMは「あらゆるものがノード」、およびDOM固有の構成 (たとえばNodeList) の問題に直接取り組んでいます。JDOMはさまざまなノード・タイプ (DocumentElementAttribute など) を別個のJavaクラスとして定義しています。つまり、開発者はnew を使ってそれらを作成できるので、型キャストを頻繁に行う必要がありません。JDOMはストリングをJavaString として表し、ノードの集合は通常のList クラスおよびIterator クラスとして表します。(JDOMは独自のクラスをDOMクラスに代わって使用します。)

JDOMはより良いインターフェースを提供しています。すでにJSR (正式なJava仕様要求) として認められ、将来的にはコアJavaプラットフォームに組み込まれるかもしれません。ただし、現時点ではコアJava APIには含まれないので、使用をためらっている人もいます。また、IteratorおよびJavaオブジェクトを頻繁に作成しなければならないため、パフォーマンス上の問題が存在するという報告もあります (参考文献を参照)。

JDOMの普及度と利便性に満足できるのであれば、また、Javaコードやプログラマーを他の言語に移す必要がただちにあるわけではないのであれば、JDOMを使ってみる価値は大いにあるでしょう。この記事を書くためにさまざまな企業のプロジェクトを分析しましたが、それらの企業はまだJDOMに満足できず、単純なDOMを使用していました。この記事でも同じ方針です。


よくあるコーディング上の問題

いくつかの大規模なXMLプロジェクトを分析した結果、DOMの使用に関して共通した問題がいくつか観察されました。以下では、その一部を示します。

コードの肥大化

今回検討したすべてのプロジェクトにおいて、最も重大な問題が明らかになりました。簡単な操作を行うのに多数の行のコードが使われているのです。ある例では、1つの属性の値を検査するのに16行のコードが使われていました。同じタスクを3行のコードで行い、しかも堅固さとエラー処理を改善することが可能です。DOM APIの低水準な性質、メソッドやプログラミング・パターンを不適切に適用したこと、さらにはAPIの知識が完全に身に着いていなかったことが、コードの行数の肥大化につながりました。以下に示すのは、こうした問題を示す具体的なケースを要約したものです。

DOMの走査

我々が調べたコードの中で、最も多かったタスクはDOMの走査、つまり全探索です。リスト1 は、文書のconfig セクションの下にある "header" というノードを探し出すために必要なコードを凝縮したものです。

リスト1では文書がルートから走査され、最上位要素を検索し、その最初の子 (configNode) を取得して、最後にconfigNode の子を個別に調べます。残念ながらこのメソッドは冗長であるだけでなく、脆弱な面が多く見られ、潜在的なバグもあります。

一例をあげると、コードの第2行はgetFirstChild メソッドを使用して中間的なconfig ノードを取得します。ここですでに多数の潜在的な問題が存在します。ルート・ノードの最初の子は、ユーザーが実際に探しているノードではないかもしれません。最初の子を盲目的に追いかけることにより、実際のタグ名を無視しています。文書内の誤った部分を探索することになりかねません。ソースXML文書のルート・ノードの後に空白文字や改行がある場合、このシナリオのようなエラーが頻繁に発生します。この場合、ルート・ノードの最初の子は意図された要素ノードではなく、実際にはNode.TEXT_NODE ノードです。読者がこれを実験するには、サンプル・コードを参考文献からダウンロードし、ファイルsample.xmlを編集してsample タグとconfig タグの間に改行を入れてください。コードはただちに例外を発生して中断するようになります。意図されたノードまで正しくナビゲートするには、root のそれぞれの子ノードを調べて、Text ノードではない、探している名前と一致するノードを見出す必要があります。

さらにリスト1は、文書構造が予期されている構造とは異なる可能性を無視しています。たとえばroot に子ノードがまったく含まれない場合、configNodenull と設定されて、サンプルの第3行はエラーを発生させます。したがって、文書を正しくナビゲートするには、子ノードを1つずつ調べて適切な名前かどうかを検査するだけでなく、それぞれのステップで、各メソッド呼び出しが有効な値を戻したかどうか検査する必要があります。どんな入力データでも扱える、堅固でエラーを発生させないコードを作成するには、詳細を注意深く考慮し、たくさんの行のコードを書く必要があります。

最後に、リスト1のサンプルに含まれるすべての機能は、ただgetElementsByTagName 関数を呼び出すだけで実装できるのです (開発者がそれを知っていればの話ですが)。この点については後で説明します。

要素内のテキスト値を検索する

分析対象となったプロジェクトにおいて、DOM走査の次に数の多かったタスクは、要素内に含まれるテキスト値の検索でした。
<sometag>The Value</sometag>
というXMLの一部を考えてみてください。sometag までナビゲートが済んだ後、テキスト値 (The Value) をどのように取得すればいいのでしょうか。直感的には、次のような実装が考えられるかもしれません。

sometagElement.getData();

ご想像のとおり、上記のコードは意図された処理を実行しません。実際のテキストは1つまたは複数のノードとして保管されているため、sometag 要素に対してgetData または同様の関数を呼び出すことはできません。より良い方法としては、次のようなものがあります。

sometag.getFirstChild().getData();

この2番目の試行の問題点は、最初の子に必ずしも値が含まれるとは限らないことです。処理命令その他の埋め込まれたノードがsometag に含まれる可能性があり、テキスト値がただ1つのノードではなく複数のノードにまたがって含まれる可能性もあります。空白文字がしばしばテキスト・ノードとして表記されることに注意してください。ですから、sometag.getFirstChild() 呼び出しによって、タグとその値の間にある改行が取得されてしまう可能性があります。実際、すべての子を走査し、ノード・タイプがNode.TEXT_NODE かどうかを検査して、それぞれの値を並べて最終的に完全な値を得る必要があります。

なおJDOMの場合には便利なgetText 関数があるので、この問題はすでに解決されています。また、DOM Level 3でもgetTextContent メソッドが予定されており、必要に答えています。ここでの教訓は、可能であれば高水準APIを使うべきであるということです。

getElementsByTagName

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

NodeList names = someElement.getElementsByTagName("name");

によって、someElement ノード内に含まれるnames という名前のノード集合NodeList が戻されます。これは、先に示した走査よりも確かに便利です。同時に、よくあるバグの原因にもなります。

問題は、getElementsByTagName が文書を再帰的に走査して、マッチするすべてのノードを戻すことです。たとえば、顧客情報、企業情報、それに製品情報を含んでいる1つの文書があるとしましょう。この3つのすべての項目には、内部にname (名前) タグが含まれる可能性があります。顧客の名前を検索するためにgetElementsByTagName を呼び出した場合、プログラムは誤った動作をして、顧客名だけでなく製品名、企業名をも取得するでしょう。サブツリーの中でこの関数を呼び出すと危険は低くなりますが、XMLの柔軟な性質ゆえに、処理対象のサブツリーの構造が予期されたとおりのものかどうか、検索対象の名前と一致する無効な子が含まれないかどうかを確かめるのは非常に困難です。


DOMの有効な使用法

DOMの設計上のこうした制限のもとで、この仕様をどのように効果的に利用できるのでしょうか。DOMの使用法に関する基本原則と指針を以下に示すとともに、便利な関数のライブラリーをご紹介します。

基本原則

以下のわずかな基本原則に従うだけで、DOMをかなり効果的に使用できるようになります。

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

これらの原則は、よくある問題を分析した結果、直接得られたものです。すでに述べたように、DOMの走査はエラーの主要な原因となっています。しかし、走査は最も頻繁に必要とされる機能の1つでもあります。DOMを使用せずに、どのように文書を走査できるのでしょうか。

XPath

XPathは文書の一部にアクセスし、検索し、マッチングするための言語です。XPathはW3C勧告であり、ほとんどの言語およびXMLパッケージに実装されています。読者のお手元のDOMパッケージもまた、直接的に、あるいはアドオンでXPathをサポートしているかもしれません。この記事のサンプル・コードでは、XPathサポートのためにXalanパッケージを使用しています。

XPathはファイル・システムやURLと同様のパス表記を使用して、文書の一部を指定およびマッチングします。たとえば/x/y/z というXPathは、文書のルート・ノードx を検索し、その下にノードy、さらにその下にノードz があることを検査します。このステートメントによって、指定されたパス構造に一致するすべてのノードが戻されます。

文書の構造の点でも、またノードの値および属性の点でも、これより複雑なマッチングが可能です。/x/y/* というステートメントは、x を親とするすべてのy ノードの下にある、すべてのノードを戻します。/x/y[@name='a'] は、x を親とするすべてのy ノードのうち、name という属性を持ち、その値がa であるノードをすべてマッチングします。便利なことに、XPathは実際の要素ノードに到達するために空白文字テキスト・ノードをふるいに掛け、要素ノードのみを戻します。

XPathとその使用法について詳細に述べることは、この記事の範囲を超えています。参考文献のリンクから、優れたチュートリアルをご覧ください。少し時間を取ってXPathを学べば、XML文書の処理ははるかに簡単になるでしょう。


関数のライブラリー

さまざまなDOMプロジェクトを分析して驚いたことに、コピー・アンド・ペーストを行うコードがとても大量に存在します。他の分野では優れたプログラミング手法を採用する経験豊かな開発者が、なぜヘルパー・ライブラリーを作成せずにコピー・アンド・ペーストばかり行うのでしょうか。その理由として考えられるのは、DOMが複雑であるため学習カーブも急な曲線を描き、開発者たちは必要な操作を実行してくれる初歩のコードをとりあえず使用するためでしょう。ヘルパー・ライブラリーを形成する適正な関数を書くのに必要な専門知識は、長い時間をかけてやっと身に着くわけです。

その習得時間を少しでも短縮するために、読者が独自のライブラリーを初めて作るうえで役立つ基本的なヘルパー関数をいくつかご紹介しましょう。

findValue

XML文書を扱うとき、最も頻繁に行う処理は、特定のノードの値を検索することです。すでに説明したように、この処理の場合、文書を走査して目的のノードを見付けるうえで、またノードの値を取り出すうえで難しい問題が存在します。しかし走査はXPathを使って単純化することができ、値の取得はコードを一度だけ書いて、後は再利用できます。低レベルの関数を2つ使ってgetValue 関数を実装しました。使用した2つの関数とは、Xalanパッケージに付属のXPathAPI.selectSingleNode (指定されたXPath式に一致する最初のノードを見付けて戻す関数)、およびノード内に含まれるテキストの連結された値を非再帰的に戻すgetTextContents です。なお、JDOMのgetText 関数、あるいはDOM Level 3に含めるよう提案されているgetTextContent メソッドを、getTextContents の代わりに使用することもできます。リスト2 は単純化したコードです。完全な機能はサンプルコードをダウンロードしてください (参考文献をご覧ください)。

findValue を呼び出すとき、検索を始める起点のノードと、検索対象ノードを指定するXPathステートメントとを渡します。この関数は指定されたXPathに一致する最初のノードを見付けて、そのテキスト値を取り出します。

setValue

もう1つ頻繁に行う処理は、ノードの値を希望する値に設定することです (リスト3 を参照)。この関数は (findValue と同様に) 開始ノードとXPathステートメントを入力とするほか、一致するノードの値として設定するストリングを入力とします。この関数は目的のノードを検出し、その子をすべて除去して (つまり、その中にあるすべてのテキストと他の要素を取り除いて)、テキスト・コンテンツを指定されたストリングに設定します。

appendNode

プログラムの中にはXML文書内の値を検索して変更するものもありますが、他方、ノードを追加または除去することによって文書の構造そのものを変更するプログラムもあります。このヘルパー関数は、文書へのノードの追加を単純化します (リスト4 をご覧ください)。

この関数に渡すパラメーターは、XPathの開始点となる上位ノード、追加する新しいノードの名前、および、追加する場所を指定する (つまり新しいノードの親ノードを指定する) XPathステートメントです。新しいノードは、文書内の指定された場所に付加されます。


最終分析

言語に依存しないDOMの設計のおかげで、適用範囲が非常に広がり、多数のシステムやプラットフォームで実装されるようになりました。その代償として、個別の言語用に設計されたAPIと比較して、DOMは難解で直感的に学習できません。

いくつかの単純な原則に従うだけで、DOMをベースとして効果的に利用し、使いやすいシステムを構築することができます。DOMの将来のバージョンには、多数のユーザー・グループの知恵と経験が盛り込まつつあります。そして、今回説明した問題のいくつかは、将来のバージョンで解決されるでしょう。JDOMのようなプロジェクトは、APIをより自然なJavaらしいものに調整しようとしています。さらに、この記事でご紹介した技法を利用すれば、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, Java technology
ArticleID=240112
ArticleTitle=JavaでDOMとXPathを使って効果的にXMLを処理する
publish-date=12012001