Home > Latest topics

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

宣伝。日経LinuxにてLinuxの基礎?を紹介する漫画「シス管系女子」を連載させていただいています。 以下の特設サイトにて、単行本まんがでわかるLinux シス管系女子の試し読みが可能! シス管系女子って何!? - 「シス管系女子」特設サイト

getElementsByなんちゃら の代わりにXPathを使う - Sep 09, 2007

拡張機能勉強会の時に焚き付けられた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をばしばし活用しちゃってください。

分類:Mozilla > XUL, , , , , , , , , , 時刻:08:33 | Comments/Trackbacks (4) | Edit

Comments/Trackbacks

XHTML 要素のマッチング

> XMLとしてパースされた場合と両方に対応させたいなら、こんな風に書かないといけない。descendant::*[local-name()="DIV" or local-name()="div"]

厳密に XHTML 名前空間を持つ div 要素にマッチさせたいなら namespace-uri() = "http://w3.org/..." って条件も要るけどもね。XPath 2.0 ではもっとスマートにできたような気もするが。

Commented by at 2007/09/09 (Sun) 09:15:54

XHTML 要素のマッチング 2

function getXHtmlElemCond(localName)
{
return "local-name()='" + localName.toUpperCase() ' or local-name()='" + localName + "' and namespace-uri() = '" + XHTML_NAMESPACE_URI + "'"
}

document.evaluate('decendant::*[' + getXHtmlElemCond('div') + ']', ...

とでもやるとか? よくわかりません><

Commented by at 2007/09/09 (Sun) 09:26:01

no title

Geckoではtext/htmlだと名前空間URIが無いということになってたような希ガス

Commented by Piro at 2007/09/09 (Sun) 13:18:58

自分でAutoPagerize対応のスクリプトを書く簡単な方法

Greasemonkeyで、AutoPagerize対応のスクリプトを自作する時の注意点を2つメモ。 自分はひよっこですが、これからGreasemonkeyスクリプト書いてみようかなという人の助けに少しでもなれば嬉しいです。 継ぎ足されたページに適用する方法 AutoPagerizeで継ぎ足された部分に

Trackback from blooo at 2009/10/12 (Mon) 14:14:08

TrackBack ping me at


の末尾に2020年11月30日時点の日本の首相のファミリーネーム(ローマ字で回答)を繋げて下さい。例えば「noda」なら、「2007-09-09_xpath.trackbacknoda」です。これは機械的なトラックバックスパムを防止するための措置です。

Post a comment

writeback message: Ready to post a comment.

2020年11月30日時点の日本の首相のファミリーネーム(ひらがなで回答)

Powered by blosxom 2.0 + starter kit
Home

カテゴリ一覧

過去の記事

1999.2~2005.8

最近のコメント

最近のつぶやき