第 11 回 XPath

本日の内容


このドキュメントは http://edu.net.c.dendai.ac.jp/ 上で公開されています。

11-1. XPath

XPath は XML 中の特定の要素を指し示すときに使う表現です。 一つだけではなく集合も扱え、さらに関数も使えます。 いわば XML 文書用のデータベース検索です。

なお、 XPath の最新バージョンは XPath 2.0 ですが、 Java 6 で対応してい るのは XPath 1.0 ですので、以下は XPath 1.0 を解説します。 また、 XQuery 1.0 は XPath2.0 の拡張になっています。 一方、 XPointer は XML 文書の特定の位置を指すための規格ですが、このう ち XSL との共通部分が XPath になっています。

概要

次のような XML 文書を考えます。

<?xml version="1.0" encoding="Shift_JIS" standalone="no" ?>
<class name="データ構造とアルゴリズム II" >
<teacher name="坂本直志" />
<time day="tue" period="2" />
</class>

これに対して、「/class/teacher」という XPath は class 要 素に含まれている teacher 要素を指すことになります。 但し、これは該当するもの全てを指すので、 以下の XML だと二つの teacher 要素を指すことになります。

<?xml version="1.0" encoding="Shift_JIS" standalone="no" ?>
<class name="情報通信基礎実験" >
<teacher name="金田先生" />
<teacher name="坂本直志" />
<time day="mon" period="3" />
<time day="mon" period="4" />
</class>

Java の API

Java 6 では XPath 1.0 に対応しています。 javax.xml.xpath パッケージで抽象化していますので、他の API 同様、ビル ダを作成したのち、 XPath オブジェクトを作ります。 XPath オブジェクトの evaluate メソッドで XPath の解釈をします。 evaluate の引数は 3 つあります。

  1. 一つ目は XPath を表現した文字列
  2. 二つ目は入力で、 org.xml.sax.InputSource オブジェクト
  3. 三つ目は出力の型指定で、 javax.xml.xpath.XPathConstants 内の定 数

ここで、 XPathConstants の定数とは以下のものです。

実際に受け取る型
BOOLEANjava.lang.Boolean
NODEorg.w3c.dom.Node
NODESETorg.w3c.dom.NodeList
NUMBERjava.lang.Double
STRINGjava.lang.String

XPath は該当する要素の集まりを指すことが多いですから、通常は XPathConstants.NODESET を使用することになります。 このとき、 evaluate の戻り値は Object 型なので、戻り値にダウンキャスト する必要があります。 つまり xpath を javax.xml.xpath.XPath オブジェクト、 is を org.xml.sax.InputSource オブジェクトとすると、 典型的な問い合わせは次のようになります。


NodeList nodes = (NodeList) xpath.evaluate(
                  xpathの表現,is, XPathConstants.NODESET);

例11-1

下記は XML 文書を標準入力で受け取り、 /class/teacher で得られる NodeList の各 Element に対して、 name 属性の値を表示しています。


import javax.xml.xpath.*;
import org.xml.sax.*;
import org.w3c.dom.*;
class Rei {
  private static XPath getXPath(){
    final XPathFactory factory = XPathFactory.newInstance();
    return factory.newXPath();
  }
  public static void main(String[] args) throws Exception {
    final InputSource is = new InputSource(new FileInputStream("sample.xml"));
    final XPath xpath = getXPath();
    final NodeList nodes = (NodeList) xpath.evaluate(
                                "/class/teacher",is, XPathConstants.NODESET);
    for (int i = 0; i < nodes.getLength(); i++) {
      System.out.println(((Element)nodes.item(i)).getAttribute("name")); 
    } 
  }
}

さらに、InputSource オブジェクトの代わりに、 DOM のオブジェクトを与え ることもできます。 一方、同じ XPath の表現を何度も使う場合は、java.util.regex.Pattern 同 様にパターンを与えてコンパイルすることで高速化することもできます。 この場合 compile メソッドの戻り値の型は XPathExpression 型になります。

例11-2

下記は入力として DOM を与え、XPath の表現をコンパイルしてから結果を得 ています。


