Hatena::ブログ(Diary)

ぶろぐ。@はてな Twitter

Web関係の技術記事中心のblog
2016年以降の記事は tkawaのはてぶろ。 に移行しました。

2009-02-06 (Fri)

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">&laquo; Googleニュースの新ブログパーツ(ウィジェット)</a> |
<a href="http://labs.cybozu.co.jp/blog/akky/">メイン</a>
</p>
<h2>2009&#24180;02&#26376;06&#26085;</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&#24180;02&#26376;06&#26085; 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]

シンプルに始点終点を同一レベルの条件で並べる。境界が含まれなくていいならこっちがいいね。

参考

*1script[1]でいいのかな?YQLではうまくいかない

nanto_vinanto_vi 2009/02/09 22:44 補集合(という言い方も変ですが)を考えるとうまくいくのではないでしょうか。
id("center")/div/node()[not(following-sibling::h2[following-sibling::h3] or preceding-sibling::p[@class = "posted" and a])]
(親要素、範囲の先頭の要素、範囲の末尾の要素の特定は id:os0x さんの書いた式[1]を参考にしました。)
[1] http://d.hatena.ne.jp/os0x/20090209/1234160617

tkawatkawa 2009/02/10 10:29 なるほど、境界が含まれないなら逆転の発想ですね。
本文に追記しました。ありがとうございます。

スパム対策のためのダミーです。もし見えても何も入力しないでください
ゲスト


画像認証