uhyohyo.net

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

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

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

述語とは

今まで、

/html/body/child::p

のように、XPathの式を軸とノードテストで記述してきました。軸で大まかに絞り込み、ノードテストでさらに絞り込んできたのです。

といっても、ノードテストで絞り込めるのはせいぜい要素名とか属性名くらいです。もっと細かい条件を指定したい場合もあることでしょう。そういったときに使うのが述語です。

述語の描き方は、

/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]

というのがあります。ここで、position関数は、そのノードの番号を返します。つまり、position()が2以下、すなわち1番目と2番目のノードのみが当てはまるということです。

また、さっきの数値の場合も、/html/body/p[1]/html/body/p[position()=1]と同じだということができます。

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

属性による絞り込み

ノードのうち、属性による絞り込みをしたい場合は次のようにします。例として、lang属性が"ja"であるp要素のみを集めるには、次のようにします。

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

attribute::langはlang属性ですが、属性を取得するにはattribute軸を使うのでした。

attribute軸が使えるということは、当然他の軸も使えるのですが、それではどのノードを基準にするのでしょうか。

それはもちろん、ノードテストによって絞りこまれた各ノードです。つまり、

/html/body/p

によって絞りこまれたbodyの子のp要素それぞれについて、そのattribute::langを取得して、それが"ja"であればtrueになるので、結果としてbodyのp要素のうちlang属性が"ja"であるものが取得されます。

ここで、attribute::langで取得できるのは、Attrというノードであるということを前回解説しました。つまり、attribute::lang="ja"というのはノードセットと文字列を比較していることになります。

こういったときは、型変換が行われます。ノードセットと文字列を比較する場合、ノードセットを文字列に変換して比較します。型変換というのは、九章第七回でも登場した概念ですね。

それでは、ノードセットを文字列に変換するとはどういうことでしょう。実は、各ノードを一つずつ文字列に変換して、一つでも等しいものがあればtrueになります。ノードを文字列に変換するときには、その子孫のテキストノードを全て連結します。例えば、

<p>abc<span>def</span>ghi</p>

というp要素を文字列に変換した場合、"abcdefghi"となります。今回の場合属性ですが、属性の場合はその値そのままです。

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

これが役立つのは、次の場合です。今回は、その要素に限らず祖先のノードのどれかのlang属性が"ja"であればいいということにしてみます。

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

今回、まずp要素はdescendant軸です。つまり、bodyの子に限らずbody以下の全てのpから探すということです。

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

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

ところで、今まで見てきた通り、ノードセットと文字列を=で比較すると、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要素になります。