XPath テスター

XPath を評価して全一致を表示。名前空間の罠つきで解説。

入力
待機中文書を貼り付けると検査します。入力中にそのまま検証されます。

すべてこのタブ内で実行されます。貼り付けた内容がアップロード・記録・送信されることはありません。 ネットワークパネルを開いて確認する.

文書を貼り付けて式を入力すると、一致したノードがすべて、種類・そこへ到達するパス(/catalog/book[2]/title)・値とともに並びます。一覧の上には一致件数と評価時間が出ます。評価はブラウザー自身のエンジンを document.evaluate 経由で使うので、このページは XPath ライブラリを一切配信しませんし、貼り付けたものがタブの外へ出ることもありません。

このツールが要るのは、式がこれからデバッグしにくい場所へ行こうとしているときです。Schematron の規則、XSLT の match パターン、Camel のスプリッター、スクレイピングのセレクター。//book が何も選ばないとここで分かるほうが、本番で黙って空の結果が返ってから気づくより安上がりです。

違うのは名前空間の扱いです。無料のテスターの多くは document.evaluate に null のリゾルバーを渡すので、プレフィックス付きの式はすべて例外になり、既定名前空間を持つ文書はどれも説明なしで 0 件になります。こちらは文書内のすべての xmlns 宣言を集めてまとめて束縛し、空の結果を返す前に既定名前空間の罠を名指しします。

何にも一致しない式

これは XPath で最もよくある失敗で、しかも症状が「本当にその要素が無い」場合と区別できません。ルートに xmlns="urn:books" が付いた文書を考えてください。その中の要素の名前は book ではありません。展開名は {urn:books}book です。XPath 1.0 に既定名前空間の概念はないので、プレフィックスのない名前は「名前空間なし」を意味し、//book は {}book を求めます。そんなノードは存在しません。0 件、エラーなし。

MDN はこう言い切っています。XPath には、通常の要素参照に適用されている既定名前空間を拾う方法がない。代わりに、その URI に自分でプレフィックスを束縛してください。プレフィックスは式にとってローカルなので、文書が xmlns:b="urn:books" と書いていても、x を束縛しさえすれば //x:book と書いてかまいません。

このページは既定名前空間を見つけると、そこにプレフィックス ns を束縛します。ですから //ns:book は何の準備もなしに動きますし、実際に見つかった URI を入れた形で、結果の上にこの罠を表示します。名前空間の入力欄は prefix=uri の組で自分の束縛を受け付け、そちらが文書から拾ったものより優先されます。

  • プレフィックスを束縛する://ns:book/ns:title。最短で、コードでもこう書きたい形です。
  • 名前空間を無視する://*[local-name()="book"]。どの名前空間の book にも一致します。リゾルバーを自分で制御できない場面のために覚えておく価値があります。
  • プレフィックスなしで厳密に://*[namespace-uri()="urn:books" and local-name()="book"]。
  • 属性は別扱いです。既定名前空間は属性名には決して適用されないので、<book xmlns="urn:books" id="7"/> では要素は {urn:books}book でも属性はただの {}id です。@ns:id ではなく @id で選んでください。

スラッシュ、述語、位置

/ は子へのステップです。// は /descendant-or-self::node()/ の省略形で、だからこそ /catalog/book はルート直下の book 要素だけを見つけ、//book はどの深さのものも見つけます。後者は寛容な代わりに、大きな文書ではかなり遅くなります。1 つのノードの子ではなく、すべてのノードを訪ねるからです。

述語の添字は 0 ではなく 1 から始まるので、[0] で終わる式は黙って何も返しません。もっと分かりにくい罠は、述語が式全体ではなくそのステップに結び付くことです。//book[1] は「自分の親にとって最初の book 子である book すべて」を意味するので、カタログが 3 つある文書では 3 ノード返ります。全体の最初の 1 件が欲しいなら括弧が要ります。(//book)[1] です。

