uhyohyo.net

JavaScript初級者から中級者になろう

十章第二回 DOMでのXPathの利用

このページの最終更新日:

DOMでXPathを使う

いよいよ今回、DOMでXPathを使う方法を紹介します。

ここで出てくるのが、documentが持つevaluateというメソッドです。

これは、5個の引数を渡してXPathを処理してもらい、結果をXPathResultというオブジェクトで返すものです。

具体的なサンプルを見てみましょう。


var result = document.evaluate('/html', document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null);

ここでは、/htmlという単純なXPath文を文字列として第一引数に渡しています。evaluateはこのXPath文を処理して結果を返します。

それでは引数の解説ですが、第一引数は既に述べたようにXPath文を文字列で渡します。

つぎに、第二引数にはdocumentが渡されています。これは、ルートノードです。すなわち、(このノードも含めて)このノードより下で探索するということです。documentということは、木構造全部ということですね。基本はdocumentで事足りるでしょう。

次は第三引数で、これは今nullとなっていますが、ここには名前空間解決器(後述)を渡します。不要な場合はnullです。

第四引数は結果のタイプです。今回はXPathResult.FIRST_ORDERED_NODE_TYPEです。これは明らかに定数ですね。当然他にもあるのですが、各定数の解説は後述とします。今回指定した定数は、結果のノードセットのうち最初の1つを取得するというものです。

次に第五引数はnullとなっていますが、これは再利用するオブジェクトです。この引数に以前のevaluateで結果として返ってきたXPathResultオブジェクトを渡すと、そのオブジェクトに結果が書き込まれます。すなわち、結果のオブジェクトを再利用できるのです。どういうことかというと、通常document.evaluateで結果を得るときは新しいXPathResultが作られて結果として返されます。つまり、1回実行するごとに新しいXPathResultオブジェクトがどんどんできるのです。これは無駄だということで、この第五引数に使い終わったXPathResultオブジェクトを渡すと、新しいXPathResultを作らずに渡されたオブジェクトを書き換えてそれを返してくれます。繰り返しXPathを利用する場合はこれを活用するのがよいかもしれません。

では肝心の、結果のXPathResultからノードを得る方法を解説します。これは結果タイプによって変わってきます。詳しくは後述しますが、今回の場合(第四引数にXPathResult.FIRST_ORDERED_NODE_TYPEを指定した場合)はresult.singleNodeValueで該当するノードが取得できます。試してみましょう。


<!doctype html>
<html>
  <head>
    <title>test</title>
  </head>
  <body>
    <p>test</p>
    <script type="text/javascript">
      var result = document.evaluate('/html', document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null);

      console.log(result.singleNodeValue);
    </script>
  </body>
</html>

実行するとhtml要素がログに表示されます。つまり、このXPath文の結果としてhtml要素が取得できたということです。

結果タイプ

それでは、結果タイプの解説をします。結果タイプによって、XPathResultオブジェクトから結果のノードを取得する方法が変わります。

結果タイプは定数で、XPathResultのプロパティとして利用できます。

結果タイプにはいくつかの種類がありますが、それぞれについてORDEREDUNORDEREDのバージョンがあります。ORDEREDは、ノードが文書順(最初から順番に並んでいる)になっています。対してUNORDEREDの場合、並び順は適当です。ORDEREDの場合かならず文書順に探さないといけませんが、UNORDEREDの場合ブラウザが探しやすい順番で探せるので、UNORDEREDのほうが少し速い可能性があります。

ANY_UNORDERED_NODE_TYPE, FIRST_ORDERED_NODE_TYPE

