XSLT により XML ファイルを HTML として表示する際のノウハウ的なもの

Web サービスや Web アプリケーションなどでは、データを XML でやりとりすることも多いと思います。 XML ファイルをブラウザで表示するようなこともあるかもしれません。 そういうとき、XML ファイルのテキストデータがそのまま表示されるよりも、HTML 形式で表示された方が見やすいわけで、そのために使用するのが XSLT というものです。

XSLT とは、XSL (Extensible Stylesheet Language) による変換 (Transform) のことです。 XSL は XML に適用するスタイルシートの一種で、XSLT により XML 構造を別の XML 構造に変換したり、HTML や単なるテキストデータに変換する事ができます。

XSLT によって XML を HTML に変換するための方法については各種サイトにて解説がありますので、ここでは注意点を書いておきます。

名前空間の扱い

XSL ファイルの中では、XSL 適用先 XML の各要素を XPath により選択します。
XSL 適用先の XML名前空間 URI が使われていないならば名前空間について特に気にする必要はありませんが、XSL 適用先 XML名前空間 URI が使われているならば名前空間にも気をくばる必要があります。

どのように名前空間を指定するかという方法は簡単で、XSL 側で名前空間接頭辞 (prefix) を宣言し、XPath でもその prefix を使えば良いです。

例えば、以下のようになります。

<!-- 適用先 XML -->
<?xml-stylesheet type="text/xsl" href="style.xsl"?>
<test-data xmlns="http://www.vividcode.info/test">
<!-- test-data 要素の名前空間 URI は Dttp://www.vividcode.info/test -->
</test-data>

<!-- XSL ファイル -->
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:p="http://www.vividcode.info/test">
<!-- prefix p を名前空間 URI http://www.vividcode.info/test と結びつける -->

...

<!-- 上で宣言したので XPath の中で prefix p を使用できる -->
<xsl:apply-templates select="/p:test-data">

...

DOCTYPE 宣言

XSL のルート要素において、出力後の HTML の DOCTYPE のシステム識別子と公開識別子を指定することができます。 これらを指定しないと IE (ver. 6 から 8 で確認) においてレンダリングモードが過去互換モードになってしまいますので、指定してやる必要があります。

以下、例 *1

<!-- XSL ファイル -->
...
<xsl:output method="html" media-type="text/html" encoding="utf-8"
doctype-public="//W3C//DTD HTML 4.0 Transitional//EN"
doctype-system="http://www.w3.org/TR/REC-html40/loose.dtd" />
...

IE のための条件付コメントを出力する方法

IE は、ある特殊な形式で書かれたコメントを条件付コメントとして認識します。 例えば、以下のような形式のコメントです。

<!-- HTML ファイル -->
...
<!--[if IE 6.0]>IE 6 はここに書かれている内容をコメントとして扱わず、
通常通り書かれているとみなす<![endif]-->

このような条件付コメントを XSL でも出力することができます。 方法としては単純で、単にコメント要素を XSL で生成し、そのコメント要素の中身を条件付コメントの形式で書くだけです。 注意点としては、コメント内に "<" という文字を書く必要があるため、それを "&lt;" として書くか、全体を CDATA セクションにしなければならない、ということです。 以下の例では全体を CDATA セクションにしています 。

<!-- XSL ファイル ((コードにミスがあったため修正しました。 (2010-02-28 18:08 JST) )) -->
...
<xsl:comment><![CDATA[[if IE 6]>IE 6 はここに書かれている内容をコメントとして扱わず、
通常通り書かれているとみなす<![endif]]]></xsl:comment>
...

サンプル

最後に XSLT によって XML ファイルを HTML に変換するサンプルを置いておきます。

XSLT 自体は IE 5.5 でも使えるものですので互換性などは気にせず使って良い技術だと思います。

*1: HTML で出力するなら XHTMLDTD よりも HTML 4.01 の DTD を書いたほうがいいよね、ということで修正しました。 (2010-03-04 04:00 JST)

DOM 3 XPath において名前空間に属する Node を指定する方法

Document Object Model (DOM) Level 3 には XPath に関する仕様も含まれています。

