Sep 13, 2007

CSS3セレクタとXPathでの表現の対応表

拡張機能勉強会の時に焚き付けられたText Shadowのコード(textshadow.js)を教材にして拡張機能開発のノウハウを解説していくシリーズ。

XPathをノードの検索に活用する方法を紹介したけど、肝心のXPathが書けなきゃ意味がないわけで。でもXPathって、ノードセットがどうとかノードテストがどうとか軸がどうとか修飾がどうとか、いざ勉強しようとしてもこれ専用の用語がやたらたくさん登場してきてものすごく萎える。CSSのセレクタの方が、機能は限られてるけどまだ分かりやすい。CSSのセレクタとXPath式の対応表があればいいのになあ、ということを、だいぶ前から僕は思ってた。

実は何年か前、哀さんのサイト(Black Box)でそういうコンテンツがあったんだけど、移転のゴタゴタか何かで消滅したままになってる。しかも、今「CSS XPath」みたいなキーワードでGoogleで検索してみて上位にくるエントリは、情報が不十分だったり間違いが含まれてたりする。

というわけで、CSS3セレクタ(このエントリを書いた時点ではワーキングドラフト)とXPath式の対応表で、詳細な物を作ってみた。

基本
CSSのセレクタ 対応するXPath式 備考
*(セレクタの最初に書く場合) /descendant::*
EE要素/セレクタの最初に書く場合) /descendant::E Geckoでは、text/htmlなHTML文書だとXPath式内での要素名は大文字で書く必要がある。text/htmlとapplication/xml+htmlの両方に対応させたければ、/descendant::*[local-name()="DIV" or local-name()="div"]のように書く必要がある。
*(子セレクタ、子孫セレクタ、隣接セレクタ、後続セレクタに書く場合) *
EE要素/子セレクタ、子孫セレクタ、隣接セレクタ、後続セレクタに書く場合) E Geckoでは、text/htmlなHTML文書だとXPath式内での要素名は大文字で書く必要がある。text/htmlとapplication/xml+htmlの両方に対応させたければ、/descendant::*[local-name()="DIV" or local-name()="div"]のように書く必要がある。
ここから下の例のXPath式で、頭にEや*がある物は、式の途中に出てくる場合を想定している。式の最初に書く場合は、いずれも/descendant::E/descendant::*と書くことになる。
E[foo] E[@foo]
E[foo="bar"] E[@foo="bar"]
E[foo~="bar"] E[contains(concat(" ",@foo," "), " bar " )] 「空白区切りのリストの中で、対応する値がある」という条件に対して、属性値と検索したい文字列の両方の前後に空白文字を加えた上で、contains()で検索している。
E[foo^="bar"] E[starts-with(@foo, "bar" )]
E[foo$="bar"] E[substring(@foo, string-length(@foo) - string-length("bar") + 1) = "bar"] XPath 1.0にはend-with()が無いので、文字列操作を使う必要がある。XPath 1.0では文字の位置は0からではなく1から始まることに注意が必要。なお、検索対象の文字列が固定なら、- string-length("bar") + 1の部分は- 2のように直接数値を書いてもよい。
E[foo*="bar"] E[contains(@foo, "bar" )]
E[hreflang|="en"] E[@hreflang="en" or starts-with(@hreflang, "en-")]
E:root /E
または
E[not(parent::*)]
E:nth-child(n) E[count(preceding-sibling::*) = n - 1]
E:nth-last-child(n) E[count(following-sibling::*) = n - 1]
E:nth-of-type(n) E[count(preceding-sibling::E) = n - 1]
E:nth-last-of-type(n) E[count(following-sibling::E) = n - 1]
E:first-child E[not(preceding-sibling::*)]
E:last-child E[not(following-sibling::*)]
E:first-of-type E[not(preceding-sibling::E)]
E:last-of-type E[not(following-sibling::E)]
E:only-child E[count(parent::*/child::*) = 1]
または
*[parent::* and last() = 1 and self::E]
または
*[parent::* and last() = 1 and local-name()="E"]
child::は省略することもできる(短縮表記)。
E:only-of-type E[count(parent::*/child::E) = 1]
または
E[parent::* and last() = 1]
child::は省略することもできる(短縮表記)。
E:empty E[not(*) and not(text())]
:link
:visited
[@href and contains(" a A area AREA ", concat(" ",local-name()," "))]
または
[@href and (self::a or self::area)]
要素名を条件にしているので、HTML以外の場合はその文書が使っているXMLの語彙に合わせて書き換える必要がある。:visitedの識別は、JavaScriptなどを使わないと不可能。
E:link
E:visited
E[@href]
E:active
E:hover
E:focus
XPathのみでは再現不可能。JavaScriptなどを使って識別する必要がある。
E:target XPathのみでは再現不可能。JavaScriptなどを使って識別する必要がある。
E:lang(fr) E[ancestor-or-self::*[@lang = "fr" or starts-with(@lang, "fr-")] and count(ancestor-or-self::*[@lang][1]/ancestor::*) = count(ancestor-or-self::*[@lang = "fr" or starts-with(@lang, "fr-")][1]/ancestor::*)] この式ではxml:langのことは考慮していないので、注意が必要。
E:enabled E[(@enabled and (@enabled = "true" or @enabled = "enabled")) or (@disabled and @disabled = "false")]
E:disabled E[(@disabled and (@disabled = "true" or @disabled = "disabled")) or (@enabled and @enabled = "false")]
E:checked E[(@checked and (@checked = "true" or @checked = "checked")) or (@selected and (@selected = "true" or @selected = "selected"))] selected属性についても評価するのは余計かも……
E::first-line XPathのみでは再現不可能。JavaScriptなどを使って識別する必要がある。
E::first-letter XPathのみでは再現不可能。JavaScriptなどを使って識別する必要がある。
E::selection XPathのみでは再現不可能。JavaScriptなどを使って識別する必要がある。
E::before XPathのみでは再現不可能。
E::after XPathのみでは再現不可能。
E.warning E[contains(concat(" ",normalize-space(@class)," "), " warning ")] 属性セレクタの場合と同様に、「空白区切りのリストの中で、対応する値がある」という条件に対して、class属性の属性値と検索したいクラス名の両方の前後に空白文字を加えた上で、contains()で検索している。また、@classにはタブ文字や2つ以上のスペースも登場し得るので、normalize-space()で正規化している。
E#myid E[@id="myid"] 属性名を条件にしているので、HTMLやXULやSVGなど「id属性=ID」となっている言語以外の場合はその文書が使っている言語の仕様に合わせて書き換える必要がある。
E:not(s) E[not(s)]
E F E/descendant::F
E > F E/child::F child::は省略することもできる(短縮表記)。
E + F E/following-sibling::*[1][local-name()="F"]
または
E/following-sibling::*[1][self::F]
E ~ F E/following-sibling::F

