Sep 09, 2007

getElementsByなんちゃら の代わりにXPathを使う

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

W3CのDOMでは、要素ノード(およびそのリスト)を得る方法として以下の方法がある。

getElementById(aName)
IDをキーにして単一の要素ノードを得る。
getElementsByTagName(aTagName)
タグ名、要素名をキーにして要素ノードのリストを得る。
childNodes
子ノードのリスト。

本当はネームスペースを指定して検索する物もあるんだけど、ここでは割愛。

これら以外に、W3C DOMではないがこういうのもある。

getElementsByClassName(aClassName)
クラス名をキーにして要素ノードのリストを得る。WHATWGのWeb Applications 1.0で定義されており、Firefox 3で利用可能。
getElementsByAttribute(aName, aValue)
属性名と属性値をキーにして要素ノードのリストを得る。属性値として「*」を渡すとその属性を指定された要素全てを得る。FirefoxでXULドキュメントにおいて利用可能。

ただ、探したい要素ノードの条件が複雑な時は、これらを使って取得したノードリストをループ回して条件判断しないといけないし、そもそもこれらでは要素ノード以外は取得できない。そこで最近のJS界隈でよく使われているのが、XPathだ。

XPathとは、/html/descendant::li[@class="navigation"]という風な「式」でXMLノードを特定する技術だ。XPathの書き方を新たに憶える必要はあるが、これを使えば、複雑な条件に合致するノードのリストを一発で取得することができる。コードが簡潔になるのはいいことだし、FirefoxでもSafari 3でもOperaでも、普通にDOMとJavaScriptでごりごりやるのに比べて20倍以上高速に動作するという話もある。

XUL Tipsのページに書いてるけど、FirefoxではDOM3 XPathで提案されているXPath関係の機能が利用できる。詳しい解説はHawk's W3 Laboratoryの「DOMとXPathの連携」(サイトが消えてるので、インターネットアーカイブからどうぞ)を見て欲しい。リンク先では「Gecko用」と書かれてるけど、現在ではOperaとSafari 3でも利用できるようになっている。

面倒な説明を省くと、使い方はこんな感じ。


var nodes = document.evaluate('/descendant::*[@class="navigation"]',
              document, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
for (var i = 0, maxi = nodes.snapshotLength; i < maxi; i++)
{
  this.processNavItems(nodes.snapshotItem(i));
}

通常のノードリストと違って、全体の長さはlengthプロパティではなくsnapshotLengthプロパティで取得し、各項目は添字やitem()メソッドではなくsnapshotItem()メソッドで取得する。

evaluate()メソッドの引数には、XPath式、その式を評価する時のコンテキストノード、ネームスペースリゾルバー、結果の型、最後にnullを渡す。

第3引数のネームスペースリゾルバーというのは何かというと、要するにlookupNamespaceURI()というメソッドを持っていて、そこに何か文字列を投げるとそれに対応する名前空間URIの文字列を返す、という機能を持ったオブジェクトのこと。JavaScriptで実装するならこうなる。

var resolver = {
    lookupNamespaceURI : function(aPrefix)
    {
      switch (aPrefix)
      {
        case 'xul':
          return 'http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul';
        case 'html':
        case 'xhtml':
          return 'http://www.w3.org/1999/xhtml';
        case 'xlink':
          return 'http://www.w3.org/1999/xlink';
        default:
          return '';
      }
    }
}

なんでこういう物が必要かというと、/descendant::html:div[@class="foobar"]という風な式を評価する時に、名前空間接頭辞(この例ならhtml)に対応する名前空間URIを調べる(解決する)必要があるからだ。とはいえ、こういう名前空間を使った式を使うのでもなければ(属性値とか要素の順番とかくらいしか条件に使わないなら)、ネームスペースリゾルバーを使う必要はない。ということで、さっきの例でも第3引数にはnullを渡している。

第4引数の「型」。これは、やたらたくさんあってどう使い分けたらいいか大変困るのだけれども、僕の場合はXPathResult.ORDERED_NODE_SNAPSHOT_TYPEを使うことがとても多い。前述の例を見てもわかるとおり、結果の形がノードリストに似ている。なので、表題の通り「getElementsByなんちゃら の代わりにXPathを使う」には、これが一番使い方が似ていることになる。

第5引数は、本当はXPathResult型……つまりevaluate()の返り値と同じ型のオブジェクトを渡すとそれを再利用するらしいんだけど、試してみても特に何も起こらなかった(ぉぃ)ので、普通はnullでいい。

最後におまけとして、DOM3 XPathを使って具体的にどういう事ができるかという例をいくつか挙げてみる。

  • 無駄なセパレータだけを非表示にする
  • 「その要素が親要素の何番目の子であるか」を調べる→document.evaluate('preceding-sibling::*', node, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null).snapshotLengthでその要素より前にある要素の個数を取得できるので、つまり、この数値が「親要素から見た時のその要素の順番」である。
  • 「その要素のネストの深さ」を調べる→document.evaluate('ancestor::*', node, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null).snapshotLengthで「祖先要素の個数」が分かる。これを比較して「よりネストの深い方」という風な条件判断ができる。

基本的には、今までgetElementsByTagName()とかの方法でノードを取得して条件分岐でさらにフィルタリングしてたものが、もっと高速に簡単になる、ノードの取得とフィルタリングを一度に行えると考えるといい。XPath式で表現可能なフィルタリング条件に限って言えば、ぶっちゃけ、これさえあればDOM2 TraversalのTreeWalkerとかは存在価値がだいぶ減ってしまうと言えよう。

注意点も書いておく。

  • Geckoの場合、text/htmlな文書では要素名は全て大文字として解釈される(これはDOMの仕様による)。なので、XPath式の中でも大文字で要素名を書かないといけない。XMLとしてパースされた場合と両方に対応させたいなら、こんな風に書かないといけない。descendant::*[local-name()="DIV" or local-name()="div"]
  • 軸としてancestorやprecending-siblingなどを指定しても、取得できるノードのリストは文書順で並んでいる。例えば、あるリンクに対してancestor::*と指定しても、「親のli要素」「その親のul要素」「その親のdiv要素」……という順番でなく、「ルートのhtml要素」「その子のbody要素」「その子のdiv要素」……という順番で結果が帰ってくる。Geckoでは、これはevaluate()メソッドに渡す型の指定が何であっても変わらない。

というわけで、上記の点に気をつけてXPathをばしばし活用しちゃってください。

エントリを編集します。

wikieditish message: Ready to edit this entry.











拡張機能