最近のブラウザである Firefox 3.5 や Safari 4、Opera 10 などの JavaScript 実装は DOM Level 3 に対応しており、XPath 機能も使う事ができます。 これらのブラウザであれば、XPath 機能を使った JavaScript を含む HTML を解釈してくれるため、要素の選択などを XPath を使って簡単に行う事ができます。

// XPath 式の実行
// 第 1 引数が XPath 式, 第 2 引数が context node, 第 3 引数は XPathResolver オブジェクト (HTML であれば null でよい),
// 第 4 引数は結果の型, 第 5 引数は (必要であれば) 結果を格納するオブジェクト (普通は null でよい)
var xpath_result = document.evaluate( "string(//title)", document, null, XPathResult.STRING_TYPE, null );
// 結果の XPathResult オブジェクトから実際の値を取得
var str = xpath_result.stringValue;

上記コードを実行すると変数 str には、文書中の title 要素 (複数ある場合は最初に現れるもの) の文字列値が格納されます。 実際に動くサンプルは下記ページにあります。

XHTML 中で XPath を使うためには名前空間 URI の解決を行わなければならない

上で述べたように HTML で XPath を使う事ができるわけですが、同じようにして XHTML (MIME タイプが "application/xhtml+xml" のもの) において XPath を使おうとすると、問題が起きます。 HTML の要素は名前空間に属していない *1 のに対して、XHTML の要素は名前空間 URI "http://www.w3.org/1999/xhtml" に属しているという違いがあるためです。

XPath の仕様には、以下のように書いています。

A QName in the node test is expanded into an expanded-name using the namespace declarations from the expression context. This is the same way expansion is done for element type names in start and end-tags except that the default namespace declared with xmlns is not used: if the QName does not have a prefix, then the namespace URI is null (this is the same way attribute names are expanded). It is an error if the QName has a prefix for which there is no namespace declaration in the expression context.
(XML Path Language - 2.3 節 より引用)

つまり、XPath 式中の node test において、名前空間に属する node を指定したい場合は、prefix を付けなければならない、ということです。 また、XPath 式中の prefix と namespace URI を結びつける具体的な方法については特に書かれていません。

XPathNSResolver を使用して名前空間の解決を行う

さて、XPath の仕様には prefix と namespace URI を結びつける具体的な方法が書かれていませんが、DOM 3 XPath 仕様にその方法が書かれています。

インターフェイス XPathNSResolver を実装したオブジェクトを使い、名前空間の解決を行います。 XPathNSResolverの生成には、XPathEvaluator#createNSResolver メソッド を使用します。 XPath 式中で利用する prefix を宣言している Node を引数に渡して XPathEvaluator#createNSResolver メソッド を呼び出すと、期待している XPathNSResolver が得られます。

一般的には XPathEvaluator インターフェイスは Document インターフェイスを実装しているオブジェクトに実装されています。 JavaScript であれば window.document オブジェクトという事になります。

// まずは名前空間の宣言を行う要素を生成 (要素名などは適当でよい)
var nsr_elem = document.createElementNS(null, "ns-resolver");
// 次に, その要素に名前空間の宣言のための属性を付与
// (今は prefix "h" を namespace URI "http://www.w3.org/1999/xhtml" に結びつける)
// (必要に応じて複数の名前空間の宣言をしてよい)
nsr_elem.setAttributeNS("http://www.w3.org/2000/xmlns/", "xmlns:h", "http://www.w3.org/1999/xhtml");
// XPathNSResolver の生成
var nsr = document.createNSResolver( nsr_elem );

この XPathNSResolver があれば、XPath 式中に、namespace URI "http://www.w3.org/1999/xhtml" に結び付けられた prefix "h" を使う事ができるようになります。

// 第 3 引数に, 先ほど生成した XPathNSResolver オブジェクトを指定して XPath 式の実行
var xpath_result = document.evaluate( "string(//h:title)", document, nsr, XPathResult.STRING_TYPE, null );
// 結果の XPathResult オブジェクトから実際の値を取得
var str = xpath_result.stringValue;

実際に動くサンプルは下記ページにあります。

HTML 5 と XPath の関係

まだ草案の段階ですが、HTML 5 においては XPath を使う時の事情がちょっと変わりそうなのでメモ程度に書いておきます。