import java.io.*;
import javax.xml.parsers.*;
import javax.xml.xpath.*;
import org.w3c.dom.*;
class Rei {
  private static Document getDocument(InputStream is) throws Exception {
    final DocumentBuilderFactory dbf
          = DocumentBuilderFactory.newInstance();
    final DocumentBuilder db = dbf.newDocumentBuilder();
    return db.parse(is);
  }
  private static XPath getXPath(){
    final XPathFactory factory = XPathFactory.newInstance();
    return factory.newXPath();
  }
  public static void main(String[] args) throws Exception {
    final Document doc = getDocument(new FileInputStream("sample.xml"));
    final XPath xpath = getXPath();
    final XPathExpression expr = xpath.compile("/class/teacher");
    final NodeList nodes = (NodeList) expr.evaluate(doc, 
                                                    XPathConstants.NODESET);
    for (int i = 0; i < nodes.getLength(); i++) {
      System.out.println(((Element)nodes.item(i)).getAttribute("name")); 
    } 
  }
}

11-2. XPath の表現

概要

それでは XPath の表現について見ていきましょう。

XPath は一つの式であり、通常の計算も可能です。

例11-3

XPath に 1+1 を計算させるくだらない例


import javax.xml.parsers.*;
import javax.xml.xpath.*;
import org.w3c.dom.*;
class Rei {
  private static Document getNewDocument() throws Exception {
    final DocumentBuilderFactory dbf
          = DocumentBuilderFactory.newInstance();
    final DocumentBuilder db = dbf.newDocumentBuilder();
    return db.newDocument();
  }
  private static XPath getXPath(){
    final XPathFactory factory = XPathFactory.newInstance();
    return factory.newXPath();
  }
  public static void main(String[] args) throws Exception {
    final XPath xpath = getXPath();
    final Double x = (Double) xpath.evaluate(
            "1+1",getNewDocument(), XPathConstants.NUMBER);
    System.out.println(x);
  }
}

式として返す値は既に示したように、NODESET, BOOLEAN, NUMBER, STRING のどれ かです。 これは、あらかじめ確認し、 evaluate の呼び出し時に指定しなければ なりません。

さて、式のうち、もっとも重要な要素がロケーションパスと呼ば れる、 XML の NODESET を取り出す構文です。 これには様々な機能が用意されていて、豊富な指定方法があります。 そのため、良く使う表現については省略文法という記法が用意さ れています。 しかし、まず、省略なしの文法を見てから、よく使う省略文法を見ることにします。

非省略文法

ロケーションパスは次の構文からなります。

[1] LocationPath ::= RelativeLocationPath | AbsoluteLocationPath
[2] AbsoluteLocationPath ::= '/' RelativeLocationPath ? 
               | 省略型絶対ロケーション記法
[3] RelativeLocationPath ::= Step | RelativeLocationPath '/' Step
                              | 省略型相対ロケーション記法

これは、つまり、 Step が /(スラッシュ) で区切られて、複数回現れるとい う構造です。 次に Step ですが、次のようになっています。

[4] Step ::= AxisSpecifire NodeTest Predicate* | 省略型Step
[5] AxisSpecifire ::= AxisName '::' | 省略型Step識別子

つまり、 Step は 「AxisName::NodeTest」という形の後に 0 個以上の Predicate が続くという書式になります。 それでは、 Axis(軸), NodeTest, Predicate(述語) について説明します。

Axis

Axis とは選択するノードの関係を指定するものです。 AxisName には次のものがあります。

AxisName意味
child ノードの子のノード
descendant ノードの子や孫などの子孫全てのノード
parent ノードの親
ancestor ノードの親、親の親など全ての先祖
following-sibling 後続の兄弟ノード
preceding-sibling 先方の兄弟ノード
following 子孫を除いた以降のノード
preceding 祖先を除いた以前のノード
attribute 属性を返す
namespace 名前空間ノード
self 自分自身
descendant-or-self 自分自身と子孫のノード
ancestor-or-self 自分自身と祖先のノード

なお、上記において、 attribute は Attr、 namespace は 名前空間の型を返 しますが、それ以外は Element またはそのリストを返します。

なお、この選択とは、木構造で言う所の部分木を取得するものです。 そのため、子要素が孫要素を含んでいる場合、子要素を選択すると、各子要素 ごとに孫要素を子要素と含むように取り出されます。

