読者です 読者をやめる 読者になる 読者になる

XPathにおける//*とdescendant::*の違い

XPath

XPath Cookbookネタで書いてたんですが、長くなったのでとりあえずこちらに。
id:taizoooにリクエストされた//とdescendant::の違いについて。

下準備として、こういうHTMLをサンプルとして使用します。(サンプルはFirebugのコンソールで実行できます)

document.body.innerHTML = <><![CDATA[
<ul id="root">
	<li>
		<a href="#a1">a1</a>
	</li>
	<li>
		<a href="#b1">b1</a>
	</li>
	<li>
		<a href="#c1">c1</a>
		<a href="#c2">c2</a>
	</li>
	<li>
		<a href="#d1">d1</a>
		<a href="#d2">d2</a>
	</li>
</ul>
]]></>.toString();

まず、//*とdescendant::* が取得する要素は同じものになります。

console.log($x('id("root")//*'));            // [li, a #a1, li, a #b1, li, a #c1, a #c2, li, a #d1, a #d2]
console.log($x('id("root")/descendant::*')); // [li, a #a1, li, a #b1, li, a #c1, a #c2, li, a #d1, a #d2]

//は省略形で、省略しない形にすると/descendant-or-self::node()/
なので、id("root")//*を省略しない形に直すと、id("root")/descendant-or-self::node()/child::* になります。
//は-or-selfがあるのでul#rootが含まれそうに思えますが、/*にはchildが省略されているのでrootは含まれません。
同様に、//aとdescendant::a、//liとdescendant::liも結果に差はありません。これだけ見ると//とdescendant::に違いがない様に思えてしまいます。

しかし、述語*1を使うとはっきりと違いがでてきます。

console.log($x('id("root")//a[1]'));            // [a #a1, a #b1, a #c1, a #d1]
console.log($x('id("root")/descendant::a[1]')); // [a #a1]
console.log($x('id("root")//a[2]'));            // [a #c2, a #d2]
console.log($x('id("root")/descendant::a[2]')); // [a #b1]

こうやって結果を見てみるとすぐにわかると思いますが、//a[1]は複数のaを選択して、descendant::a[1]は(全ての)a要素のうち1つ目の要素だけを選択します。

//a[1]は/descendant-or-self::node()/child::a[1]なので、aの親要素を基点にしてそこから見た1つ目のa要素を選択するので、その条件にマッチする要素は複数存在する場合があります。対して、descendant::a[1]は基点から1つ目のa要素となるので、マッチする要素は必ず1つだけです(もちろん、これは述語でpostion=な指定した場合の話)。

//を使う場合も、括弧でグルーピングすることでdescendant::と同じ結果を得ることは可能です。

console.log($x('(id("root")//a)[1]'));          // [a #a1]

括弧を使うと可読性が下がるので、個人的には//に括弧を使うよりはdescendant::を使うことが多いです。

追記(//はなぜ/descendant-or-self::node()/なのか)

//よりdescendant::のほうが直感的なわかりやすさがあります。なぜ//は/descendant-or-self::node()/なんていう面倒な形なんでしょうか。

ここで、//a[1]を別の意味での省略しないPath、/li/a[1]にしてみます。

console.log($x('id("root")//a[1]'));   // [a #a1, a #b1, a #c1, a #d1]
console.log($x('id("root")/li/a[1]')); // [a #a1, a #b1, a #c1, a #d1]

/li/a[1]と//a[1]は同じ結果になっています。もし//がdescendant::の意味だったら、全然違う結果([a #a1])になってしまいます。
/で区切られたフルパスがあって、そのパスの任意の箇所を省略するために//があると考えると//が/descendant-or-self::node()/なことに納得です。

*1:div[1]とかdiv[@class="p"]とかの[]の部分