XPathの不便なところ
XPathの勉強。LDRFullFeedの仕様がXPathで本文を指定するというものだったので、HTML(XHTML)から必要な部分を抜き出すためにどのようなXPathが書けるかを考えてみる。
特に意味はないけどhttp://labs.cybozu.co.jp/blog/akky/archives/2009/02/interviewd-by-junior-high.htmlを例にしてみる。勝手に使ってすみません。
... <div id="center"> <div class="content"> <p align="right"> <a href="http://labs.cybozu.co.jp/blog/akky/archives/2009/02/google-news-widget.html">« Googleニュースの新ブログパーツ(ウィジェット)</a> | <a href="http://labs.cybozu.co.jp/blog/akky/">メイン</a> </p> <h2>2009年02月06日</h2> <h3>プログラマーになりたい中学生から取材を受けた</h3> <p>中学校の課題で「なりたい職業の人に会って、そのレポートを書く」というのがあるそうで、中学三年生からメールをもらい、サイボウズ・ラボの会議室でインタビューを受けた。</p> <p>...</p> <p>...</p> <p>...</p> <p>...</p> <p>...</p> <p>...</p> <p>...</p> <div id="a002149more"><div id="more"> </div></div> <p class="posted">投稿者 <a href="http://labs.cybozu.co.jp/blog/akky/">秋元</a> : 2009年02月06日 12:27</p> <script type="text/javascript"> ... </script> <script type="text/javascript" src="xxx"> </script> <h2 id="trackbacks">トラックバック</h2> <p class="techstuff">...</p> ... </div><!--/Content--> </div><!--/Center--> ...
どこを本文として抜き出すかというのもいろいろ考え方はあるが、div(content)の中の最初のpはナビゲーションなので無視してその次のh2から始まって、h3, p, p, p,...と取っていき、p(posted)が投稿者名なのでそこまで、ということにする。
XPathでこういう○○〜■■というような範囲を指定するにはイディオムみたいなものがあるらしく、こういうふうに書く:
//parent/start/following-sibling::node()[following-sibling::end]
following-sibling というのは兄弟ノードのうち後方にあるものを指す。start/following-sibling::node() でstartノードの兄弟ノードのうち後方にあるものすべて。さらに [following-sibling::end] で、それより後方にendノードがあること、という条件がつく。つまりこれで <start/>〜<end/> の内部を指すことができる。
しかし、このXPathの問題点はstartやendノードは含まれないということ。上記のソースにこれを適用してみると、startノードは //div[@class="content"]/p[@align="right"] という感じで指定できるが、endがどうも指定しづらい*1。まあ適用するソースの書き方にもよるんだけど、こういう不都合は意外と多いと思う。
加えて、XPathにstart, endと書いてあるのに範囲に含まれないというのはどうもわかりやすい書き方とはいえない。
子孫ノードを示す descendant には自身も含む descendant-or-self という書き方ができるのだから、兄弟ノードにも自身を含む following-sibling-or-self という書き方を許してくれれば、こう書くことができる:
//parent/start/following-sibling-or-self::node()[following-sibling-or-self::end]
まあそもそももっとわかりやすい形で範囲指定ができる構文を導入した方がいいのかもしれないけど。
追記
scriptを除外する条件をつけたらこの場合はうまく取れた。
//div[@class="content"]/p[@align="right"]/following-sibling::node()[name()!="script"][following-sibling::h2[@id="trackbacks"]]
さらに追記
いろいろ反応していただいたようでありがとうございます。そこで挙げられたXPathは以下の通り。
id:nanto_vi氏
id("center")/div/node()[not(following-sibling::h2[following-sibling::h3] or preceding-sibling::p[@class = "posted" and a])]
境界が含まれないなら補集合を指してnotを取ればいいというアイデア。なるほど。
id:os0x氏
id("center")/div/*[preceding-sibling::*[following-sibling::h2] and self::*[not(preceding-sibling::p[@class="posted" and a])]]
これは補集合のアイデアに近い。
id("center")/div/*[self::h2[following-sibling::h3] or (preceding-sibling::h2 and following-sibling::p[@class="posted" and a]) or self::p[@class="posted" and a] ]
これは、境界が含まれないなら境界だけを別に明示してやればいいというアイデア。長くなるけどわかりやすいですね。
というか、僕が最初にイディオムと言って書いたXPath、こうしたほうが断然わかりやすいじゃん。
//parent/node()[preceding-sibling::start and following-sibling::end]
シンプルに始点終点を同一レベルの条件で並べる。境界が含まれなくていいならこっちがいいね。