NodeTest

NodeTest は、要素の名前などを記述します。 さらに *(アスタリスク) を書くことで、全ての名前とマッチさせることがで きます。 例えば、 child::* は、対象となっているノードに対して子の 要素を全て選択することになります。 一方、「attribute::*」は注目している要素の属性を全て選択することになり ます。

さらに、この Self を /(スラッシュ) で連続させるということは、その選択 したノードに対して、さらに選択をします。 なお、 XML 文書に対して、木構造の根は空で、その子ノードが根の要素ノー ドになります。

例11-4

以下の XML 文書に対して XPath とその値を示します。

<?xml version="1.0" encoding="Shift_JIS" standalone="no" ?>
<class name="データ構造とアルゴリズム II" >
<teacher name="坂本直志" />
<time day="tue" period="2" />
</class>
XPath
/self::*
/child::*
<class name="データ構造とアルゴリズム II" >
<teacher name="坂本直志" />
<time day="tue" period="2" />
</class>
/child::*/child::*
<teacher name="坂本直志" />
,
<time day="tue" period="2" />
/child::class/child::teacher
<teacher name="坂本直志" />
/descendant::*
<class name="データ構造とアルゴリズム II" >
<teacher name="坂本直志" />
<time day="tue" period="2" />
</class>
,
<teacher name="坂本直志" />
,
<time day="tue" period="2" />

さらに NodeTest はこの他に、comment(), text(), processing-instruction(), node()という表現が可能です。

comment()
(子要素として)含んでいるコメントノードを選択
text()
(子要素として)含んでいるテキストノードを選択
processing-instruction()
(子要素として)含んでいるPIを選択
node()
(子要素として)含んでいるノードを選択

なお、上記の XML 文書に対して、/child::class/child::*/child::class/child::node() は同じになりません。 後者は node() として Element 以外にもマッチするので、 teacher 要素、 time 要素のタグの隙間にある改行コードをテキストノードとして認識します。

Predicate(述語)

述語は [](角カッコ) の中に式を書きます。 式の内容が BOOLEAN である場合は、対象となるノードに対して true のもの だけが選択されます。 一方式の内容が NUMBER だった場合は、対象となるノードのポジション番号が 一致しているものだけが選択されます。 ここでポジション番号とは対象となるノードを、先頭から 1, 2, ... と番号 をつけた値です。

例11-5

XPath
/child::*/child::*[0=0]
<teacher name="坂本直志" />
,
<time day="tue" period="2" />
/child::*/child::*[0=1]
/child::*/child::*[2]
<time day="tue" period="2" />

関数呼び出し

関数呼び出しはそのまま式になります。 関数には次のものがあります。