HTML 5 の草案において、XPath との相互作用について以下の節に書いてあります。

ここに書いてある通り、XPath 式中の QName が prefix を持っていなかった場合、名前空間が null として扱われるのではなく、条件によって変化するようです。

  1. If the context node is from an HTML DOM, the default element namespace is "http://www.w3.org/1999/xhtml".
  2. Otherwise, the default element namespace URI is null.

HTML 5 においては HTML 要素は全て名前空間 "http://www.w3.org/1999/xhtml" に属するわけですが、わざわざ XPath 式中に prefix を書かずとも良い ―― つまり、今までどおり使う事ができる、ということですね。

*1:HTML 5 の草案では、HTML 要素も名前空間 URI "http://www.w3.org/1999/xhtml" に属することとなっているが、HTML 4.01 以前では HTML 要素は名前空間に属していない

W3C DOM 3 XPath の勧告に従った方法で XPath を評価する

Java を使用し、DOM 3 Load and Save Specification に従った方法で XML 文書を読み込み DOM を構築する方法は 昨日の記事 で書きました。

同じように DOM の勧告として DOM 3 XPath Specification があります。 この勧告は DOM ツリーを XPath を使って探索する際の API について記述しています。 今日はこの勧告に従った方法で XPath を評価するサンプルを書いておきます。

サンプル

Java SE 6 で実行し、動作することを確認しました。

package info.vividcode;

import org.w3c.dom.DOMImplementation;
import org.w3c.dom.Document;
import org.w3c.dom.bootstrap.DOMImplementationRegistry;
import org.w3c.dom.ls.DOMImplementationLS;
import org.w3c.dom.ls.LSInput;
import org.w3c.dom.ls.LSParser;
import org.w3c.dom.xpath.XPathEvaluator;
import org.w3c.dom.xpath.XPathExpression;
import org.w3c.dom.xpath.XPathResult;

public class DOMXPathTest {
public static void main( String[] args ) {
try {

// ===== Load and Save に関する実装取得 =====
// DOMImplementationRegistry オブジェクトを取得 (ここから DOM 実装を取得できる)
DOMImplementationRegistry dir = DOMImplementationRegistry.newInstance();
// Load and Save 機能および XPath 機能を実装しているか,
// それらの機能と協調できる DOM 実装を取得
DOMImplementation imp = dir.getDOMImplementation("+LS 3.0 +XPath 3.0");
// 本来はここで imp が null でないかどうかチェックすべき
// (該当する DOM 実装が無ければ null になる)

// ===== XML 文書のパース =====
// Load and Save 機能を持つオブジェクトを取得
DOMImplementationLS impLS = (DOMImplementationLS) imp.getFeature("+LS", "3.0");
// LSInput クラスのインスタンスを取得
LSInput input = impLS.createLSInput();
// 読み込み元の XML として文字列を設定
input.setStringData(
"<test>1 つ目のテキストノード<t2>2 つ目のテキストノード</t2></test>" );
// LSParser クラスのインスタンス取得
LSParser parser = impLS.createLSParser(
DOMImplementationLS.MODE_SYNCHRONOUS, null );
// XML をパースして DOM Document オブジェクトを取得
Document doc = parser.parse( input );

// ===== XPath 式を実行 =====
// XPath 機能を持つオブジェクトを取得
XPathEvaluator evaluator = (XPathEvaluator) imp.getFeature("+XPath", "3.0");
// XPath 式を表すオブジェクトを生成
XPathExpression exp =
evaluator.createExpression( "string(//text())", null );
// XPath 式を実行し, 結果を取得
// 結果は第 2 引数で指定した型に自動的に変換される
// 変換しない場合は XPathResult.ANY_TYPE を指定すればよい
XPathResult res =
(XPathResult) exp.evaluate( doc, XPathResult.STRING_TYPE, null );
// 結果の文字列値を取得し, 表示
System.out.println( res.getStringValue() );
// => "1 つ目のテキストノード" と表示されるはず

} catch( Exception e ) {
e.printStackTrace();
}
}
}

解説

DOM 実装の取得

// Load and Save 機能および XPath 機能を実装しているか, 
// それらの機能と協調できる DOM 実装を取得
DOMImplementation imp = dir.getDOMImplementation("+LS 3.0 +XPath 3.0");

