web-dev-qa-db-ja.com

XPathクエリ(HTMLテーブルのスクレイピング)がFirebugでのみ機能し、開発中のアプリケーションでは機能しないのはなぜですか?

これは、週に1〜2回ポップアップする同様の(ただし、具体的な質問が多すぎてターゲット候補にはならない)すべてに正規のQ&Aを提供することを目的としています。

テーブルを含むWebサイトを解析する必要があるアプリケーションを開発しています。 WebページをスクレイピングするためのXPath式を導出するのは退屈でエラーが発生しやすい作業なので、これにはFirebugのXPath抽出機能(または他のブラウザーの同様のツール)を使用したいと思います。

入力例は次のようになります。

<!-- snip -->
<table id="example">
  <tr>
    <th>Example Cell</th>
    <th>Another one</th>
  </tr>
  <tr>
    <td>foobar</td>
    <td>42</td>
  </tr>
</table>
<!-- snip -->

最初のデータセル(「foobar」)を抽出したい。 FirebugはXPath式を提案します

//table[@id="example"]/tbody/tr[2]/td[1]

which XPathテスタープラグインでは正常に機能しますが、自分のアプリケーションでは機能しません(結果が見つかりません)。クエリを//table[@id]に切り詰めると、再び機能します。

何が問題なのですか?

20
Jens Erat

問題:DOMには_<tbody/>_タグが必要です

Firebug、Chromeの開発者ツール、JavaScriptのXPath関数などは、基本的なHTMLソースコードではなく、[〜#〜] dom [〜#〜]で動作します。

HTMLのDOMでは、フッターのテーブルヘッダー(_<thead/>_、_<tfoot/>_)に含まれていないすべてのテーブル行がテーブル本体タグ_<tbody/>_に含まれている必要があります。したがって、(X)HTMLの解析中にタグが欠落している場合、ブラウザーはこのタグを追加します。たとえば、 MicrosoftのDOMドキュメント

テーブルでtbody要素が明示的に定義されていない場合でも、tbody要素はすべてのテーブルで公開されます。

stackoverflowに関する別の回答の詳細な説明 があります。

一方、 HTMLでは必ずしもそのタグを使用する必要はありません

TBODY startタグは、テーブルにテーブル本体が1つだけ含まれ、テーブルの頭または足のセクションがない場合を除いて、常に必要です。

ほとんどのXPathプロセッサは生のXMLで動作します

JavaScriptを除いて、ほとんどのXPathプロセッサはDOMではなく生のXMLで動作するため、_<tbody/>_タグを追加しません。また、 tag-souphtmltidy などのHTMLパーサーライブラリは、「DOM-HTML」ではなくXHTMLのみを出力します。

これは、PHP、Ruby、Python、Java、C#、Google Docs(Spreadsheets)などのStackoverflowに投稿された一般的な問題です。 Seleniumはブラウザ内で実行され、DOMで動作するため、影響を受けません!

問題の再現

Firebug(またはChromeの開発ツール)によって表示されるソースを、右クリックして[ページソースを表示](またはブラウザーで呼び出されるもの)を選択するか、コマンドで_curl http://your.example.org_を使用して表示されるソースと比較します。ライン。後者にはおそらく_<tbody/>_要素が含まれていません(それらはめったに使用されません)。Firebugは常にそれらを表示します。


解決策1:_/tbody_軸ステップを削除する

行き詰まっているテーブルに本当に_<tbody/>_要素が含まれていないかどうかを確認します(最後の段落を参照)。もしそうなら、おそらく別の種類の問題があります。

次に、_/tbody_軸ステップを削除して、クエリが次のようになるようにします。

_//table[@id="example"]/tr[2]/td[1]
_

解決策2:_<tbody/>_タグをスキップする

これはかなり汚い解決策であり、ネストされたテーブルでは失敗する可能性があります(内部テーブルにジャンプする可能性があります)。非常にまれなケースでのみこれをお勧めします。

_/tbody_軸ステップを子孫または自己ステップに置き換えます。

_//table[@id="example"]//tr[2]/td[1]
_

解決策3:_<tbody/>_タグの有無にかかわらず両方の入力を許可する

テーブルを事前に確認できない場合、または「HTMLソース」とDOMコンテキストの両方でクエリを使用する場合。ソリューション2のハックを使用したくない/使用できない場合は、代替クエリ(XPath 1.0の場合)を提供するか、「オプションの」軸ステップ(XPath 2.0以降)を使用します。

  • XPath 1.
    _//table[@id="example"]/tr[2]/td[1] | //table[@id="example"]/tbody/tr[2]/td[1]_
  • XPath 2.//table[@id="example"]/(tbody, .)/tr[2]/td[1]
41
Jens Erat

同じ問題に遭遇したばかりです。すべてのtbodyタグが存在するかどうかをチェックし、そのようにdomをトラバースする再帰関数をほとんど作成しました。その後、正規表現を知っていることを思い出しました。 :)

解析する前に、htmlを文字列として取得します。不足している<tbody>タグと</tbody>タグを正規表現で挿入し、それをDOMDocumentオブジェクトにロードし直します。

Jens Eratが良い説明をしていますが、ここにあります

解決策4:HTMLソースに常に正規表現付きの<tbody>タグがあることを確認します

JavaScript
    var html = '<html><table><tr><td>foo</td><td>bar</td></tr></table></html>';
    html.replace(/(<table([^>]+)?>([^<>]+)?)(?!<tbody([^>]+)?>)/g,"$1<tbody>").replace(/(<(?!(\/tbody))([^>]+)?>)(<\/table([^>]+)?>)/g,"$1</tbody>$4");

PHP
    $html = $dom->saveHTML();
    $html = preg_replace(array('/(<table([^>]+)?>([^<>]+)?)(?!<tbody([^>]+)?>)/','/(<(?!(\/tbody))([^>]+)?>)(<\/table([^>]+)?>)/'),array('$1<tbody>','$1</tbody>$4'),$html);
    $dom->loadHTML($html);

正規表現だけ:

matches `<table>` tag with whatever else junk inside the tag and between this and the next tag if the next tag is NOT `<tbody>` also with stuff inside the tag

    /(<table([^>]+)?>([^<>]+)?)(?!<tbody([^>]+)?>)/

replace with

    $1<tbody>

the $1 referencing the captured `<table>` tag with contents.
Do the same for the closing tag like this:

    /(<(?!(\/tbody))([^>]+)?>)(<\/table([^>]+)?>)/

replace with

    $1</tbody>$4

このようにして、domには常に必要に応じて<tbody>タグが付けられます。

2
Peter Rakmanyi