为什么我的XPath查询(抓取HTML表)只能在Firebug中工作,而不是我正在开发的应用程序?

这是为了给所有类似的问题提供一个规范的问答(但是有太多具体的问题可能成为一个关闭的目标候选人),每周一次或两次出现。

我正在开发一个应用程序,需要解析一个网站的表格。 由于派生网页的XPath表达式是无聊和容易出错的工作,我想使用FirebugXPath提取器功能 (或其他浏览器中的类似工具)。

示例输入如下所示:

<!-- 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] 

在任何XPath测试器插件中工作正常,但不是我自己的应用程序(未找到结果) 。 如果我减少查询//table[@id] ,它再次工作。

怎么了?

问题:DOM需要<tbody/>标签

Chrome的开发工具Firebug,JavaScript中的XPath函数和其他工作在DOM上 ,而不是基本的HTML源代码

HTML的DOM要求所有不包含在页脚表头( <thead/><tfoot/> )中的表格行包含在表格主体标签<tbody/> 。 因此,如果浏览器在解析(X)HTML时缺少该标记,则会添加此标记。 例如, 微软的DOM文档说

即使表没有显式定义一个tbody元素, tbody元素也暴露给所有的表。

有关于stackoverflow的另一个答案有一个深入的解释 。

另一方面, HTML不一定需要使用该标签 :

TBODY开始标记始终是必需的,除非表只包含一个表体和没有表头或脚部分。

大多数XPath处理器使用原始XML

不包括JavaScript,大多数XPath处理器都使用原始XML,而不是DOM,因此不会添加<tbody/>标记。 此外,HTML解析器库(如标签汤和htmltidy)仅输出XHTML,而不是“DOM-HTML”。

这是一个在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.0
    //table[@id="example"]/tr[2]/td[1] | //table[@id="example"]/tbody/tr[2]/td[1]
  • XPath 2.0//table[@id="example"]/(tbody, .)/tr[2]/td[1]

刚刚遇到同样的问题。 我几乎写了一个递归函数来检查每个tbody标记是否存在并遍历dom,然后我记得我知道正则表达式。 🙂

解析之前,获取一个字符串的HTML。 用正则表达式插入缺少的<tbody></tbody>标记,然后将其加载回到DOMDocument对象中。

简斯·埃拉特给出了一个很好的解释,但这里是

解决方案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>标签。