まずここで、"LS" (Load and Save) および "XPath" の機能を実装しているか、またはそれらの機能と協調できる DOM 実装を取得しています。 "LS" および "XPath" の前に "+" が付いていますが、これは 「DOM 実装自身が "LS" や "XPath" の機能を実装していなくても、協調できれば良い」 ということを表しています。 "+" を付けずに

DOMImplementation imp = dir.getDOMImplementation("LS 3.0 XPath 3.0");
とした場合は、DOM 実装自身が "LS" と "XPath" の機能を実装していなければなりませんので、敷居が高くなります。 私の環境の場合、"+" を付けなければ該当する DOM 実装はありませんでした。 基本的には機能名の前には "+" を付けると良いでしょう。

なお、機能名の後ろに "3.0" とありますが、これはその機能の版を表しています。 現在のところ "LS" と "XPath" の版は "3.0" のみです。

XML 文書の読み込み

XML 文書の読み込みに関しては 昨日の記事 を参照してください。 また、W3C DOM 3 Load and Save Specification も参照してください。

XPath 機能を持つオブジェクトを取得

// XPath 機能を持つオブジェクトを取得
XPathEvaluator evaluator = (XPathEvaluator) imp.getFeature("+XPath", "3.0");

ここで、DOM 実装から XPath 機能を持つオブジェクト (XPathEvaluator インターフェイスを実装しているオブジェクト) を取得しています。 getFeature メソッドでは、第 1 引数に機能名、第 2 引数にその版を指定します。 本来は第 1 引数の機能名の前に "+" を付ける必要はないはずなのですが、何故か私の環境では "+" を付けなければ null が返されました。 おそらくバグだと思われます。

XPath 式を表すオブジェクトを取得

// XPath 式を表すオブジェクトを生成
XPathExpression exp = evaluator.createExpression( "string(//text())", null );

次に createExpression メソッドを使って XPath 式を表すオブジェクト (XPathExpression インターフェイスを実装しているオブジェクト) を取得します。 第 1 引数が XPath 式 (文字列) で、第 2 引数は名前空間を解決するためのオブジェクトです。 名前空間を考えなくて良い場合は null で構いません。

XPath 式の実行

// XPath 式を実行し, 結果を取得
XPathResult res = (XPathResult) exp.evaluate( doc, XPathResult.STRING_TYPE, null );

ここで実際に XPath 式を実行します。 第 1 引数が XPath 式を評価するときの context node です。 XPath 式の中で相対パスを使っている場合は context node は重要になりますが、絶対パスを使っている場合は Document ノードで良いでしょう。 第 2 引数は、XPath 式の実行結果をどの型で評価するかを指定します。 ここで型を指定しておけば、自動的にその型に変換されます。 (今回の場合は、XPath 式の実行結果は String 型で、第 2 引数の指定も String 型なので変換されない。) 自動的な型変換をして欲しくない場合は XPathResult.ANY_TYPE を指定します。 第 3 引数は null で良いでしょう。

第 2 引数として指定できる定数にどのようなものがあるかは Interface XPathResult のドキュメントページ をご覧ください。

実行結果の取得

// 結果の文字列値を取得し, 表示
System.out.println( res.getStringValue() );

実行結果は (XPathResult インターフェイスを実装している) res に格納されています。 ここから値を取り出すためには各種 getter を使用します。 今回の場合は結果として文字列が得られていますので、getStringValue メソッドを使用してその文字列を取得できます。

その他にどのようなメソッドがあるかは Interface XPathResult のドキュメントページ をご覧ください。

Java で XML 文書を読み込んで DOM ツリーを構築するサンプル

XML 文書をツリー形式で扱うためのモデルとして DOM があり、W3C がその仕様を公開しています。 例えば DOM 3 Core Specification などです。

しかし、XML 文書を読み込んで DOM ツリーを構築したり、逆に DOM ツリーから XML 文書を出力したりするという部分については実装依存だと (今まで) 思っていました。 が、実はその部分 (内部処理ではなく API に関して) もちゃんと仕様がありました。