position() と last() はコンテキスト、つまり現在のステップが生み出したノードリストに対する関数です。book[last()] は各親の下の最後の book です。裸の数字は [position() = 2] の省略形であり、だから //book[@lang="en"][1] と //book[1][@lang="en"] は別の集合になります。前者は絞ってから 1 件取り、後者は 1 件取ってから絞ります。

  • child:: が既定の軸なので、book と child::book は同じ式です。
  • descendant:: は下方向を探し、parent::(..)と ancestor:: は上方向を探します。
  • following-sibling:: と preceding-sibling:: は同じ階層に留まります。「この title の次にある price」はこう言います。
  • attribute:: は @ と書き、self:: は省略形では . と書きます。
  • namespace:: は仕様にありますが Firefox は実装していません。これを前提にしないでください。

XPath 1.0 に無いもの

ブラウザーが実装しているのは XPath 1.0 だけです。この API は DOM Level 3 XPath に由来し、今では取り下げられた W3C Note となり、WHATWG DOM 標準の 8 節に生き残っています。2.0 を持つブラウザーはなく、これから出る見込みもありません。ですからこのページは、提供できないバージョンを宣伝するのではなく、天井がどこかを明記します。

XPath 1.0 の型は 4 つです。ノード集合、文字列、数値、真偽値。2.0 はそのモデルを列(シーケンス)とスキーマ対応の型付けに置き換え、いちばん惜しまれるものを持ち込みました。matches()、replace()、tokenize()、本物の日付型、for 式と if 式。3.1 はマップと配列、そして => 矢印を足しました。ここではそのすべてが動きません。PHP の DOMXPath、.NET の XPathNavigator、Java の javax.xml.xpath も同様に 1.0 なので事情は同じです。いちばん使う回避策は、tokenize の代わりの substring-before と substring-after、文字クラスの代わりの translate() です。

結果の読み方

すべての式がノードを返すわけではありません。count(//book) は数値を、string(/catalog/@id) は文字列を返すので、パネルは 4 つの型のどれが返ってきたかを示し、スカラーは空のリストではなく値として印字します。スカラーの 0 と空のノード集合は、多くのツールでは見た目が同じで、意味は違います。

各一致には、ノードの種類(要素、属性、テキスト、CDATA、コメント、処理命令)、そこへ到達するパス、そして値が表示されます。要素なら直列化した XML、属性なら属性値です。一致した内容はマークアップとしてではなく常にテキストとしてページに書き込まれるので、script 要素を含む文書でも何かが実行されることはありません。式を実行する前に文書は整形式でなければならず、束縛されていないプレフィックスは利用可能なプレフィックスとともに名指しで報告され、描画されるのは先頭 1,000 件ですが、一覧の上の件数は本当の総数です。

コードで同じことをする

6 つとも XPath 1.0 を実装しており、6 つとも名前空間プレフィックスを自分で登録させます。文書自身のプレフィックスが自動的に拾われることは決してありません。だからどのサンプルにも名前空間の引数が出てきます。

const source = `<?xml version="1.0"?>
<library xmlns="urn:books">
  <book id="b1"><title>XML in a Nutshell</title></book>
</library>`;

const doc = new DOMParser().parseFromString(source, 'application/xml');
if (doc.querySelector('parsererror')) throw new Error('not well-formed');

// document.createNSResolver is deprecated: it now returns its input
// unchanged and is kept only for compatibility. Write the resolver
// yourself. These prefixes are local to the expression and need not
// match the ones the document uses.
const NS = { bk: 'urn:books' };
const resolve = (prefix) => NS[prefix] ?? null;

// Snapshot, not iterator: an iterator result is invalidated by any
// mutation of the document while you are still walking it.
const snap = doc.evaluate(
  '//bk:book[@id="b1"]/bk:title',   // //title would match nothing
  doc,
  resolve,
  XPathResult.ORDERED_NODE_SNAPSHOT_TYPE,
  null,
);

for (let i = 0; i < snap.snapshotLength; i++) {
  console.log(snap.snapshotItem(i).textContent);
}
from lxml import etree

# lxml's defaults resolve entities and fetch external DTDs. All three
# flags below are needed to close that off.
parser = etree.XMLParser(resolve_entities=False, no_network=True, load_dtd=False)
tree = etree.fromstring(source.encode('utf-8'), parser)

# lxml refuses a None key in the namespaces map, for the same reason the
# spec does: XPath 1.0 cannot address a default namespace. Give it a prefix.
ns = {'bk': 'urn:books'}

for title in tree.xpath('//bk:book/bk:title', namespaces=ns):
    print(title.text)

# An invalid expression raises rather than returning empty.
try:
    tree.xpath('//bk:book[')
except etree.XPathEvalError as e:
    print(f'bad expression: {e}')

# The escape hatch when you cannot register prefixes:
tree.xpath("//*[local-name()='book']")
import javax.xml.XMLConstants;
import javax.xml.namespace.NamespaceContext;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.xpath.*;
import org.w3c.dom.NodeList;
import java.util.Iterator;
import java.util.Map;

DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();

// setNamespaceAware is FALSE by default. Leave it off and every prefixed
// element is treated as one long local name, so bk:book never matches.
// This is the usual cause of "it works in xmllint but not in Java".
dbf.setNamespaceAware(true);
dbf.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
dbf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);

