レベル: 中級 Elliotte Rusty Harold, Adjunct Professor, Polytechnic University
2006年 07月 25日 更新 2008年 08月 25日
XPath 式は、詳細な DOM (Document Object Model) ナビゲーション・コードよりもずっと簡単に書くことができます。XML 文書から情報を抽出する場合、最も手軽で単純な方法は、XPath 式を Java™ プログラムの中に埋め込んでしまう方法です。Java 5 では、XPath を使って文書をクエリーするための、XML オブジェクト・モデルに依存しないライブラリー、javax.xml.xpath パッケージが導入されています。
2007年 6月 27日 – 読者からのコメントに対応し、著者はコンパイル対象の XPath 式 (リスト 3 に続く 3 つ目の短いコード) を更新しました。
2008年 8月 25日 – 読者からのコメントに対応し、著者は "http://www.example.org/books" を "http://www.example.com/books" に変更しました。該当箇所は、リスト 6、リスト 9 の直前の文章に記載されている名前空間、そしてリスト 9 の 3 箇所です。
もし皆さんが誰かに牛乳を 1 ガロン 買ってきてくれるように頼む場合、頼む相手に対して、どんなことを言うでしょうか。「ちょっと行って、牛乳を 1 ガロン買ってきてください」と言うかもしれません。まさか、「玄関のドアから家を出て、歩道に出たら右に曲がり、3 ブロック歩いてください。そして右に曲がり、半ブロックいたら、右に曲がって店に入ります。売り場番号 4 に行き、その売り場を 5 メートル行って、左に曲がります。1 ガロンの容器に入った牛乳を取り上げます。それをレジに持って行き、代金を払います。そして、来た経路を逆にたどって家に戻ってください」などというばかげた言い方はしないでしょう。普通の大人であれば、「ちょっと行って、牛乳を 1 ガロン買ってきてください」と頼みさえすれば、自分の判断で買ってくる程度の知性は持ち合わせているものです。
クエリー言語とコンピューターでの検索は似ています。「Cryptonomicon のコピーを見つけなさい」と言う方が、ある種のデータベースを検索するために詳細なロジックを書くよりも簡単です。また検索操作のロジックはどれも非常に似ているため、「Neal Stephenson による全著書を見つけなさい」のようなステートメントによる汎用言語を作り出すことができます。そうしておいてから、ある種のデータ・ストアに対してこうしたクエリーを処理するエンジンを書けばよいのです。
XPath
数多くあるクエリー言語のうち、SQL (Structured Query Language) は、ある種のリレーショナル・データベースをクエリーするために設計され、最適化された言語です。SQL ほど有名ではないクエリー言語としては、OQL (Object Query Language) や XQuery などがあります。しかし、この記事の主題は、XML 文書のクエリー用に設計された、XPath です。例えば、ある文書の中にある、Neal Stephenson による全著書のタイトルを見つけるための単純な XPath クエリーは、次のようなものです。
//book[author="Neal Stephenson"]/title |
これと対照的に、同じ情報を DOM のみを使って検索しようとすると、リスト 1 のようになります。
リスト 1. Neal Stephenson による著書のタイトル要素すべてを検索する DOM コード
ArrayList result = new ArrayList();
NodeList books = doc.getElementsByTagName("book");
for (int i = 0; i < books.getLength(); i++) {
Element book = (Element) books.item(i);
NodeList authors = book.getElementsByTagName("author");
boolean stephenson = false;
for (int j = 0; j < authors.getLength(); j++) {
Element author = (Element) authors.item(j);
NodeList children = author.getChildNodes();
StringBuffer sb = new StringBuffer();
for (int k = 0; k < children.getLength(); k++) {
Node child = children.item(k);
// really should to do this recursively
if (child.getNodeType() == Node.TEXT_NODE) {
sb.append(child.getNodeValue());
}
}
if (sb.toString().equals("Neal Stephenson")) {
stephenson = true;
break;
}
}
if (stephenson) {
NodeList titles = book.getElementsByTagName("title");
for (int j = 0; j < titles.getLength(); j++) {
result.add(titles.item(j));
}
}
}
|
信じられないことかも知れませんが、リスト 1 の DOM コードは、単純な XPath 式ほど汎用的でもなく、単純でもありません。皆さんが自分でコードを作成し、デバッグし、維持管理しようとしたら、どちらを選ぶでしょうか。答えは明らかです。
しかし、確かに XPath は表現力豊かですが、Java 言語ではありません。実際のところ、XPath は完全なプログラミング言語ではないのです。XPath では、表現できないことが沢山あり、またクエリーできないことも沢山あります。例えばXPath は、ISBN (International Standard Book Number: 国際標準図書番号) が一致しない本をすべて見つける、といったことができません。また、外部の口座データベースを参照して印税支払い期限が来た著者をすべて見つける、といったこともできません。幸い、XPath は Java プログラムと統合することができ、両方の世界の長所を組み合わせることができます。つまり Java に適したことを Java で行い、XPath に適したことを XPath で行うことができるのです。
最近まで、Java プログラムが XPath クエリーを行うための具体的な API (application program interface) は、XPath エンジンごとに異なっていました。Xalan は独自の API を持っており、Saxon はまた別の API を持っており、そして他のエンジンはまた別の API を持っていました。これはつまり、ある 1 つの製品にコードがロックされてしまう傾向があることを意味します。理想的には、余計な手間はかけず、またコードの書き直しもすることなく、異なるパフォーマンス特性を持った様々なエンジンを試したいものです。
こうした理由から、Java 5 では、エンジンやオブジェクト・モデルに依存しない XPath ライブラリーを提供するために、javax.xml.xpath パッケージが導入されています。また Java 1.3 以降であれば、JAXP (Java API for XML Processing) 1.3 を別途インストールすることで、このパッケージを利用することができます。他の製品としては、Xalan 2.7 と Saxon 8 とが、このライブラリーの実装を含んでいます。
単純な例
では、これが実際にどのように動作するのかを説明するところから始めましょう。その後で、少しばかり詳細に入って行くことにします。まず、Neal Stephenson 著による本を見つけるために、本のリストをクエリーしたい、としましょう。具体的には、リスト 2 に示す形式のリストを考えます。
リスト 2. 本の情報を含む XML 文書
<inventory>
<book year="2000">
<title>Snow Crash</title>
<author>Neal Stephenson</author>
<publisher>Spectra</publisher>
<isbn>0553380958</isbn>
<price>14.95</price>
</book>
<book year="2005">
<title>Burning Tower</title>
<author>Larry Niven</author>
<author>Jerry Pournelle</author>
<publisher>Pocket</publisher>
<isbn>0743416910</isbn>
<price>5.99</price>
</book>
<book year="1995">
<title>Zodiac</title>
<author>Neal Stephenson</author>
<publisher>Spectra</publisher>
<isbn>0553573862</isbn>
<price>7.50</price>
</book>
<!-- more books... -->
</inventory> |
 |