そしてちゃんと Java でも実装されているというからびっくりしました。 「Java DOM XML」 などのキーワードで検索して XML 文書をパースして DOM 木を取得する方法を調べると、検索の上位は javax.xml.parsers.DocumentBuilder クラス を使用する方法で占められているので今まで全く気づきませんでした。

せっかくなので javax.xml.parsers.DocumentBuilder クラス を使用して XML 文書をパースし javax.xml.transform.Transformer クラス を使用してシリアライズするサンプルと、W3C Load and Save の API を使ってパースおよびシリアライズするサンプルを置いておきます。

DocumentBuilder クラスおよび Transformer クラスを使用

XML 文書のパースに javax.xml.parsers.DocumentBuilder クラス を使用し、DOM ツリーを文字列に変換して出力するために javax.xml.transform.Transformer クラス を使用する方法です。

package info.vividcode;

import java.io.File;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;

import org.w3c.dom.Document;

public class DOMTest1 {
public static void main( String[] args ) {
try {

// ===== XML 文書のパース =====
// DocumentBuilder クラスのインスタンスを取得
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
DocumentBuilder builder = dbf.newDocumentBuilder();
// パース
Document doc = builder.parse( new File("file.xml") );

// ===== DOM ツリーのシリアライズ =====
// Transformer クラスのインスタンスを取得
TransformerFactory tff = TransformerFactory.newInstance();
Transformer transformer = tff.newTransformer();
// シリアライズする DOM オブジェクト
DOMSource src = new DOMSource();
src.setNode( doc );
// シリアライズした結果を出力する先
StreamResult output = new StreamResult();
output.setOutputStream( System.out );
// シリアライズ
transformer.transform( src, output );

} catch ( Exception e ) {
e.printStackTrace();
}
}
}

DOM Load and Save の API を使用する方法

DOM Load and Save で規定されているインターフェイスを使用して XML 文書のパース、および DOM ツリーのシリアライズを行うサンプルです。

package info.vividcode;

import java.io.FileInputStream;

import org.w3c.dom.bootstrap.DOMImplementationRegistry;
import org.w3c.dom.ls.DOMImplementationLS;
import org.w3c.dom.ls.LSInput;
import org.w3c.dom.ls.LSOutput;
import org.w3c.dom.ls.LSParser;
import org.w3c.dom.ls.LSSerializer;
import org.w3c.dom.DOMImplementation;
import org.w3c.dom.Document;

public class DOMTest2 {
public static void main( String[] args ) {
try {

// ===== Load and Save に関する実装取得 =====
// DOMImplementationLS のインスタンスを取得
DOMImplementationRegistry dir = DOMImplementationRegistry.newInstance();
DOMImplementation imp = dir.getDOMImplementation("+LS 3.0");
// 本来はここで imp が null でないかチェックすべき (該当する DOM 実装が無ければ imp は null)
DOMImplementationLS impLS = (DOMImplementationLS) imp.getFeature("+LS", "3.0");

// ===== XML 文書のパース =====
// LSInput クラスのインスタンスを取得し, 読み込み元設定
LSInput input = impLS.createLSInput();
input.setByteStream( new FileInputStream("file.xml") );
// LSParser クラスのインスタンス取得
LSParser parser = impLS.createLSParser(
DOMImplementationLS.MODE_SYNCHRONOUS, null );
// パース
Document doc = parser.parse(input);

// ===== DOM ツリーのシリアライズ =====
// LSOutput クラスのインスタンスを取得し, 出力先設定
LSOutput output = impLS.createLSOutput();
output.setByteStream( System.out );
output.setEncoding( "UTF-8" );
// LSSerializer クラスのインスタンス取得
LSSerializer serializer = impLS.createLSSerializer();
// シリアライズ
serializer.write( doc, output );

} catch( Exception e ) {
e.printStackTrace();
}
}
}

変更

      DOMImplementationLS impLS = 
(DOMImplementationLS) dir.getDOMImplementation("LS 3.0");
となっていたところを
      DOMImplementation   imp   = dir.getDOMImplementation("Core 3.0");
DOMImplementationLS impLS = (DOMImplementationLS) imp.getFeature("LS", "3.0");
と変更しました。 こちらの方が W3C DOM に従った形になっているはずです。 [2009.10.12 朝]