以上の基本の例を組み合わせて使うことになる。以下に応用例を示す。

応用
CSSのセレクタ 対応するXPath式 備考
div.note.column /descendant::div[contains(concat(" ",@class," "), " note ") and contains(concat(" ",@class," "), " column ")]
section > h, section > body /descendant::section/child::h | /descendant::section/child::body
または
/descendant::section/child::*[local-name()="h" or local-name()="body"]
または
/descendant::section/child::*[self::h or self::body]
XPath式では、|演算子で複数の式の評価結果の和集合を得られる。また、条件を入れ子にできるので、似た条件のCSSセレクタは一つのXPath式にまとめることもできる。
div p:first-child > em /descendant::div/descendant::p[not(preceding-sibling::*)]/em
:link[href$=".pdf"], :visited[href$=".pdf"] /descendant::*[contains(" a A area AREA ", concat(" ",local-name()," ")) and @href and substring(@href, string-length(@href) - 3) = ".pdf"]
html em:not(.highlight) /descendant::html/descendant::em[not([contains(concat(" ",@class," "), " highlight ")])] Geckoでtext/htmlとapplication/xml+htmlの両方で有効にしようと思ったら、/self::*[local-name()="HTML" or local-name()="html"]/descendant::*[(local-name()="EM" or local-name()="em") and not([contains(concat(" ",@class," "), " highlight ")])]と書く必要がある。
tr:nth-child(even)
tr:nth-child(2n)
/descendant::tr[count(preceding-sibling::*) mod 2 = 1]
tr:nth-child(odd)
tr:nth-child(2n + 1)
/descendant::tr[count(preceding-sibling::*) mod 2 = 0]
tr:nth-last-child(even)
tr:nth-last-child(2n)
/descendant::tr[count(following-sibling::*) mod 2 = 1]
tr:nth-child(3n) /descendant::tr[count(preceding-sibling::*) mod 3 = 1]
tr:nth-child(5n + 2) /descendant::tr[(count(preceding-sibling::*) + 2) mod 5 = 1]

手作業で変換するのがめんどい人は、Text Shadowのコードを抜き出して作ったselector.jsを使って下さいってことで……

