uhyohyo.net

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

十章第四回 述語による絞り込み

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

今回は述語(Predicates)について解説します。

述語とは

今まで、ロケーションステップはchild::pのように述語とノードテストによって記述してきました。軸で方向を絞り込み、ノードテストでノードの種類(や要素名)を絞り込んできたのです。

といっても、ノードテストによる絞り込みでは不十分で、もっと細かい条件を指定したい場合もあることでしょう。そういったときに使うのが述語です。

述語の書き方は、/html/body/child::p[1]のように[ ]で囲んだ式をくっつけます。

述語が数値の場合

この場合、式といってもただの数値です。

述語が数値の場合、絞りこまれて候補に上がったノードのうち、その番号番目のもの1つに絞られます。この順番は文書順です。

つまり、今回の場合は「1番目」ということになります。配列などと違い、1番目が一番最初なので注意しましょう。

つまり、この式は、body要素の子のp要素のうち一番最初のp要素の1つだけからなるノードセットを表すというわけです。

また、次のような書き方もあります。

/html/body/p[last()]

ここで関数が出てきました。この関数lastは、ノードセットの一番最後の番号を返すというものです。つまり、これはbodyの子のうち一番最後のp要素を表しているということです。

他にも、/html/body/p[last() - 1]のように、演算子-を使って、「一番後ろの1つ前」を表したりもできます。

述語が真偽値の場合

述語は真偽値の場合もあります。この場合、絞りこまれて候補に上がったノードそれぞれに対して述語が評価され、述語がtrueになるものだけに絞りこまれます。

例えば、/html/body/p[position() <= 2]とすると、body要素の子のp要素のうち、position() <= 2を満たすノードのみが返ります。ここで、position関数は、そのノードの番号を返します。つまり、このXPath文はbody要素の子のp要素のうちposition()が2以下、すなわち1番目と2番目のノードのみを返します。

よくよく考えてみると、さっきの数値の場合も、/html/body/p[1]/html/body/p[position()=1]の省略形になっていることが分かります。

ここで、XPathでは、==ではなく=で等しいかどうか比較できることに注意しましょう。!=はXPathでも同じです。

属性による絞り込み

前回と今回説明したことを組み合わせると、属性による絞り込みをすることができます。例として、lang属性が"ja"であるp要素のみを集めるには、次のようにします。

/html/body/descendant::p[attribute::lang="ja"]

これはつまり、attribute::lang="ja"に当てはまるp要素のみに絞り込むということです。明らかに、attribute::langの部分は1つのロケーションステップから成る式です。「現在のノード」のlang属性を示すノードのノードセットを返します。

では、この場合の「現在のノード」とは何でしょうか。実は、述語の中の式を評価する場合、絞り込みの対象となっている各ノードが「現在のノード」となります。

つまり、/html/body/descendant::p[attribute::lang="ja"]の動作はこうです。まず/html/body/descendant::pまでの部分でp要素が列挙されます。そのそれぞれのp要素に対してattribute::langを求め、それが"ja"であるp要素のみ残します。結果として、p要素のうちlang属性が"ja"であるもののみ残ることになります。

なお、ここで=がノードセットと文字列の比較を行っており、暗黙のうちにノードセットが文字列に変換されています。

ただし、ノードセットと文字列の比較は特殊です。というのも、ノードセットに入っているノードは複数かもしれません。このような状況でノードセットと文字列を比較した場合、ノードセット中の各ノードが文字列に変換され、ひとつでも等しいものがあればtrueとなります。

今回の場合1つずつのp要素についてlang属性を処理しているので、ノードセットはノードが1つだけです。だから、結局lang属性が"ja"かどうか判定しているということです。

これを用いると、例えばattribute::node() = "ja"のような条件を書いた場合、「存在する属性のうちどれか1つでも"ja"ならtrue」のような使いどころのよくわからない条件になります。

余談ですが、属性ノードを文字列に変換すると属性の値になる一方、要素ノードなど木構造中に存在するノードを文字列に変換するときには、その子孫のテキストノードを全て連結した値になります。例えば、<p>abc<span>def</span>ghi</p>というp要素を文字列に変換した場合、"abcdefghi"となります。

属性による絞り込みの応用

ノードセットと文字列の比較を応用すると、「その要素に限らず祖先のノードのどれかのlang属性が"ja"であればOK」というような判定もできます。

/html/body/descendant::p[ancestor-or-self::*/@lang = "ja"]

述語の中でancestor-or-self軸が使われています。この軸は、自身とその親以上全てでした。ノードテストは*(全ての要素ノード)なので、p要素とその親全てのノードセットということになります。そして、次の@langはlang属性を取得します。つまり、ノードセットのノードそれぞれに対してlang属性が取得され、それらのノードセットができあがるということです。

そして、できあがった属性のノードセットと"ja"を比較しているので、lang属性の中に一つでも"ja"があればtrueになります。

ところで、今まで見てきた通り、ノードセットと文字列を=で比較すると、1つでも等しいものがあればtrueになります。これを、「全て等しければtrue、他はfalse」とするにはどうすればよいでしょう。

それは、次のようにします。

not(ノードセット != "文字列")

notは、JavaScriptでいうところの!のような関数で、trueならfalse、falseならtrueを返します。一方、!=は、=と同じで「一つでも違えばtrue」です。つまり、全て同じならfalseになるので、これをnotで反転してtrueにすればよいのです。

最後に、述語は次のように複数指定して、条件を複数もたせることができます。

/html/body/p[@lang="ja"][last()]

まず、ノードテストでbodyの子のpに絞りこまれ、次の述語でlang属性が"ja"のものに絞りこまれ、その次の述語で、絞りこまれたp要素のうち最後のp要素になります。

以上でXPathの解説は終わりですが、ここでは解説していない関数などもあります。興味がある方は詳しく調べてみてもよいでしょう。