ノードセット関数
戻り値の型関数意味
NUMBER last() 対象となるノードのサイズ(つまり最後の要素の番号)を返す
NUMBER position() 対象となるノードの位置の番号を返す
NUMBER count(NODESET) node-set のノード数を返す
NODESET id(object) 引数が STRING なら、その文字列を ID として持つ NODESET を返す
STRING local-name(node-set?) node-set あるいは無ければ対象ノードの展開名のローカル部分を返す
STRING name(node-set?) node-set あるいは無ければ対象ノードの展開名を返す
STRING namespace-uri(node-set?) node-set あるいは無ければ対象ノードの展開名の名前空間URIを返す
文字列関数
戻り値の型関数意味
STRING string(object?) 対象となるオブジェクトあるいは無ければ対象ノードを文字列に変換する
STRING concat(STRING,STRING,STRING*) 引数を結合する
BOOLEAN starts-with(STRING,STRING) 一つ目の引数が二つ目の引数で始まるとき true
BOOLEAN contains(STRING,STRING) 一つ目の引数が二つ目の引数を含んでいるとき true
STRING substring-before(STRING,STRING) 一つ目の引数の中で二つ目の引数が始まる直前までの文字列を返す。 substring-before("1999/04/01","/") は 1999 を返す
STRING substring-after(STRING,STRING) 一つ目の引数の中で二つ目の引数の最初の出現以降の文字列を返す。 substring-after("1999/04/01","/") は 04/01 を返す
STRING substring(STRING,NUMBER,NUMBER?) 一つ目の引数の文字列の二つ目の引数から始まり、あれば三番目の引数の 長さの、無ければ最後までの部分列を返す。 substring("12345",1.5,2.6) は 234 を返す
NUMBER string-length(STRING?) 引数あるいは無ければ対象ノードの文字列値の長さを返す
STRING normalize-space(STRING?) 引数あるいは無ければ対象ノードの文字列中の空白列を全て一つの空白に した文字列を返す
STRING translate(STRING,STRING,STRING) 一つ目の引数の文字のうち、二つ目の引数中の文字がある場合、二つ目の 引数中の文字の位置と同じ位置にある三つ目の引数の文字に交換した文字列 を返す。 translate("bar","abc","ABC") は BAr を返す
BOOLEAN関数
戻り値の型関数意味
BOOLEAN boolean(object) オブジェクトをブール値に変換する。数値の場合は NaN と正負の 0 以外 は true
BOOLEAN not(BOOLEAN) 真偽を反転させた値を返す
BOOLEAN true() true を返す
BOOLEAN false() false を返す
BOOLEAN lang(STRING) 引数の示す言語が xml:lang の示す言語を含む時 true を返す
NUMBER関数
戻り値の型関数意味
NUMBER number(object) オブジェクトを数値に変換する。NODESETでは一旦文字列に変換された 後、数値に変換される
NUMBER sum(NODESET) node-set の各ノードについて、数値に変換した後合計の値を求め、返す
NUMBER floor(NUMBER) 引数よりも大きくない最大の整数を返す
NUMBER celing(NUMBER) 引数よりも小さくない最小の整数を返す
NUMBER round(NUMBER) 引数にもっとも近い整数を返す

BOOLEAN型

BOOLEAN は java.lang.Boolean に結びつけられ、論理値を表します。 演算子には or と and があります。=, !=, <, <=, >, >= があ ります。

  1. 両辺が NODESET で文字列変換して比較可能な時はその比較した値が返り ます。 片方が NODESET の場合はもう一方の型に NODESET を変換した後、比較した値 を返します。
  2. 両辺に NODESET が無く、演算子が = か != の場合、 BOOLEAN, NUMBER, STRING の優先順位で型を合わせた後、変換し比較されます。一方が BOOLEAN で他方が NUMBER なら、NUMBER が BOOLEAN に合わせられた後、比較されま す。
  3. 両辺に NODESET が無く、演算子が <, <=, >, >= の時は、両 方共 NUMBER に変換された後、比較されます。 なお、この比較演算子を XML 中に記述する時は &lt; など実体参照を使っ て表現する必要があります。

NUMBER型

NUMBER は java.lang.Double と結びつけられ、浮動小数点型を意味します。 これには通常の数の他、正負の無限大と、NaN と呼ばれる非数値も含みます。 演算子には、 +, -, *, div, mod があります。 div は割り算、 mod は Java の % と同じ余りを求める整数演算です。

なお、符号を反転させるには -(マイナス) 記号を使いますが、 XML の名前に も -(マイナス)記号を許してます。 そのため、名前に対して符号を反転するには、空白を入れて名前の一部でない ことを示す必要があります。

STRING型

XML の仕様により、文字列は UNICODE の抽象キャラクタにより構成される。 これは UTF-16 とは異なる。

NODESET型

|(縦棒) 演算子はノードセットを合成します。

省略構文

省略構文は以下の表現になります。 基本的に child:: は省略可能です。

省略構文等価な非省略構文
// /descendant-self::node()
名前 child::名前
. self::node()
.. parent::node()
@value attribute::value

11-3. XPath の用例

例11-6

以下の XML 文書に対して XPath とその値を示します。

<?xml version="1.0" encoding="Shift_JIS" standalone="no"?>
<itemlist>
  <item>
    <data name="品名" value="りんご"/>
    <data name="単価" value="200"/>
    <data name="個数" value="3"/>
    <data name="合計" value="600"/>
  </item>
  <item>
    <data name="品名" value="みかん"/>
    <data name="単価" value="100"/>
    <data name="個数" value="5"/>
    <data name="合計" value="500"/>
  </item>
  <item>
    <data name="品名" value="もも"/>
    <data name="単価" value="300"/>
    <data name="個数" value="1"/>
    <data name="合計" value="301"/>
  </item>
