XPathによる
特定部分の指定

XPath - XML文書の特定の部分を指定

XML文書から必要な情報を抽出する際、その情報がDOMツリーのどこにあるのかが問題です。 例えばフィード(RSS 2.0)の先頭の項目のタイトルが欲しいのであれば、 DOMツリーでの位置は、 ルート要素である rss 要素の子ノードの channel 要素の先頭の子ノードである item 要素の子ノードの title 要素の内容、ということになります。

<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0">
  <channel>
    <item>
      <title>りんご</title>
      <link>http://www.apple.com/jp/</link>
    </item>
    <item>
      <title>みかん</title>
      <link>https://orangeamps.com/</link>
    </item>
    <item>
      <title>バナナ</title>
      <link>https://en.wikipedia.org/wiki/The_Velvet_Underground_%26_Nico</link>
    </item>
  </channel>
</rss>

この位置の情報を、単純な文字列で表現する方法があります。それがパス(path)です。

    /rss/channel/item

パスは、ファイルシステムにおけるファイルやフォルダ(ディレクトリ)の位置、 URLにおけるサイト内のフォルダ階層を表現する方法として広く使われています。

    C:\Program Files\Java
    https://www.w3.org/TR/xpath-3/

XML文書の特定の部分をパスで指定する方法として、 XPath (XML Path Language) があります。

必要な情報がどこにあるのかをパスを用いて表現することにより、 そのパスを入力とした抽出処理を考えることができるようになります。

XPath の基本

ツリーモデル

XPath では、XML文書がノードの親子関係に基づくツリー構造になっているとします。

ロケーションパス

相対位置指定

.
カレントノード
要素名A
カレントノードの子要素である要素A
要素名A/要素名B
その要素Aの子要素である要素B
要素名A//要素名C
カレントノードの子要素である要素Aの子孫要素C
要素名A/@属性名D
カレントノードの子要素である要素Aの属性D
要素名A/text()
カレントノードの子要素である要素Aの子ノードであるテキストノード

ただ要素名を書くと、カレントノードの子要素になります。

絶対位置指定

/
ルートノード
/要素名A
ルート要素A
/要素名A/要素名B
ルート要素Aの子要素である要素B
//要素名C
文書内のすべての要素C
/要素名A/要素名B/@属性D
ルート要素Aの子要素である要素Bの属性D

カレントノードが "/" の場合の "RDF" は、"/RDF" とも書けるわけです。 ディレクトリの相対/絶対パスと同じ話です。

"//item" とすると、DOMツリー上の全item要素がマッチします。 これは、getElementsByTagName("item") とするのと同じことです。 便利なようですが、指定された要素をツリー内で全探索することになるので、 パスが分かっている場合には省略せずに記述することで無駄な探索を避けることができます。

条件によるノードの絞り込み

XPathでは、条件を付加することによりマッチする要素を絞り込むことができます。 それをうまく使うとプログラムをスマートに記述することができます。

プログラミング

Java では java.xml モジュールの javax.xml.xpath パッケージが XPath version 1.0 をサポートします。

import javax.xml.xpath.*;

以下の手順を踏みます。

  1. DOM ツリーを構築する。
  2. XPath クラスのインスタンスを生成する。
  3. XPath 式を評価し、結果を得る。

DOM ツリーの構築

XML文書の DOM ツリーをあらかじめ構築しておきます。 その際、名前空間を使わないように指定しておくと、XPath 式の中でも名前空間の指定が不要になります。

	// inputStream は対象に合わせて用意しておく
	
	DOMImplementationRegistry registry = DOMImplementationRegistry.newInstance();
	DOMImplementationLS implementation = (DOMImplementationLS)registry.getDOMImplementation("XML 1.0");
	// 読み込み対象の用意
	LSInput input = implementation.createLSInput();
	input.setByteStream(inputStream);
	input.setEncoding("UTF-8");
	// 構文解析器(parser)の用意
	LSParser parser = implementation.createLSParser(DOMImplementationLS.MODE_SYNCHRONOUS, null);
	parser.getDomConfig().setParameter("namespaces", false);
	// DOMの構築
	Document document = parser.parse(input);

