photo author:snj14 email: web:http://white.s151.xrea.com/blog/ home:Shizuoka,Japan about:blog,my outputs

特定のclass属性を持った任意の要素にマッチするXPath

2008/06/10 (since: 2008/02/11 )

結論

特定のclass属性を持った任意の要素にマッチするXPath(hogeは指定したいclass属性名)

//*[contains(concat(" ",normalize-space(@class)," "), " hoge ")]

特定の要素にしたい場合は適当に

div[contains(concat(" ",normalize-space(@class)," "), " hoge ")]

などとする.

概要

特定のclass属性を持った任意の要素にマッチするXPathというのはアドオンやUserJavaScript,スクレイピングの際にDOMノードを特定するために良く使いますが,XPathの書き方がマズイ人がたまにいます.普通に考えたらXPathはこうなります.

XPath1::

//*[@class="hoge"]

class属性は以下の引用部分に書かれているとおり,スタイルシート以外の目的で使っても良く,後述しますが複数の値を持つことも許されています.

The class attribute has several roles in HTML: * As a style sheet selector (when an author wishes to assign style information to a set of elements). * For general purpose processing by user agents.

しかし,XPath1では他のUserJavaScriptがclass属性を追加した時などに問題が起きます.(例えばcustomize googleというアドオンのWayBack Machineという機能も,今はどうかわかりませんが,2007-09-05時点ではLDRizeと一緒に使えないという問題がありました)

こうした問題が起きる原因は以下の2つです.

  1. class属性が複数の値を持てることを考慮していない
  2. white spaceがタブや改行を含むことを考慮していない

最終的には結論で示したXPathになるので大したことはありませんが,一応W3Cの原文の引用と一緒に説明します.

class属性は複数の値を持てる

Multiple class names must be separated by white space characters.

なので,XPathは以下のようになるかと思います.

XPath2

//*[contains(concat(" ",@class," "), " hoge ")]

XPath2の動きを説明すると,

  1. //で強制的にルート要素から,全ての子孫要素に対して探索を行います(XHRした結果に対して似たようなことをしたいときは.//にする必要がある)
  2. \*で全ての要素にマッチします
  3. []で1でマッチした要素(この場合は全ての要素)を更に絞り込みます
  4. @classは,例えばマッチした要素が<div class="foo hoge bar">なら "foo hoge bar" に置換されます
  5. concat(" ",@class," ")で文字列の結合を行います => " foo hoge bar "
  6. contains(" foo hoge bar ", " hoge ")で,1つ目の文字列の中に2つめの文字列が含まれるかのチェックを行います.
    1. concatで前後に半角スペースを結合したので,hogeが真ん中になくてもマッチします.

white spaceはタブや改行を含む

White space (spaces, newlines, tabs, and comments)
User agents should interpret attribute values as follows: * Replace character entities with characters, * Ignore line feeds, * Replace each carriage return or tab with a single space.

なので,XPathは以下のようになります.

XPath3

//*[contains(concat(" ",normalize-space(@class)," "), " hoge ")]

normalize-space

XPathの動きを説明する前にnormalize-spaceの調査.

The normalize-space function returns the argument string with whitespace normalized by stripping leading and trailing whitespace and replacing sequences of whitespace characters by a single space. Whitespace characters are the same as those allowed by the S production in XML.

日本語訳

normalize-space 関数は、引数に指定した文字列の空白文字を正規化して返す。つまり前後の空白文字を取り除き、連続する空白文字を1つの空白文字に置き換える。 空白文字は、XMLの S プロダクションで使用できるものと同じである。

S production in XMLってなんぞやってことでこれも調べる.

White Space [3] S ::= (#x20 | #x9 | #xD | #xA)+

確認(firebugで以下を実行)

// spaceとtabは普通に表示したら分からなかったのでキモい感じになってしまった
var chars = ["#x20","#x9","#xD","#xA"];
for(var i=0; chars.length>i; i++){
 chars[i].match(/#x([0-9A-F]+)/);
 var str = String.fromCharCode(parseInt(RegExp.$1,16));
 switch(str){
  case " ":
  console.log("space")
  break;
  case "\t":
  console.log("tab")
  break;
  default:
  console.log([str])
 }
}

// 実行結果
// space
// tab
// ["\r"]
// ["\n"]

なので,XPath3の動きは

  1. 途中まではXPath2と一緒
  2. normalize-space()で連続する空白文字()を半角スペースに置換,先頭と末尾の空白文字を削除
  3. 途中からもXPath2と一緒

case sensitive

class = cdata-list [CS]
CS:: The value is case-sensitive (i.e., user agents interpret "a" and "A" differently).

class属性の値はcase sensitive,つまり,大文字と小文字は別物なので,これの変換はいらない.

ちなみに,rel属性の値はcase insensitiveなので変換が必要(でもやり方が美しくない)で,しかも複数の値を持てる.(e.g. rel="friend met")