抽象ファクトリー
XPathFactory は抽象ファクトリーです。抽象ファクトリーによる設計パターンでは、この 1 つの API によって、様々なオブジェクト・モデル (DOM や JDOM、 XOM など) をサポートすることができます。別のモデルを選択する場合には、そのオブジェクト・モデルを特定するURI (Uniform Resource Identifier) を XPathFactory.newInstance() メソッドに渡します。例えば、http://xom.nu/ は XOM を選択します。しかし現実には、この API がサポートするオブジェクト・モデルとしては、今のところ DOM のみです。
|
|
対象となるすべての本を発見する XPath クエリーは非常に単純で、 //book[author="Neal Stephenson"] です。これらの本のタイトルを見つけるためには、単純にもう 1 ステップ追加し、//book[author="Neal Stephenson"]/title のようにします。最後に、実際に必要なのは、title 要素の子であるテキスト・ノードなので、もう 1 ステップ追加します。ですから完全な式としては、//book[author="Neal Stephenson"]/title/text() となります。
今度は、この検索を Java 言語から実行し、発見した本すべてのタイトルを出力する単純なプログラムを作成します。まず、この文書を DOM の Document オブジェクトにロードする必要があります。単純にするために、この文書はカレント作業ディレクトリーの books.xml ファイルの中にあるものとします。下記は、この文書を構文解析し、対応した Document オブジェクトを作成するための単純なコード・フラグメントです。
リスト 3. JAXP で文書を構文解析する
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
factory.setNamespaceAware(true); // never forget this!
DocumentBuilder builder = factory.newDocumentBuilder();
Document doc = builder.parse("books.xml"); |
ここまではごく標準的な JAXP と DOM であり、何も新しいものはありません。
次に、XPathFactory を作成します。
XPathFactory factory = XPathFactory.newInstance(); |
次に、このファクトリーを使って XPath オブジェクトを作成します。
XPath xpath = factory.newXPath(); |
この XPath オブジェクトは、XPath 式をコンパイルします。
XPathExpression expr = xpath.compile("//book[author='Neal Stephenson']/title/text()"); |
 |
