Re:Re:XPathの動作にまつわる試行錯誤
以前書いた記事(id:t_f_m:20110321:1301004931 XPathの動作にまつわる試行錯誤 - 近江在住)の内容を検証・仕様確認してくださった方がいらっしゃったので、この記事はそれへの返答となります。
tumblr:xkansanさんありがとうございます。
検証の要点
問題となったXPath、サイト構造とほぼ同様のテスト環境によるテスト。
この結果を見て疑問に思ったことは、
- id(“target-ul”)/li[2][substring-before(self::li/a/@href, “p”) = substring-before(preceding-sibling::li/a/@href, “n”)]/a という XPath で 「記事を読む」 の a ノードが選択されるのはなぜか
- id(“target-ul”)/li[3][substring-before(self::li/a/@href, “p”) = substring-before(preceding-sibling::li/a/@href, “n”)]/a という XPath でなにも選択されないのはなぜか
という2点。
Re: XPathの動作にまつわる試行錯誤 - xK.memo
問題の切り分け。それぞれの解題は以下。
空文字問題
1. について
これは、 li[2] の述語で空文字同士の比較が行われているため。 以下の2つの XPath とその結果を見比べればわかると思う。
分かってしまえば、こちらは至極単純。
substring-before(self::li/a/@href, "p")
を実行した結果も
substring-before(preceding-sibling::li/a/@href, "n")
を実行した結果も、共に空文字となり、空文字同士の比較は真。当然である。
仮にこれを[1][2]のような文字付加に依らず回避するならば、
id("target-ul")/li[substring-before(self::li/a/@href, "p")][substring-before(self::li/a/@href, "p") = substring-before(preceding-sibling::li/a/@href,"n")]/a
のように空文字そのものの真偽を判定してやれば良いだろうか。かなり冗長だが。
ノードと文字列
2. について
(中略)これまでは substring-before の中でノードセットが文字列に変換される際には1つのノードしかノードセットに含まれなかったのでノードセットの文字列化について特に触れなかったのだけれど、この場合のように複数のノードを含むノードセットはどのように文字列化されるのか。
これは http://www.w3.org/TR/xpath/#section-String-Functions に書いてある。
A node-set is converted to a string by returning the string-value of the node in the node-set that is first in document order.文書順で一番最初のノードの文字列値がノードセットの文字列値になるとある。 この場合にあてはめると
つまり、仕様に従ってノードから文字列(正しくはノードセット、ノード集合から文字列)に変換しているだけなので、別にバグというわけではない。何となく、文字列セットのようなものに変換されて、全て比較に回されるのではないかと思っていたのだが、世の中そんなに甘くなかった。
これを回避するための方策は
なのでノードセットを文字列化するときには、そのノードセットに1つのノードのみが含まれているようにするのがよいのでは。
とあるし、実際そうするしかない。
id("target-ul")/li[substring-before(self::li/a/@href, "p") = substring-before(preceding-sibling::li/a/@href,"n")]/a
の中の
preceding-sibling::li/a/@href
の部分が唯一のノードを返せばよい。少し冗長になるが、
preceding-sibling::li[contains(a/@href,"n")]/a/@href
として、一意に存在するノードを拾い上げれば良い。
対策
というわけで t_f_m さんが書こうとしていた XPath は、 id(“MainContent”)/div[@class=”pager”]/div/ul/li[substring-before(substring-after(self::li/a/@href,”/photos/”),”-p”) = substring-before(substring-after(preceding-sibling::li [1] /a/@href,”/news/”),”-n”) and substring-before(substring-after(self::li/a/@href,”/photos/”),”-p”) != “” ]/a みたいに書いたらいいのではないか、と思う。少し不格好かもしれないけれど。
あるいは、ページ構造によっては id(“MainContent”)/div[@class=”pager”]/div/ul/li [last()] [substring-before(substring-after(self::li/a/@href,”/photos/”),”-p”) = substring-before(substring-after(preceding-sibling::li [1] /a/@href,”/news/”),”-n”)]/a と書けるかもしれない。
『出来るだけマルチバイト文字をXPathとして使いたくない』という前提があるので、こういう書き方をせざるを得ないのだろうな、と思う。文字列も集合として取り扱えるのなら、ここまでトリッキーな書き方をしなくて済むのだろうけれど。まあ、この実装の利点も何となく分かるので、不満というほどでもない。
で、一意にノード同士を比較することが出来るなら元の問題もそこまで大きく騒がなくて済んだので、意味を失わない程度に書き換えるなら、自分はこうするかなあ、と。
id("MainContent")/div[@class="pager"]/div/ul/li[contains(a/@href,"photos")][substring-before(substring-after(a/@href,"/photos/"),"-p") = substring-before(substring-after(preceding-sibling::li[contains(a/@href,"news")]/a/@href,"/news/"),"-n")]/a
xkansanさんの書いてくれた例と大差はないが、自分なりのこだわりというヤツ。