さらに

      DOMImplementation   imp   = dir.getDOMImplementation("+LS 3.0");
// 本来はここで imp が null でないかチェックすべき (該当する DOM 実装が無ければ imp は null)
DOMImplementationLS impLS = (DOMImplementationLS) imp.getFeature("+LS", "3.0");
と変更しました。 dir.getDOMImplementation("+LS 3.0") で、Load and Save 機能を実装している、または協調可能な DOM 実装を取得し、imp.getFeature("+LS", "3.0") で Load and Save 機能を実装しているオブジェクトを取得しています。 [2009.10.12 夜]

XHTML における空要素の扱い

HTML 4.01 では内容モデルが EMPTY の要素 (hr 要素や meta 要素など) は以下のように表現していました。

<hr>
XML で言うところの閉じタグを省略した形であり、省略 「できる」 のではなく省略 「しなければならない」 のでした。

では、XHTML ではどのようにすれば良いのでしょうか。 XHTML における空要素、および内容モデルが EMPTY である要素についてメモ代わりに書いておきます。

HTML との互換性を保ったまま XHTML を記述するための指針が XHTML 1.0 の仕様の付録 C にあります。

たとえば <br /> や <hr />, <img src="karen.jpg" alt="Karen" /> といったように、空要素の末尾の / と > との前にスペースを1個組み込むこと。また、たとえば <br /> といったように、空要素には最小化タグ文法を使うこと。これは、XMLで許容されている代わりの文法 <br></br> は、多くの既存のユーザエージェントでは与えられる結果が一定しないからである。
内容モデルが EMPTY ではない要素に空インスタンスが与えられる場合 (たとえば空のタイトルや段落) には、最小化形式は使わないこと (たとえば <p /> は使わず <p></p> を使う)。
つまり、内容モデルが EMPTY の要素は、EmptyElemTag の形式で記述し、それ以外の場合は例え子ノードが何も無くても STagETag を書く、ということです。

XHTML 1.0 Transitional について、内容モデルが EMPTY の要素を以下に挙げておきます。

  • base
  • meta
  • link
  • hr
  • br
  • basefont
  • param
  • img
  • area
  • input
  • isindex
  • col

以上です。

Java で XPath を使う方法

注意 [2009.10.13]

この記事で書かれている方法は、W3C DOM 3 XPath Specification とは異なる独自実装を用いています。
W3C DOM 3 XPath の勧告に従った方法で XPath を評価する という記事を書きましたので、そちらに記した方法で XPath 式を実行することをおすすめします。

本文

JavaXPath を使う方法に関しては javax.xml.xpath.XPath vs org.apache.xpath.XPathAPI という文書を読むとよくわかる。 が、各クラスの名前空間が明示されていなくてちょっと悩むかも。 というわけで import 文で名前空間を明示したサンプルコードを書いた。

XML名前空間に関する部分は含んでいない。 なお、読み込んだ XML ファイルは さきほど書いた記事ソースコード中にあるものと同等である。

package info.vividcode;

import java.io.File;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathFactory;

import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.w3c.dom.traversal.NodeIterator;

import com.sun.org.apache.xpath.internal.XPathAPI;

public class Test {
    
    public static void main( String[] args ) {
        try {
            // ===== XML 文書の読み込み =====
            DocumentBuilder builder = DocumentBuilderFactory.newInstance()
                                                        .newDocumentBuilder();
            Document doc = builder.parse( new File("file.xml") );
            // ===== javax.xml.xpath.XPath を使う方法 =====
            // 準備
            XPathFactory factory = XPathFactory.newInstance();
            XPath xpath = factory.newXPath();
            // 単一ノード取得
            String location = "/test/t1/text/text()";
            System.out.println( xpath.evaluate(location, doc) );
            // 複数ノード取得
            location = "//t1/t2[2]/text()";
            NodeList entries = (NodeList) xpath.evaluate(
                                    location, doc, XPathConstants.NODESET );
            for( int i = 0; i &lt; entries.getLength(); i++ ) {
                System.out.println( entries.item(i).getNodeValue() );
            }
            // ===== org.apache.xpath.XPathAPI を使う方法 =====
            // 単一ノード取得
            location = "/test/t1/text/text()";
            System.out.println( XPathAPI.eval(doc, location) );
            // 複数ノード取得
            //location = "//t1/t2[2]/text()";
            location = "/descendant::t2[2]/text()";
            NodeIterator it = XPathAPI.selectNodeIterator(doc, location);
            Node node;
            while( (node = it.nextNode()) != null ) {
                System.out.println( node.getNodeValue() );
            }
        } catch ( Exception e ) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }
    
}

