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 の仕様 (W3C)

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

## XPath の基本

### ツリーモデル

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

ツリーの最上位のノードをルートノード( / )と呼びます。ルートノードは文書要素(ルート要素)を子ノードに持ちます (上記の例では rss 要素)。
ルートノードと要素ノードには子ノードがあります。子ノードになり得るのは、要素ノード、テキストノードなどです。属性ノードは子ノードではありませんが、要素ノードに従属します。

### ロケーションパス

#### 相対位置指定

.
カレントノード
要素名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では、条件を付加することによりマッチする要素を絞り込むことができます。
それをうまく使うとプログラムをスマートに記述することができます。

XPathの式の後に[n]をつけて、n番目にマッチするノードだけを指定することができます(1から数えます)。
例えば festival[2] とすれば 2番目の festival 要素を表します。text()[1] とすれば 1番目のテキストノードを表します。
演算子を使った絞り込みもできます。festival[performer/artist='Bjork'] とすると、festivalから見た performer/artist 要素の内容が Bjork の場合だけマッチします。
なお、「=」は比較の演算子です。代入ではありません。
属性を指定することもできます。artist[@vocal='female'] とすると、artist 要素で vocal="female" という属性の指定があるもののみマッチします。
条件の中であらかじめ用意された関数を使うことができます。

artist[starts-with(.,'T')] とすると、artist 要素で、その要素の内容(.)が'T'で始まっているもののみマッチします。
contains(artist,'The') とすれば、子要素 artist が 'The' を含んでいるときに true になります。
属性の値が空白区切りで複数指定されていることがあります(例: class="foo bar" )。
この例で foo が指定されていればマッチさせたい場合、contains(@class,'foo') とします。
ただし、foo を含む属性値が他に存在する場合(例: class="fool" )、区切り文字の空白も含めてマッチさせる必要があります。
contains(concat(" ", normalize-space(@class), " "), " foo ")

条件は論理式なので and や or で結合することができます。

# プログラミング

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

パッケージ javax.xml.xpath (Oracle)

import javax.xml.xpath.*;

以下の手順を踏みます。

DOM ツリーを構築する。
XPath クラスのインスタンスを生成する。
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 のインスタンスを生成してもよいでしょう。