即時に評価する
XPath 式を 1 度しか使わないのであれば、コンパイル・ステップを省略し、XPath オブジェクトに対して evaluate() メソッドを呼ぶことができます。しかし、同じ式を何度も繰り返して使用する場合には、おそらくコンパイルした方が早いでしょう。
|
|
最後に、結果を得るために XPath 式を評価します。この式は、あるコンテキスト・ノードに対して評価されます。この場合は、コンテキスト・ノードは文書全体です。また、戻り型を指定する必要もあります。ここでは、ノードセットが戻るように要求します。
Object result = expr.evaluate(doc, XPathConstants.NODESET); |
今度はこの結果をDOM の NodeList にキャストし、すべてのタイトルが見つかるまで、このリストに対して繰り返しを行います。
NodeList nodes = (NodeList) result;
for (int i = 0; i < nodes.getLength(); i++) {
System.out.println(nodes.item(i).getNodeValue());
} |
リスト 4は、以上のすべてを 1 つのプログラムにまとめたものです。上記では説明を避けましたが、こうしたメソッドが、いくつかのチェック例外 (throws 文節の中で定義する必要があります) をスローする場合もあることにも注意してください。
リスト 4. 固定の XPath 式を使ってXML 文書をクエリーする完全なプログラム
import java.io.IOException;
import org.w3c.dom.*;
import org.xml.sax.SAXException;
import javax.xml.parsers.*;
import javax.xml.xpath.*;
public class XPathExample {
public static void main(String[] args)
throws ParserConfigurationException, SAXException,
IOException, XPathExpressionException {
DocumentBuilderFactory domFactory = DocumentBuilderFactory.newInstance();
domFactory.setNamespaceAware(true); // never forget this!
DocumentBuilder builder = domFactory.newDocumentBuilder();
Document doc = builder.parse("books.xml");
XPathFactory factory = XPathFactory.newInstance();
XPath xpath = factory.newXPath();
XPathExpression expr
= xpath.compile("//book[author='Neal Stephenson']/title/text()");
Object result = expr.evaluate(doc, XPathConstants.NODESET);
NodeList nodes = (NodeList) result;
for (int i = 0; i < nodes.getLength(); i++) {
System.out.println(nodes.item(i).getNodeValue());
}
}
} |
XPath データ・モデル
XPath と Java のように異なる 2 つの言語を混合して使用する場合には、両者の継ぎ目が何かしら目に付くものです。すべてが完全にフィットするわけではありません。XPath と Java 言語の型システムは異なります。XPath 1.0 には、下記の 4 つの基本的なデータ型しかありません。
- ノードセット (node-set)
- 数値 (number)
- ブール値 (boolean)
- 文字列 (string)
一方 Java 言語では、ユーザー定義のオブジェクト型を含めて、はるかに多くの型があります。
大部分の XPath 式、特にロケーション・パスは、ノードセット (node-set) を返します。しかし、他のものを返す場合もあります。例えば count(//book) という XPath 式は、文書中にある本の数 (number) を返します。また count(//book[@author="Neal Stephenson"]) > 10 という XPath 式は、ブール値 (boolean) を返します。つまり文書中に Neal Stephenson 著の本が 10 冊よりも多くある場合には true を、10 冊以下の場合には false を返します。
evaluate() メソッドは、Object を返すように宣言されます。実際に何を返すかは、XPath 式の結果と、どんな型が要求されているかに依存します。一般的に、次のように言うことができます。
- XPath の number は java.lang.Double にマップされます。
- XPath の string は java.lang.String にマップされます。
- XPath の boolean は java.lang.Boolean にマップされます。
- XPath の node-set は org.w3c.dom.NodeList にマップされます。
 |
XPath 2
ここまでは、XPath 1.0 で作業しているものと想定しています。XPath 2 では、型システムが大幅に拡張され、改訂されています。Java XPath API で XPath 2 をサポートするために必要となる主な変更は、新しい XPath 2 型を返すための定数を追加することです。
|
|
Java で XPath 式を評価する場合、2 番目の引数は、必要な戻り値の型を定義します。戻り値の型として指定できるものには、下記の 5 つがあります。どれも、javax.xml.xpath.XPathConstants クラスの、名前付き定数です。
- XPathConstants.NODESET
- XPathConstants.BOOLEAN
- XPathConstants.NUMBER
- XPathConstants.STRING
- XPathConstants.NODE
最後にあげた XPathConstants.NODE は、実は XPath の型に対応していません。これを使うのは、XPath 式が 1 つのノードしか返さないことが分かっている場合、あるいは、必要なノードは 1 つのみ、という場合のみです。XPath 式が 1 つ以上のノードを返す場合に XPathConstants.NODE を指定すると、evaluate() は文書の順序で最初のノードを返します。もし XPath 式が空のセットを選択する場合に、XPathConstants.NODE を指定すると、evaluate() は null を返します。
もし、要求された変換が行えない場合には、evaluate() は XPathException を投げます。
名前空間コンテキスト
XML 文書の中の要素が名前空間である場合には、その文書をクエリーするための XPath 式は同じ名前空間を使う必要があります。この XPath 式は同じ接頭辞を使う必要はなく、同じ名前空間 URI を使う必要があるだけです。もちろん、XML 文書がデフォルト名前空間を使う場合には、ターゲット文書で接頭辞を使っていなくても、XPath 式は接頭辞を使う必要があります。
しかし Java プログラムは XML 文書ではないため、通常の名前空間解決を適用することはできません。代わりに、名前空間 URI に接頭辞をマップするオブジェクトを提供します。このオブジェクトは、javax.xml.namespace.NamespaceContext インターフェースのインスタンスです。例えば、リスト 5 に示すように、books という文書が http://www.example.com/books という名前空間に置かれたとしましょう。
リスト 5. デフォルト名前空間を使った XML 文書
<inventory xmlns="http://www.example.com/books">
<book year="2000">
<title>Snow Crash</title>
<author>Neal Stephenson</author>
<publisher>Spectra</publisher>
<isbn>0553380958</isbn>
<price>14.95</price>
</book>
<!-- more books... -->
</inventory> |
Neal Stephenson による全著書のタイトルを見つけるための XPath 式は、//pre:book[pre:author="Neal Stephenson"]/pre:title/text() のようなものになります。しかし、pre という接頭辞を、http://www.example.com/books という URI にマップする必要があります。NamespaceContext インターフェースが JDK (Java software development kit) や JAXP にデフォルトで実装されていないのは多少ばかげた話ですが、実際にないのです。しかし、自分で実装しても、それほど難しいものではありません。リスト 6 は、この 1 つの名前空間のみのための単純な実装を示しています。また、xml 接頭辞もマップする必要があります。
リスト 6. ある 1 つの名前空間とデフォルトをバインドするための単純なコンテキスト
import java.util.Iterator;
import javax.xml.*;
import javax.xml.namespace.NamespaceContext;
public class PersonalNamespaceContext implements NamespaceContext {
public String getNamespaceURI(String prefix) {
if (prefix == null) throw new NullPointerException("Null prefix");
else if ("pre".equals(prefix)) return "http://www.example.com/books";
else if ("xml".equals(prefix)) return XMLConstants.XML_NS_URI;
return XMLConstants.NULL_NS_URI;
}
// This method isn't necessary for XPath processing.
public String getPrefix(String uri) {
throw new UnsupportedOperationException();
}
// This method isn't necessary for XPath processing either.
public Iterator getPrefixes(String uri) {
throw new UnsupportedOperationException();
}
} |
マップを使ってバインディングを保存すること、また、再利用性の高い名前空間コンテキストが可能なセッター・メソッドを追加することは、それほど難しくはありません。
NamespaceContext オブジェクトを作成したら、式をコンパイルする前に、このオブジェクトを XPath オブジェクトにインストールします。そこから後は、これまでと同じように、こうした接頭辞を使ってクエリーを行うことができます。一例を下記に示します。
リスト 7. 名前空間を使う XPath クエリー
XPathFactory factory = XPathFactory.newInstance();
XPath xpath = factory.newXPath();
xpath.setNamespaceContext(new PersonalNamespaceContext());
XPathExpression expr
= xpath.compile("//pre:book[pre:author='Neal Stephenson']/pre:title/text()");
Object result = expr.evaluate(doc, XPathConstants.NODESET);
NodeList nodes = (NodeList) result;
for (int i = 0; i < nodes.getLength(); i++) {
System.out.println(nodes.item(i).getNodeValue());
} |
関数リゾルバー
場合によると、XPath 式の中で使用するための拡張関数を Java 言語で定義すると便利な場合があります。こうした関数によって、純然たる XPath では困難な、あるいは不可能なタスクを行うのです。しかし、こうした関数は真の関数である必要があり、単なる適当なメソッドであってはなりません。つまり、こうした関数は何か副作用を持つものであってはならないのです。(XPath 関数は、任意の順序で、また任意の回数、評価することができます。)
Java XPath API を通してアクセスされる拡張関数は、javax.xml.xpath.XPathFunction インターフェースを実装する必要があります。このインターフェースは、evaluate という 1 つのメソッドを宣言します。
public Object evaluate(List args) throws XPathFunctionException |
このメソッドは、Java 言語が XPath に変換できる下記の 5 つの型のうち、どれかを返します。
- String
- Double
- Boolean
- Nodelist
- Node
例えばリスト 8 は、ISBN のチェックサムを調べて Boolean を返す拡張関数を示しています。このチェックサムの基本的ルールとしては、最初の 9 桁の各桁に、その桁の位置を掛けます (つまり、最初の桁には 1 を掛け、2 番目の桁には 2 を掛け、など) 。これらの値は加算され、それを 11 で割った余りが取られます。もし余りが 10 だとすると、最後の桁は X です。
リスト 8. ISBN をチェックするための XPath 拡張関数
import java.util.List;
import javax.xml.xpath.*;
import org.w3c.dom.*;
public class ISBNValidator implements XPathFunction {
// This class could easily be implemented as a Singleton.
public Object evaluate(List args) throws XPathFunctionException {
if (args.size() != 1) {
throw new XPathFunctionException("Wrong number of arguments to valid-isbn()");
}
String isbn;
Object o = args.get(0);
// perform conversions
if (o instanceof String) isbn = (String) args.get(0);
else if (o instanceof Boolean) isbn = o.toString();
else if (o instanceof Double) isbn = o.toString();
else if (o instanceof NodeList) {
NodeList list = (NodeList) o;
Node node = list.item(0);
// getTextContent is available in Java 5 and DOM 3.
// In Java 1.4 and DOM 2, you'd need to recursively
// accumulate the content.
isbn= node.getTextContent();
}
else {
throw new XPathFunctionException("Could not convert argument type");
}
char[] data = isbn.toCharArray();
if (data.length != 10) return Boolean.FALSE;
int checksum = 0;
for (int i = 0; i < 9; i++) {
checksum += (i+1) * (data[i]-'0');
}
int checkdigit = checksum % 11;
if (checkdigit + '0' == data[9] || (data[9] == 'X' && checkdigit == 10)) {
return Boolean.TRUE;
}
return Boolean.FALSE;
}
} |
次のステップは、この拡張関数を Java プログラムから利用できるようにすることです。そのためには、式をコンパイルする前に、XPath オブジェクトに javax.xml.xpath.XPathFunctionResolver をインストールします。関数リゾルバーは、XPath の名前とこの関数の名前空間 URI を、この関数を実装するJava クラスにマップします。リスト 9 は、http://www.example.com/books という名前空間を持つ valid-isbn という拡張関数を、リスト 8 のクラスにマップする単純な関数リゾルバーです。例えば //book[not(pre:valid-isbn(isbn))] という XPath 式は、ISBN チェックサムが一致しないすべての本を見つけます。
リスト 9. valid-isbn 拡張関数を認識する関数コンテキスト
import javax.xml.namespace.QName;
import javax.xml.xpath.*;
public class ISBNFunctionContext implements XPathFunctionResolver {
private static final QName name
= new QName("http://www.example.com/books", "valid-isbn");
public XPathFunction resolveFunction(QName name, int arity) {
if (name.equals(ISBNFunctionContext.name) && arity == 1) {
return new ISBNValidator();
}
return null;
}
} |
拡張関数は名前空間内に存在している必要があるため、拡張関数を含む式を評価する場合には、たとえクエリー対象の文書が名前空間を使っていなくても NamespaceResolver を使う必要があります。XPathFunctionResolver と XPathFunction、そして NamespaceResolver はインターフェースなので、(その方が便利であれば) これらをすべて同じクラスの中に置くこともできます。
まとめ
クエリーを書く場合には、Java や C のような命令型言語 (imperative language) を使うよりも、SQL や XPath といった宣言型の言語を使った方が、はるかに簡単です。また逆に、複雑なロジックを書く場合には、SQL や XPath といった宣言型の言語を使うよりも、Java や C のようなチューリング完全言語 (Turing complete language) を使った方が、はるかに簡単です。幸いなことに、JDBC (Java Database Connectivity) や javax.xml.xpath といった API を使えば、この 2 種類の言語を混合して使うことができます。世界のデータが次第に XML に移行するにつれ、既に java.sql が重要であるのと同じように、 javax.xml.xpath も重要なものとなるでしょう。
参考文献 学ぶために
製品や技術を入手するために
- java.net のJAXP Project から、Java 1.3 と 1.4 用の JAXP 1.3 をダウンロードしてください。
- この記事で取り上げた XPath API をサポートする、Apache Projectの XSLT エンジン、Xalan 2 を調べてみてください。
- Michael Kayによる XSLT エンジン、SAXON 8 も、この記事で取り上げた XPath API をサポートしています。試してみてください。
- 皆さんの次期開発プロジェクトを、IBM trial software を使って構築してください。developerWorks から直接ダウンロードすることができます。
議論するために
著者について  | 
|  | Elliotte Rusty Haroldはニューオーリンズ出身であり、時たま、おいしいgumbo(オクラ入りのスープ)を食べに帰っています。ただし現在はニューヨークのブルックリン近郊のProspect Heightsに、妻のBethと猫のCharm(charmed quarkからとりました)とMarjorie(義理の母の名前からとりました)と一緒に住んでいます。彼はPolytechnic Universityのコンピューター・サイエンスの非常勤教授として、Java技術とオブジェクト指向プログラミングを教えています。彼のCafe au Lait Webサイトは、インターネット上で最も人気のある独立系Javaサイトの一つです。また、そこから派生したCafe con Lecheは、最も人気のあるXMLサイトの一つです。彼の最近の著作には『Java I/O, 2nd edition』があります。現在はXML処理用のXOM APIやJaxen XPathエンジン、Jesterテスト・カバレッジ・ツールなどに取り組んでいます。 |
記事の評価
|