宣伝。日経LinuxにてLinuxの基礎?を紹介する漫画「シス管系女子」を連載させていただいています。
以下の特設サイトにて、単行本まんがでわかるLinux シス管系女子の試し読みが可能!
拡張機能勉強会の時に焚き付けられた、Text Shadowのコード(textshadow.js)を教材にして拡張機能開発のノウハウを解説していくシリーズ。
XPathをノードの検索に活用する方法を紹介したけど、肝心のXPathが書けなきゃ意味がないわけで。でもXPathって、ノードセットがどうとかノードテストがどうとか軸がどうとか修飾がどうとか、いざ勉強しようとしてもこれ専用の用語がやたらたくさん登場してきてものすごく萎える。CSSのセレクタの方が、機能は限られてるけどまだ分かりやすい。CSSのセレクタとXPath式の対応表があればいいのになあ、ということを、だいぶ前から僕は思ってた。
実は何年か前、哀さんのサイト(Black Box)でそういうコンテンツがあったんだけど、移転のゴタゴタか何かで消滅したままになってる。しかも、今「CSS XPath」みたいなキーワードでGoogleで検索してみて上位にくるエントリは、情報が不十分だったり間違いが含まれてたりする。
というわけで、CSS3セレクタ(このエントリを書いた時点ではワーキングドラフト)とXPath式の対応表で、詳細な物を作ってみた。
CSSのセレクタ | 対応するXPath式 | 備考 |
---|---|---|
* (セレクタの最初に書く場合) |
/descendant::* |
|
E (E要素/セレクタの最初に書く場合) |
/descendant::E |
Geckoでは、text/htmlなHTML文書だとXPath式内での要素名は大文字で書く必要がある。text/htmlとapplication/xml+htmlの両方に対応させたければ、/descendant::*[local-name()="DIV" or local-name()="div"] のように書く必要がある。 |
* (子セレクタ、子孫セレクタ、隣接セレクタ、後続セレクタに書く場合) |
* |
|
E (E要素/子セレクタ、子孫セレクタ、隣接セレクタ、後続セレクタに書く場合) |
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セレクタでできる範囲よりもずっと柔軟な表現ができますんで。(なので、同じ結果を得られる式でも様々な書き方ができるし、上の表に挙げた例も、これら以外にまだまだ色々な書き方があり得る。)
むかーしxsltごりごり書いてたのを思い出した。
なんつーかXpathって正規表現っぽいよね。
使いこなすと便利だけどやりすぎると意味がわからんよーになるとことかw
'/' が表すのはルートノード (DOM でいう Document) であってルート要素ノード (DOM でいう Document#documentElement) ではありません。よって E:root に対する /self::E というのは誤りで、/child::E としなくてはなりません。また、同様の理由により、*、E に対応するのは /descendant::*、/descendant::E で十分だと思います。
また、属性セレクタに関しては以下がより正しく CSS の指定を再現できると思います。(元のままだと hreflang="end" なども引っかかってしまう)
E[hreflang|="en"] →
E[@hreflang = "en" or starts-with(@hreflang, "en-")]
リンク擬似クラスに関しても、E:link のように要素名を指定しているなら、E[@href] で十分かと思います。*:link に対してなら表の書き方で、かつ E を * に置き換えたものでもいけますが。(厳密に言うと link 要素も加えるべきかもしれません)
さらに、lang 擬似クラスに関しては次の式で表現可能かと思います。(xml:lang も考慮に入れるともっと複雑になりますが)
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::*)]
ほかにも、効率がいいかどうかは知りませんが、このような書き方ができますね。
E:nth-child(n) →
/descendant-or-self::node()/child::*[position() = n and self::E] (文書要素も含める場合)
/descendant::*/child::*[position() = n and self::E] (文書要素を含めなくてもいい場合。以下同様)
E F:nth-child(n) →
E/descendant-or-self::*/child::*[position() = n and self::F]
E:nth-last-child(n) →
/descendant::*/child::*[position() = last() - n + 1 and self::E]
E:nth-of-type(n) →
/descendant::*/child::E[position() = n]
/descendant::*/child::E[n]
E:nth-last-of-type(n) →
/descendant::*/child::E[position() = last() - n + 1]
/descendant::*/child::E[last() - n + 1]
E:(first|last)-(child|of-type) も n に 1 を代入することで同様。
E:only-child →
/descendant::*/child::*[last() = 1 and self::E]
E:only-of-type →
/descendant::*/child::E[last() = 1]
*:link *:visited →
*[@href and (self::a or self::area)]
E + F →
E/following-sibling::*[position() = 1 and self::F]
ご指摘ありがとうございます。さっそく表にいくつか反映させました。
xml:lang 属性に関しては lang 関数というそのものずばりの関数があったのですね。元の式にも効率の悪い部分があったので、あわせて以下でいけると思います。
E:lang(fr) →
E[lang("fr") or ancestor-or-self::*[@lang][position() = 1 and starts-with(concat(@lang, "-"), "fr-")]]
... 車輪の再実装ですがタグ名とクラス名を指定してDOM要素抽出を行なうという用途のための小さなコードを書きました。既存ライブラリを使わない小さなサイズのスクリプトでちょっと...
Greasemonkeyで、AutoPagerize対応のスクリプトを自作する時の注意点を2つメモ。 自分はひよっこですが、これからGreasemonkeyスクリプト書いてみようかなという人の助けに少しでもなれば嬉しいです。 継ぎ足されたページに適用する方法 AutoPagerizeで継ぎ足された部分に
の末尾に2020年11月30日時点の日本の首相のファミリーネーム(ローマ字で回答)を繋げて下さい。例えば「noda」なら、「2007-09-13_selector-to-xpath.trackbacknoda」です。これは機械的なトラックバックスパムを防止するための措置です。
writeback message: Ready to post a comment.