XPathをちゃんと使うなら、横着せずにきちんと勉強するのがおすすめです。XPathはちゃんと使えばCSSセレクタでできる範囲よりもずっと柔軟な表現ができますんで。(なので、同じ結果を得られる式でも様々な書き方ができるし、上の表に挙げた例も、これら以外にまだまだ色々な書き方があり得る。)

エントリを編集します。

wikieditish message: Ready to edit this entry.










E[(@checked and (@checked = "true" or @checked = "checked")) or (@selected and (@selected = "true" or @selected = "selected"))] selected属性についても評価するのは余計かも…… E::first-line XPathのみでは再現不可能。JavaScriptなどを使って識別する必要がある。 E::first-letter XPathのみでは再現不可能。JavaScriptなどを使って識別する必要がある。 E::selection XPathのみでは再現不可能。JavaScriptなどを使って識別する必要がある。 E::before XPathのみでは再現不可能。 E::after XPathのみでは再現不可能。 E.warning E[contains(concat(" ",normalize-space(@class)," "), " warning ")] 属性セレクタの場合と同様に、「空白区切りのリストの中で、対応する値がある」という条件に対して、class属性の属性値と検索したいクラス名の両方の前後に空白文字を加えた上で、contains()で検索している。また、@classにはタブ文字や2つ以上のスペースも登場し得るので、normalize-space()で正規化している。 E#myid E[@id="myid"] 属性名を条件にしているので、HTMLやXULやSVGなど「id属性=ID」となっている言語以外の場合はその文書が使っている言語の仕様に合わせて書き換える必要がある。 E:not(s) E[not(s)] E F E/descendant::F E > F E/child::F child::は省略することもできる(短縮表記)。 E + F E/following-sibling::*[1][local-name()="F"] または E/following-sibling::*[1][self::F] E ~ F E/following-sibling::F 以上の基本の例を組み合わせて使うことになる。以下に応用例を示す。 応用 CSSのセレクタ 対応するXPath式 備考 div.note.column /descendant::div[contains(concat(" ",@class," "), " note ") and contains(concat(" ",@class," "), " column ")] section > h, section > body /descendant::section/child::h | /descendant::section/child::body または /descendant::section/child::*[local-name()="h" or local-name()="body"] または /descendant::section/child::*[self::h or self::body] XPath式では、|演算子で複数の式の評価結果の和集合を得られる。また、条件を入れ子にできるので、似た条件のCSSセレクタは一つのXPath式にまとめることもできる。 div p:first-child > em /descendant::div/descendant::p[not(preceding-sibling::*)]/em :link[href$=".pdf"], :visited[href$=".pdf"] /descendant::*[contains(" a A area AREA ", concat(" ",local-name()," ")) and @href and substring(@href, string-length(@href) - 3) = ".pdf"] html em:not(.highlight) /descendant::html/descendant::em[not([contains(concat(" ",@class," "), " highlight ")])] Geckoでtext/htmlとapplication/xml+htmlの両方で有効にしようと思ったら、/self::*[local-name()="HTML" or local-name()="html"]/descendant::*[(local-name()="EM" or local-name()="em") and not([contains(concat(" ",@class," "), " highlight ")])]と書く必要がある。 tr:nth-child(even)tr:nth-child(2n) /descendant::tr[count(preceding-sibling::*) mod 2 = 1] tr:nth-child(odd)tr:nth-child(2n + 1) /descendant::tr[count(preceding-sibling::*) mod 2 = 0] tr:nth-last-child(even)tr:nth-last-child(2n) /descendant::tr[count(following-sibling::*) mod 2 = 1] tr:nth-child(3n) /descendant::tr[count(preceding-sibling::*) mod 3 = 1] tr:nth-child(5n + 2) /descendant::tr[(count(preceding-sibling::*) + 2) mod 5 = 1] 手作業で変換するのがめんどい人は、[Text Shadowのコードを抜き出して作ったselector.js](/latest/blosxom/webtech/javascript/2007-07-31_selectorjs.htm)を使って下さいってことで……document.write('おまけでselector.jsのデモ。テキストフィールドにCSSセレクタを入れたら対応するXPath式を表示します。このエントリで書いてる内容と違う結果が出る場合は……そのうち修正します。');document.write('セレクタ:');document.write('XPath式:');XPathをちゃんと使うなら、横着せずにきちんと勉強するのがおすすめです。XPathはちゃんと使えばCSSセレクタでできる範囲よりもずっと柔軟な表現ができますんで。(なので、同じ結果を得られる式でも様々な書き方ができるし、上の表に挙げた例も、これら以外にまだまだ色々な書き方があり得る。)




-->
拡張機能