</itemlist>
XPath意味
/*/*[2] 2階層目の要素のうち、 2 番目のもの
  <item>
    <data name="品名" value="みかん"/>
    <data name="単価" value="100"/>
    <data name="個数" value="5"/>
    <data name="合計" value="500"/>
  </item>
/*/*/*[2] 全ての3階層目の 2 番目要素
<data name="単価" value="200"/>
,
<data name="単価" value="100"/>
,
<data name="単価" value="300"/>
//data[@name='品名'] data 要素のうち name 属性が '品名' と等しいもの
<data name="品名" value="りんご"/>
,
<data name="品名" value="みかん"/>
,
<data name="品名" value="もも"/>
//data[@name='品名']/@value data 要素のうち name 属性が '品名' の value 属性の値 「attribute の value='りんご' のもの」, 「attribute の value='みかん' のもの」, 「attribute の value='もも' のもの」
//data[@name='品名' and @value='りんご'] data 要素のうち name 属性が '品名' で value 属性が 'りんご' のもの
<data name="品名" value="りんご"/>
//data[@name='品名' and @value='りんご']/../data[@name='単価'] <data name='品名' value='りんご'/> と兄弟ノードのうち name属 性が '単価' の data 要素
<data name="単価" value="200"/>
//item[data[@name='単価']/@value * data[@name='個数']/@value != data[@name='合計']/@value] 各 item 要素のうち、 name="単価"を含む data 要素の value 属性の値と name="個数"を含む data 要素の value 属性の値との積が、 name="合計"を含む data 要素の value 属性の値と異なるもの
  <item>
    <data name="品名" value="もも"/>
    <data name="単価" value="300"/>
    <data name="個数" value="1"/>
    <data name="合計" value="301"/>
  </item>
sum(//data[@name='合計']/@value) 各 data 要素のうち、name="合計" 属性をもつものの value 属性の値の 和を求める 1401.0


11-4. 付録

Dentaku

itemlist.xml という XML 文書に対し、引数を XPath として解釈し、結果を XML で出力するプログラムを示します。

実行方法はjava Dentaku XPath式です。


import javax.xml.xpath.*;
import org.xml.sax.*;
import org.w3c.dom.*;
import java.io.*;
import java.util.*;
import javax.xml.namespace.*;
import javax.xml.transform.*;
import javax.xml.transform.dom.*;
import javax.xml.transform.stream.*;
class Dentaku {
  private static final XPath xpath = XPathFactory.newInstance().newXPath();
  private static XPath getXPath(){
    return xpath;
  }
  private static Transformer transformer = null;
  private static Transformer getTransformer() throws Exception {
    if(transformer == null){
      transformer = TransformerFactory.newInstance().newTransformer(); 
      final Properties prop = new Properties();
      prop.setProperty("encoding", "Shift_JIS");
      prop.setProperty("indent", "yes");
      transformer.setOutputProperties(prop);
    }
    return transformer;
  }
  private final static StreamResult streamResult = new StreamResult(System.out);
  private static StreamResult getStreamResult() throws Exception {
    return streamResult;
  }
  private static void printNode(Node n) throws Exception {
    if(n.getNodeType() == Node.ATTRIBUTE_NODE){
      System.out.println("Attr "+n.getNodeName()+": "+n.getNodeValue());
    }else{
      getTransformer().transform(new DOMSource(n),getStreamResult());
      System.out.println();
    }
  }
  private static void evalXPath(String formula,
				  InputSource is,
				  QName returnType) throws Exception {
    if(returnType==XPathConstants.NODESET){
      final NodeList nodes = 
        (NodeList) getXPath().evaluate(formula,is,returnType);
      for (int i = 0; i < nodes.getLength(); i++) {
        printNode(nodes.item(i));
      } 
    }else{
      System.out.println(
                      getXPath().evaluate(formula,is,returnType));
    }
  }
  public static void main(String[] args) throws Exception {
    evalXPath(args[0],
		  new InputSource(new FileInputStream("itemlist.xml")),
		  XPathConstants.NODESET);
  }
}

坂本直志 <sakamoto@c.dendai.ac.jp>
東京電機大学工学部情報通信工学科