この2つの結果タイプは、ノードを1つだけ取得します。というのも、前回解説したようにXPath文の結果はノードセットであり、そこには複数のノードが含まれているかもしれません(1つかもしれませんが。その中の1つだけに興味がある場合(もしくは、最初から結果のノードが1つだけと分かっている場合)は、この結果タイプを使うのがよいでしょう。

UNORDEREDの場合は結果の順番が不定なので、ノードセットがノードを複数含んでいる場合その中のどれが出てくるか分かりません。ORDEREDの場合、文書順で探して最初なので、必ず最初に見つかったノードが結果となります。

そして、結果の取得方法は、結果のsingleNodeValueプロパティにノードが入っています。

UNORDERED_NODE_SNAPSHOT_TYPE, ORDERED_NODE_SNAPSHOT_TYPE

この2つは、ノードセットに含まれるノードを全て取得できます。

この結果タイプを用いる場合、XPathResultオブジェクトのsnapshotLengthに結果のノードの個数が入っています。そして、結果のノードには0から番号がついていて、snapshotItemというメソッドに数値を渡すことでその番号のノードが返ってきます。

すなわち、複数のノードを処理するには例えば次のようにします。


      //resultは結果のXPathResultオブジェクト
      for(var i=0; i<result.snapshotLength; i++){
        var node = result.snapshotItem(i);
        //得られたnodeに対して処理をする
        console.log(node);
      }
    

なお、名前のSNAPSHOTというのは、evaluateメソッドを実行した時点でノードの探索を全て終えるということを意味しています。

UNORDERED_NODE_ITERATOR_TYPE, ORDERED_NODE_ITERATOR_TYPE

これらも複数のノードを全て扱える方法ですが、直前の2つとは異なりevaluateを実行した時点では全探索の処理が行われません。その代わりに、次のノードが必要になるたびにその都度探索するという方法をとります。

具体的には、XPathResultオブジェクトのiterateNextメソッド(引数なし)を呼び出すことでその都度次のノードを探索して返してくれます。もう無い場合はnullになります。すなわち、全てのノードを処理するには次のようにします。


      //resultは結果のXPathResultオブジェクト
      var node;
      while(node = result.iterateNext()){
        //得られたnodeに対して処理をする
        console.log(node);
      }
    

実はまだいくらか結果タイプがありますが、それはあとで説明します。

さて、SNAPSHOT系とITERATOR系の違いについて改めて説明します。前者は最初にノードの探索を全て終えると述べました。つまり、巨大な木構造に対してSNAPSHOT系でXPath文を探索すると完了まで時間がかかる可能性があります。一方、後者(ITERATOR系)ではevaluate自体は一瞬で終了します。なぜなら、実際に結果のノードを要求される(iterateNext()が呼ばれる)までは探索をさぼるからです。その代わり後者は一回のiterateNext()ごとに探索を行うためsnapshotItem()を用いたノードの取得よりは時間がかかります。つまり、この2つの違いは探索時間をいつかけるかが主です。

ただ、実際的な問題としてもう1つSNAPSHOT系とITERATOR系には違いがあります。それは、evaluateメソッドが呼ばれてXPathResultが生成されたあとに木構造が変更された場合の挙動の違いです。

SNAPSHOT系は、XPathResultが返された時点でもう探索が終了して結果が確定しているので、その後文書に変更があった場合も影響を受けず、XPathResultの結果を利用できます。

一方ITERATOR系は、XPathResultが返されたあとに探索が進みます。なので、探索の合間(つまり、iterateNext()が呼ばれる合間)に木構造が変更されてしまうと探索結果が狂ってしまう恐れがあります。したがって、ITERATOR系を指定した場合のXPathResultは木構造が変更されたら即座に無効となります(探索を続けることはできなくなり、iterateNextでノードを取得しようとするとエラーになります)。

SNAPSHOT系とITERATOR系の2つは状況に応じて使い分けるのがよいでしょう。

サンプル

XPathの話は込み入っているので、具体的なサンプルを用意してみました。単純なものですが、いろいろ自分で工夫するなどしてみましょう。


<!doctype html>
<html>
  <head>
    <title>test</title>
  </head>
  <body>
    <p id="test">test</p>
    <p id="test2">test2</p>
    <p id="test3">test3</p>
    <script type="text/javascript">
      var result = document.evaluate('/descendant::p', document, null, XPathResult.ORDERED_NODE_ITERATOR_TYPE, null);

      var node;
      while(node=result.iterateNext()){
        console.log(node);
      }
    </script>
  </body>
</html>

ここで用いられているXPath文は全てのp要素を取得するというものです。そして、タイプはORERED_NODE_ITERATORなので、このようにwhile文で処理しています。今回は、得られたノードをとりあえずconsole.log表示しています。3つのp要素が全て表示されるのが分かるはずです。さらに、ORDEREDな結果タイプを用いたのでtest,test2,test3の順番で表示されるはずです。UNORDEREDにすると順番が変わるかもしれないし変わらないかもしれません。

名前空間解決器について

今回の最後に、さっき説明を先送りにした名前空間解決器について説明します。

実はXPathはXML(やXHTML)でも使用できます。その際、名前空間を考慮する必要が出てきます。その時に必要となるのが名前空間解決器です。HTMLのように名前空間の無い文書ではこれはnullとして構いません。以下に書いてある名前空間解決器の説明は名前空間がある文書を扱う場合にのみ必要で、少しマニアックです。興味がない方は読み飛ばして構いません。

さて、前に紹介した通り、タグには <xhtml:html> のように名前空間接頭辞がついていることがありました。ところが、この接頭辞が名前空間の実体というわけではなく、実体はURI(http://www.w3.org/1999/xhtmlなど)なのでした。

XPathにおいても、例えば/xhtml:html/descendant-or-self::xhtml:pのように要素名のノードテストに対して名前空間接頭辞を付けることができます。そうなると、例えばxhtmlという名前空間接頭辞がどの名前空間を指すかをなんとかして判断する必要があることになります。XHTMLでは、これはxmlns:xhtmlという属性により設定することができました。

残念なことにXPathにはこのxmlns:xhtml属性を読んで名前空間接頭辞を名前空間(URI)に関連付ける機能がありません。恐らくそんなことのために無駄な探索をしたくないのだと思います。また、後述のカスタム名前空間解決器を使いたいという需要に応える形にもなっています。

そこで、接頭辞を名前空間に対応付ける仕組みをXPathの処理器(document.evaluate)に提供してあげる必要があります。これが名前空間解決器なのです。

それでは、この名前空間解決器が必要なときはどうやって作ればよいのでしょうか。実は、簡単な文書の場合は名前空間解決器を自動的に作ってくれるメソッドがあります。それは、document.createNSResolverです。このメソッドは引数としてノードを1つとります。すると、そのノードが知っている名前空間に対応した名前空間解決器が返り値として出てきます。

例えば、このようなXHTML文書を考えます(XHTML文書なので、保存してブラウザで表示するときは拡張子を.xhtmlとしなければなりません)。


  <?xml version="1.0" ?>
  <xhtml:html xmlns:xhtml="http://www.w3.org/1999/xhtml">
    <xhtml:head>
      <xhtml:meta charset="UTF-8" />
      <xhtml:title>test</xhtml:title>
    </xhtml:head>
    <xhtml:body>
      <xhtml:p>test</xhtml:p>
      <xhtml:script>
        var resolver = document.createNSResolver(document.documentElement);
        console.log(resolver.lookupNamespaceURI("xhtml")); // "http://www.w3.org/1999/xhtml"
      </xhtml:script>
    </xhtml:body>
  </xhtml:html>

この文書中のscript要素では、document.createNSResolverdocument.documentElementを引数として呼び出しています。これはルート要素であるhtml要素を指していますね。こうすると結果の名前空間解決器はhtml要素が知っている名前空間の情報を持っています。html要素にはxmlns:xhtml属性があり、xhtmlという名前の名前空間が定義されています。そのため、html要素はxhtmlという名前空間接頭辞を知っていることになります。

したがって、createNSResolverによって作られた名前空間解決器はxhtmlという名前空間接頭辞を知っています。このことは次の行で名前空間解決器のlookupNamespaceURIメソッドを呼ぶことで確かめられています。このメソッドは、名前空間接頭辞を引数に渡すと対応する名前空間を返すメソッドです。知らない場合はnullとなります。

createNSResolverの弱点は、引数に渡したノードが知っている名前空間接頭辞しか解決できない点です。例えば、次のように変更したサンプルを見てください。


  <?xml version="1.0" ?>
  <foo:html xmlns:foo="http://www.w3.org/1999/xhtml">
    <foo:head>
      <foo:meta charset="UTF-8" />
      <foo:title>test</foo:title>
    </foo:head>
    <xhtml:body xmlns:xhtml="http://www.w3.org/1999/xhtml">
      <xhtml:p>test</xhtml:p>
      <xhtml:script>
        var resolver = document.createNSResolver(document.documentElement);
        console.log(resolver.lookupNamespaceURI("xhtml")); // null
        console.log(resolver.lookupNamespaceURI("foo"));   // "http://www.w3.org/1999/xhtml"
      </xhtml:script>
    </xhtml:body>
  </foo:html>

これは、body要素の中と外で使う名前空間接頭辞を返るというなかなかふざけたサンプルです。こうすると、html要素はfooという名前空間は知っているものの、それより下のbody要素で定義されたxhtmlという名前空間は知らないという状態になります。よって、html要素から作られた名前空間解決器はxhtmlという接頭辞を解決することができません。

この問題を解決したい場合は十分に深い位置のノードを指定する必要があります。今回の場合、document.doculemtElementの代わりにdocument.bodyをcreateNSResolverに渡せばうまくいきますが、そのような解決法はナンセンスです。

つまり、ルート要素で全ての名前空間接頭辞が定義されるような単純な文書ならばdocument.createNSResolverを使うことができるものの、そうでない場合は有効ではありません。document.createNSResolverを使う場合はdocument.evaluateの呼び出しはたとえば次のようにすればよいでしょう。


var result = document.evaluate('/html', document, document.createNSResolver(document.documentElement), XPathResult.FIRST_ORDERED_NODE_TYPE, null);

カスタム名前空間解決器

さて、複雑な場合でも思い通りに名前空間を解決するには、カスタム名前空間解決器を作る必要があります。つまり、document.createNSResolvr任せにせずに自分で名前空間解決器を作るということです。

とはいえ、これはそんなに難しくありません。カスタム名前空間解決器は関数です。引数として名前空間接頭辞を受け取り、それに対応する名前空間を返す関数を作ればそれがカスタム名前空間解決器になります。

例えば上のサンプルに対応する名前空間解決器は次のように作れます。なお、知らない名前空間の場合はnullを返します。


function(prefix){
  if(prefix === "xhtml" || prefix === "foo"){
    return "http://www.w3.org/1999/xhtml";
  }else{
    return null;
  }
}

この関数を使うと、上の文書でも正しくXPathでノードを検索できるでしょう。


  <?xml version="1.0" ?>
  <foo:html xmlns:foo="http://www.w3.org/1999/xhtml">
    <foo:head>
      <foo:meta charset="UTF-8" />
      <foo:title>test</foo:title>
    </foo:head>
    <xhtml:body xmlns:xhtml="http://www.w3.org/1999/xhtml">
      <xhtml:p>test</xhtml:p>
      <xhtml:script>
        var resolver = function(prefix){
          if(prefix === "xhtml" || prefix === "foo"){
            return "http://www.w3.org/1999/xhtml";
          }else{
            return null;
          }
        };
        var result = document.evaluate('/xhtml:html/foo:body/foo:p', document, resolver, XPathResult.FIRST_ORDERED_NODE_TYPE, null);
        console.log(result);
      </xhtml:script>
    </xhtml:body>
  </foo:html>

お気づきの通り、名前空間接頭辞は名前空間を示すための道具にすぎませんから、文書中の接頭辞(xhtml:body)とXPath中の接頭辞(foo:body)が違っていても、同じ名前空間なら問題なくマッチします。

なお、XPathではデフォルト名前空間の扱いには注意が必要です。XPathでは、/html/body/pのように名前空間接頭辞がない要素名をノードテストとして与えた場合、名前空間がないノードにしかマッチしません。つまり、名前空間を持つ要素にマッチさせたい場合は、ノードテストに名前空間接頭辞を書いてあげる必要があります。特に、XML文書が全てデフォルト名前空間を用いて書かれているときは自分のオリジナルの名前空間接頭辞、そしてそれに対応した名前空間解決器を用意する必要があります。