XPathにおける//*とdescendant::*の違い
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"]とかの[]の部分