REXML (Ruby 標準添付ライブラリ) のバグ

REXMLXPath の実装状況を調べようとしてちょっと XPath を使ってみたのですが、どうも仕様どおりの動きをしないことがあったのでメモしておきます。

#! /usr/bin/ruby1.9
# -*- coding: utf-8 -*-

begin

STDOUT.set_encoding( "UTF-8", "UTF-8" )

# REXML 使用
require "rexml/document"

# 以下の XML 文書を DOM にする
xml_string = <<EOF
<?xml version='1.0' encoding='UTF-8' ?>
<test>
<t1><text>test1</text>
<t2>test1-1</t2>
<t2>test1-2</t2>
<t2>test1-3</t2>
</t1>
<t1><text>test2</text>
<t2>test2-1<t2>test2-1-1</t2></t2>
<t2>test2-2</t2>
<t2>test2-3</t2>
</t1>
<t1><text>test3</text>
<t2>test3-1</t2>
<t2>test3-2</t2>
<t2>test3-3</t2>
</t1>
</test>
EOF

doc = REXML::Document.new( xml_string )
p REXML::XPath.match( doc, "//t1/t2[2]/text()" )
#=> ["test1-2"] ... NG: 本当は ["test1-2", "test2-2", "test3-2"] となるはず
p REXML::XPath.match( doc, "//t2[2]/text()" )
#=> ["test1-2", "test2-2", "test3-2"] ... これは期待通りの結果
p REXML::XPath.match( doc, "/descendant::*/text()" )
#=> [] ... NG: 本当は全てのテキストノードを取得するはず.
p REXML::XPath.match( doc, "/descendant::text()" )
#=> 全てのテキストノードが取得できた. よって descendant 軸に対応していないというわけではない
p REXML::XPath.match( doc, "/descendant::*" )
#=> 全ての要素ノードが取得できた. よって任意の要素ノードに一致する記法 "*" に対応していないわけではない

rescue => err
p err
p err.backtrace()
end

Ruby 1.9.1p0 で上のコードを実行したところ、コード中に記載されているような出力がなされたのですが、1 つ目の XPath "//t1/t2[2]/text()" による結果と 3 つ目の XPath "/descendant::*/text()" による結果が思うようなものではありませんでした。

XPath "//t1/t2[2]/text()"

これは、文書中の全てのノードの子要素 t1 について、(2 つ以上の) 子要素 t2 を持っているか調べて、もし持っている場合 2 つ目の t2 を取得し、その t2 の子テキストノードを得るというものです。 文書中に、2 つ以上の子要素 t2 を持つ要素 t1 は 3 つあり、該当する t2 は子テキストを 1 つずつ持っているので、結果として得られるテキストノードは 3 つあるはずです。

しかし、実際に実行したところ 1 つのテキストノードしか得られませんでした。

"//t1/t2" という XPath も "//t2[2]" という XPath も期待通りの結果を返すため、内部的にどういう問題が起こっているのか見当もつきませんが、とにかくバグがあるようです。

XPath "/descendant::*/text()"

これは文書中の全ての要素について、その子テキストを得るような XPath なのですが、実際に実行したところ何もノードが引っかかりませんでした。

descendant 軸に対応していないのかもと思いましたが、"/descendant::text()" という XPath も "/descendant::*" という XPath はちゃんと期待通りの動きをしました。 こちらも内部的にどういう問題が起こっているのかよくわかりませんが、バグがあるようです。

階層が深くなると問題あり?

1 階層だけならちゃんと動くけど、階層を深くすると問題が起こるのかなぁ、と思ったりしますが・・・まだ詳しく動きを追ってないのでよくわかりません。 また暇なときにでもライブラリの内部実装を見てみますか。