ネストしている要素をXPathのpositionを使用して取得する

解りにくいタイトルですね。例えば以下のようなHTMLを考えます。

tableタグが3つありますが、そのうちの1つがネストしています(idがchildのtableタグ)
このネストしているtableタグをXPathでどうやって取得すれば良いかという話です。


  
    
item1-1
sub item1-1 sub item1-2
item2-1 item2-2

もちろん//table[@id=”child”]のようにidを指定すれば取得できるのですが、
//table[2]のようにインデックスを指定してネスト中のtableを取得したいというのが今回の趣旨です。

出発地点として上記のHTMLからScraperを使用してデータを取得してみました。

#!/usr/bin/perl

use strict;
use warnings;

use LWP::Simple;
use Web::Scraper;
use YAML;

my $url = 'http://location/of/html';
my $content = get($url);
my $scraper = scraper {
	process '//table', 'tables[]' => '@id'
};

my $res = $scraper->scrape($content);
print Dump $res;

すると以下のような結果になります。

---
tables:
  - parent
  - child
  - parent2

いい感じで全てのテーブルのidを取得できています。2番目に取得したいテーブルのidが入っているので、13行目を以下のように書き換えれば上手く動きそうです。

process '//table[2]', 'table' => '@id'

しかし思ったようには動きません。結果はネストしているtableではなく、ネストされていない一番下のtable(idがparent2)が取得されています。

---
table: parent2

XPathのインデックス(position関数)は1ベースですが、0ベースだったかもと以下を試してみました。

process '//table[1]', 'table' => '@id'

しかしこちらは想像通りの結果になります。

---
table: parent

しかしながら取りたいデータは//table[2]で取れるはずだとソースを追ってみたところ、XML::XPathEngineではpositionで指定したインデックスは同じ親に属するノードの中での順番という扱いだと言う事が解りました。//tableの場合は文書全体からすべてのtableタグが取得できるので//table[2]が全てのtableタグの中で2番目のデータで無ければならないと思いましたがそうではありませんでした。

まだ信じられないのでpythonで同様のコードを書いてみましたが、結果は同様です。
それでもまだ信じられないのでlibxml2を直接叩いてみたところ、やはり結果は同様でした。流石にlibxml2が仕様を間違えて実装してるはずが無いという事で、xpathの仕様を確認したところwikipediaに以下の記述がありました。

number position()
評価中のコンテクストノードの位置を数値で返す
兄弟ノードにおける位置

という事でココまで見てきた//table[2]の挙動は全く正しいわけです。

ではネストしている要素は取得できないのかと思ってRFCをもう少し詳しく読んでみると
軸にdescendantとdescendant-or-selfというのがある事が解りました。
//はdescendant-or-selfの省略形なので、駄目元でdescendantを試してみました。

process '//descendant::table[2]', 'table' => '@id'

するとなんと以下の結果が得られました。これが欲しい結果な訳ですが、ココまで半日掛かりました。

---
table: child

という訳で長々と書きましたが、まとめは以下になります。

  • 特定タグの文書全体の中で順番を指定する時はdescendant::を利用する。
  • 先にRFCとかを読んで仕様を確認しないと時間がなくなる。

でもRFCって未だに読めないんですよね。もっとチュートリアル的な位置付けのドキュメントが欲しいです。

4 thoughts on “ネストしている要素をXPathのpositionを使用して取得する”

  1. 同じ道を歩んで3時間でここにたどり着きました。
    ずいぶん時間を無駄にしたと思いましたが、半日に比べればマシですね。
    救われました、ありがとうございました。

  2. 通りすがり さん

    コメントありがとうございます。
    xpath便利ですけど、細かい挙動調べるのって結構コスト高いですよね〜
    何はともあれ、役に立ってよかったです。

  3. ・・・私は同じ道を歩んで1時間でここにたどり着きました。
    すぐにググっちゃうんで、RFC読むなんて尊敬しちゃいます。

    Rubyですが、、、nokogiriパーサで、

    table = doc.xpath(‘//table’).to_a
    puts table[15]

    で取得出来るには出来たのですが、なぜ、

    table = doc.xpath(‘//table[15]’).to_a

    だと取得できないのか、?????でした。

    おかげでスッキリしました。助かりました!

  4. そのばしのぎ さん

    コメントありがとうございます!
    僕も一度気になると分かるまでモヤモヤしてしまう感じです。
    毎度毎度、結構時間かかるんですけどやめられないです。。。

Leave a Reply

Your email address will not be published. Required fields are marked *