十章第四回 述語による絞り込み
このページの最終更新日:
今回は述語(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の解説は終わりですが、ここでは解説していない関数などもあります。興味がある方は詳しく調べてみてもよいでしょう。