为什么我的XPath查询(抓取HTML表)只能在Firebug中工作,而不是我正在开发的应用程序?
这是为了给所有类似的问题提供一个规范的问答(但是有太多具体的问题可能成为一个关闭的目标候选人),每周一次或两次出现。
我正在开发一个应用程序,需要解析一个网站的表格。 由于派生网页的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]
在任何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>
标签。