var doc = dbf.newDocumentBuilder()
    .parse(new java.io.ByteArrayInputStream(bytes));

XPathFactory xpf = XPathFactory.newInstance();
xpf.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
XPath xpath = xpf.newXPath();

Map<String, String> ns = Map.of("bk", "urn:books");
xpath.setNamespaceContext(new NamespaceContext() {
    public String getNamespaceURI(String prefix) {
        return ns.getOrDefault(prefix, XMLConstants.NULL_NS_URI);
    }
    public String getPrefix(String uri) { return null; }
    public Iterator<String> getPrefixes(String uri) { return null; }
});

NodeList nodes = (NodeList) xpath.evaluate(
    "//bk:book/bk:title", doc, XPathConstants.NODESET);

for (int i = 0; i < nodes.getLength(); i++) {
    System.out.println(nodes.item(i).getTextContent());
}
using System.Xml;
using System.Xml.XPath;

var settings = new XmlReaderSettings
{
    DtdProcessing = DtdProcessing.Prohibit,
    XmlResolver = null,
};

using var reader = XmlReader.Create(new StringReader(source), settings);
var nav = new XPathDocument(reader).CreateNavigator();

// XmlNamespaceManager is not optional. .NET has no default-namespace
// concept in XPath either, so bind a prefix and use it.
var ns = new XmlNamespaceManager(nav.NameTable);
ns.AddNamespace("bk", "urn:books");

foreach (XPathNavigator node in nav.Select("//bk:book/bk:title", ns))
{
    Console.WriteLine(node.Value);
}

// Compile once if the expression is reused: Select() reparses every call.
XPathExpression expr = nav.Compile("count(//bk:book)");
expr.SetContext(ns);
Console.WriteLine((double)nav.Evaluate(expr));
<?php
$doc = new DOMDocument();
// LIBXML_NONET stops libxml2 fetching anything the document references.
if (!$doc->loadXML($source, LIBXML_NONET)) {
    fwrite(STDERR, "not well-formed\n");
    exit(1);
}

$xpath = new DOMXPath($doc);

// Prefixes must be registered even when the document declares them.
// registerNamespace('', ...) is accepted but useless: XPath 1.0 still
// cannot address the empty prefix.
$xpath->registerNamespace('bk', 'urn:books');

foreach ($xpath->query('//bk:book/bk:title') as $node) {
    echo $node->textContent, "\n";
}

// query() returns false on an invalid expression, not an empty list.
// A loose == comparison would read that false as "no results".
$result = $xpath->query('//bk:book[');
if ($result === false) {
    fwrite(STDERR, "invalid XPath expression\n");
}
# xmllint ships with libxml2 and is almost certainly already installed.
# --xpath has no way to register a namespace prefix, so against a
# namespaced document it prints "XPath set is empty" and exits 10.
xmllint --nonet --xpath '//book/title' doc.xml

# The interactive shell does have setns, and reads fine from a heredoc:
xmllint --nonet --shell doc.xml <<'EOF'
setns bk=urn:books
xpath //bk:book/bk:title
EOF

# Or sidestep prefixes entirely:
xmllint --nonet --xpath "//*[local-name()='title']/text()" doc.xml

# xmlstarlet is the friendlier option if you can install it:
xmlstarlet sel -N bk=urn:books -t -v '//bk:book/bk:title' -n doc.xml

6 つに共通することが 2 つあります。名前空間プレフィックスは文書が供給するものではなく、あなたが宣言するものであること。そして不正な式の知らせ方がそれぞれ違うこと(DOMException が投げられる、XPathEvalError が上がる、false が返る、終了ステータス 10 になる)。だからどのサンプルも、クエリの呼び出しと結果の利用を 1 行で済ませていません。