この授業のサンプルプログラムでは、名前空間を使わないよう指定されているものが多くなっています。

XPath クラスのインスタンスの生成

XPath クラスのインスタンスを、XPathFactory クラスの newXPath() メソッドにより生成します。

	// XPath の表現を扱う XPath オブジェクトを生成
	XPathFactory factory = XPathFactory.newInstance();
	XPath xPath = factory.newXPath();

XPath 式の評価

XPath クラスの evaluate メソッドを使って XPath 式を評価します。 その際、2つめの引数に基準となる位置 (Document, Nodeなど)、 3つめの引数に戻り値の種類 (NODESET, NODEなど) を与えます。

XPath 式にあてはまるノードは一般に複数あるため、戻り値の型は NodeList になります。

	// document は DOM ツリーを参照しているものとする
	NodeList itemNodeList = (NodeList)xPath.evaluate("/rss/channel/item",
			document, XPathConstants.NODESET); // RSS 2.0

evaluate の 3つめの引数に与えている XPathConstants.NODESET は、 ノードの集合を戻り値として返してね、という指示を表しています。

あてはまるノードが1つしかないことがわかっている場合や、先頭の1つしか必要ない場合には、 ノード1つを返すように指定することもできます。 例えば、ノード node の子ノード title を得るには次のように書くことができます。

	// node は item要素のノードを参照しているものとする
	Node titleNode = (Node)xPath.evaluate("title",
			node, XPathConstants.NODE); // RSS

XPathConstants.NODE は、ノード1つを戻り値として返してね、という指示を表していて、 戻り値は Node オブジェクトになります。よって Node 型にキャストします。

ノード自身ではなくノードの内容の文字列が欲しい場合には、戻り値として XPathConstants.STRING を指定することができます。 また、その目的専用の、3つめの引数がない evaluate も用意されています。 これは戻り値の型がはじめから String になっているためキャストする必要がありません。

	// node は item要素のノードを参照しているものとする
	String title = xPath.evaluate("title", node); // RSS

サンプルのメソッド

例外処理が必要な点に注意しましょう。

import javax.xml.xpath.*;

...

    /** Feed の内容を Item オブジェクトのリストとして返す */
    public ArrayList<Item> getItemList() {
	ArrayList<Item> itemList = new ArrayList<Item>();
	try {
	    // XPath の表現を扱う XPath オブジェクトを生成
	    XPathFactory factory = XPathFactory.newInstance();
	    XPath xPath = factory.newXPath();
	    // document に対して XPath を適用
	    // XPathConstants.NODESET は戻り値がノードの集合という意味
	    // RSS 2.0
	    NodeList itemNodeList = (NodeList)xPath.evaluate("/rss/channel/item",
					document, XPathConstants.NODESET);
	    // RSS 1.0
	    if(itemNodeList.getLength() == 0) {
		itemNodeList = (NodeList)xPath.evaluate("/RDF/item",
					document, XPathConstants.NODESET);
	    }
	    // 名前空間が有効な DOM tree の場合は以下のように書く
	    //   "/*[local-name()='RDF']/*[local-name()='item']"
	    
	    // ノードリストの各 item要素から Item オブジェクトを生成
	    for(int i = 0; i < itemNodeList.getLength(); i++) {
		// Node から Item を生成する getItem(Node) があるとする
		itemList.add(getItem(itemNodeList.item(i)));
	    }
	}
	catch (DOMException e) {
            System.err.println("DOMエラー:" + e);
	}
	catch (XPathExpressionException e) { // 追加
            System.err.println("XPath 表現のエラー:" + e);
	}
	return itemList;
    }

Node から Item を生成する getItem(Node) が存在すると仮定しています。 new で Item のインスタンスを生成してもよいでしょう。