よくある質問

XPath が何も返さないのはなぜですか。

たいていは、文書に既定名前空間があるのに式にプレフィックスが無いからです。ルートに xmlns="urn:something" が付いていれば、その中の book は実際には {urn:something}book であり、//book は「名前空間なしの book」を求めます。0 件でエラーなし。XPath の側から見れば何も間違っていないからです。

このページはそれを検出し、見つけた URI を名指しし、そこにプレフィックス ns を束縛するので //ns:book がすぐ動きます。プレフィックスを避けたいなら、//*[local-name()="book"] が名前空間に関係なくローカル名で一致します。他に排除すべき原因は大文字小文字(XPath は区別します)と、添字が 1 から始まるのに [0] と書いている場合です。

式を試すとき XML はアップロードされますか。

いいえ。整形式の走査はこのタブの Web Worker で動き、式はブラウザーのローカル API である document.evaluate が評価します。ここには何かを送るサーバー側の仕組みがありません。

XPath では、それが多くのツール以上に重要です。人が式を書く相手の文書はサンプルではなく本物のペイロードだからです。取引先の API から取得したレスポンス、キューから取り出したメッセージ、SAML アサーション。ネットワークタブを開き、文書を貼り付け、式を実行して、空のままであることを確かめてください。

XPath のどのバージョンに対応していますか。

XPath 1.0 です。ブラウザーが実装しているのがそれで、代替が存在しないからです。評価は WHATWG DOM 標準の 8 節で定義された document.evaluate で行われます。Chrome も Firefox も Safari も 1.0 で、それ以上へ進む意向を表明したところはありません。

ですから matches()、replace()、tokenize()、for 式と if 式、日付型、シーケンス、マップ、配列は、ここではすべて動きません。PHP の DOMXPath、.NET の XPathNavigator、Java の javax.xml.xpath でも同じです。2.0 や 3.1 が必要なら Saxon になります。ブラウザーなら Saxon-JS、JVM や .NET なら Saxon-HE です。

XPath の / と // の違いは何ですか。

/ は直接の子を選びます。// は /descendant-or-self::node()/ の省略形で、どの深さでも選びます。ですから /catalog/book はルートの catalog のすぐ内側にある book 要素に一致し、//book はどこにある book にも一致します。先頭の / は文書のルートに錨を下ろすので、ルートが catalog の文書では /book は失敗します。

罠は // と述語を組み合わせることです。//book[1] は「文書中の最初の book」ではありません。述語はステップに掛かるので、「自分の親にとって最初の book 子である book すべて」を意味し、カタログが 3 つあれば 3 ノード返ります。意図した結果には括弧を使ってください。(//book)[1] です。

要素ではなく属性を選ぶには。

名前の前に @ を付けます。//book/@id は id 属性ノードを選び、パネルはその値・属する要素のパス・種類を表示します。属性で絞り込みたい(選びたいのではない)場合は述語に入れます。//book[@id="b1"] が選ぶのは book 要素であって属性ではありません。

属性には独自の名前空間規則があり、そこを取り違える人が多いのです。既定名前空間の宣言は属性名には決して適用されないので、<book xmlns="urn:books" id="7"/> の属性はただの {}id です。@ns:id は何にも一致しないので @id で選んでください。属性が名前空間に属するのは、xlink:href や xsi:schemaLocation のように自分でプレフィックスを書いたときだけです。

ここで HTML に対して XPath を試せますか。

その HTML が整形式の XML であるときだけです。多くはそうではありません。入力は application/xml として解析されるので、閉じていない br タグ、引用符のない属性値、URL 中の生のアンパサンドは、式が走る前に弾かれます。これは意図したものです。XPath は HTML の DOM に対しては振る舞いが変わり、要素名は小文字化され、すべてが XHTML 名前空間に入るからです。

ここのように厳密な XMLDocument に対しては、ノードテストは大文字小文字を区別し、名前空間は仕様どおりに振る舞います。現実世界の HTML には寛容なパーサーを使ってください。Python の lxml.html、Java の jsoup、あるいは CSS セレクターで足りるなら querySelector。XHTML、SVG、すでに整えた断片なら、ここでも解析できます。

関連ツール

関連する解説

これで解決できるエラー