当前位置: 首页 > 科技观察

XPath遍历dom的工具并不容易

时间:2023-03-14 12:17:46 科技观察

我第一次接触XPath是在2007年,但最近才对它产生兴趣。我以前大部分时间都试图避免它,当我不得不尝试时,每次都失败了。那时XPath对我来说并没有真正的意义。但是后来我遇到了一个特殊的解析问题(对于CSS选择器来说太复杂了,手工编码太容易了),所以我决定再试一次XPath。令我惊喜的是,这确实有效并且很有用。以下是我的个人经历。我遇到的问题。假设您管理一个歌词网站。为了保持一致的阅读体验,您想收集每行歌词的第一个单词。如果歌词是以纯文本格式保存的,可以直接使用下面的代码来实现。lyrics.gsub!(/^./){|character|character.upcase}但是如果把歌词保存成html格式就没那么简单了,因为dom结构本身没有“行”的概念,所以没有办法使用简单的A正则表达式来识别行。所以我们要做的第一件事就是定义dom结构中什么是“行首”,这里有两个简单的例子:

标签中第一个文本节点
之后的第一个文本节点看起来像这样:

Thisisthebeginningofaline.Thisistoo。

但除此之外,我们可能还必须处理嵌套的内联元素:

Thisisthebeginningofaline。Thisisnot。

一般解决方案我想到的第一个解决方案是用Ruby写一个方法,扫描dom的所有相关部分并递归地找到所有符合条件的节点。使用了几个轻量级的css选择器:yieldfirst_text_node(br.next)}enddeffirst_text_node(node)ifnode.nil?thenilelsifnode.text?thennodeelsifnode.children.any?thenfirst_text_node(node.children.first)endend这是一个比较合理的方案,但是11行代码好像有点长有点杀鸡用牛刀的感觉,仅仅为了获取dom节点而使用Ruby迭代器和条件语句感觉有点不对。应该有更好的方法吧?最后,回到主题(XPath)XPath可能由于一些原因而令人困惑。第一点,网上几乎没有什么可以参考的(W3Schools!想都别想)。RFC已经是我找到的最好的文档。第二点是XPath看起来有点像CSS。方法名称中有“路径”,所以我总是假设XPath表达式中的/和CSS选择器中的>表示相同的东西。document.xpath('//p/em/a')==document.css('p>em>a')其实XPath表达式包含很多简写形式,如果我们想搞清楚上面代码运行时会发生什么这些速记必须弄清楚什么。下面是相同表达式的完整拼写:/descendant-or-self::node()/child::p/child::em/child::a/这个XPath表达式和上面的CSS选择器效果是一样的,但不是我之前假设的方式。XPath表达式由一个或多个以/分隔的位置步骤组成。表达式中的第一个/表示文档的根节点。每个定位步骤都表示匹配到的节点,传递三个信息:我要从当前位置移动到哪里?答案是Axis,它是可选的。默认轴是child,意思是“当前选中节点的所有子节点”。在上面的例子中,descendant-or-self是第一个定位部分的轴,意思是“所有当前选中的节点及其所有的后代节点”。XPath规范中定义的大多数轴都具有语义名称,如“descendant-or-self”。我想选择什么类型的节点?选择由节点测试指定,它是每个定位步骤的组成部分。在我们之前的例子中,node()匹配所有类型;text()匹配文本节点;element()只能匹配元素,必须指定节点名(如p、em等),需要的节点名。是否可以添加额外的过滤器?也许我们只想选择所有当前节点的第一个子元素或者只选择具有href属性的标签。对于此类断言,我们可以使用谓词根据额外的树遍历过滤出符合条件的节点。这样,我们就可以根据这些节点的属性(孩子、父母或兄弟姐妹)筛选出符合条件的节点。我们的示例中没有谓词,现在让我们添加一个仅匹配href属性的标签:/descendant-or-self::node()/child::p/child::em/child::a[attribute::href]尽管谓词看起来很像括号中的定位步骤,但谓词的“节点测试”部分比定位步骤中的节点测试具有更多功能。#p#从另一个角度来看,XPath与其说是增强的CSS选择器,不如说更类似于JQuery的便利性。比如我们可以把之前的XPath表达式换成JQuery:$(document).find('*').children('p').children('em').children('a').filter('[href]')在上面的代码中,我们使用的JQuery方法与轴的作用是一样的:.children()相当于轴中的孩子,.find()相当于后代。jQuery方法中的选择器相当于XPath中的节点测试,但是jQuery不允许选择文本节点。jQuery中的.filter()方法相当于XPath中的predicate,.children('em')的作用是匹配所有匹配的

标签中的所有子元素。这样看来,XPah比jQuery强大多了。让我们回到识别行首的问题既然我们对XPath的工作原理有了深入的了解,让我们用它来解决前面提到的问题。首先,让我们简化问题,只寻找每个段落的第一个文本节点:/descendant-or-self::node()/child::p/child::text()[position()=1]上面代码的作用是:1.查找文档中的所有节点2.查找这些节点的所有

子节点3.查找这些

的文本子节点4.只保留那些满足条件的节点第一个元素注意,代码中的position()函数表示的是每个

中的第一个文本子节点,而不是整个文档中的第一个

文本子节点。接下来,为了在

中找到深度嵌套的文本节点,我们将child替换为descendant/descendant-or-self::node()/child::p/descendant::text()[position()=1]接下来是识别换行符的问题。首先,我们将这一长串代码的下一行折叠起来(因为太长了)。XPath允许这样做。添加换行识别后,代码如下:/descendant-or-self::node()/child::br/following-sibling::node()[position=1]/descendant-or-self::text()[position()=1]每一行代码的意思是:1.找到所有节点2.找到这些节点的
子节点3.找到这些
的下一个兄弟节点4.如果上面如果不是文本节点,则取其子节点中的第一个文本节点,这样我们就可以同时选择

中和
之后的新行。下面我们将上面的代码组合成一个表达式:(/descendant-or-self::node()/child::p|/descendant-or-self::node()/child::br/following-sibling::node()[position=1])/descendant-or-self::text()[position()=1]最后,我们替换简写:(//p|//br/following-sibling::node()[position=1])/descendant-or-self::text()[position=1]这样,我们用一个简单的表达式表达了一个复杂的概念。如果我们想对行添加更多的操作,只需要在匹配代码中添加更多的元素名称即可。我们究竟能从中得到什么?既然可以用相对容易理解的Ruby来实现,为什么还要选择XPath?在大多数情况下,Ruby用于编写高级代码,例如业务逻辑、集成应用程序组件以及描述复杂的领域模型。由此可见,最好的Ruby代码是用来描述意图而不是实现的。因此,使用Ruby来做一些低级或与应用程序无关的事情(遍历dom树以找到具有指定属性的节点)是一件很痛苦的事情。XPath的优势之一就是速度:XPath的遍历是通过libxml实现的,原生代码的速度非常快。对于我上面给出的示例,XPath实际上比Ruby的实现要慢得多。我猜这是因为要搜索
标记的下一个元素。因为在这个动作中,实际上是先过滤掉
后面的所有同级元素,然后再过滤掉第一个。所以XPath快不快就看你怎么用了,只是上手有点难。这是一个旨在让您使用简洁的惯用表达式遍历dom的工具。原文链接:rapgenius翻译:伯乐在线-杨帅翻译链接:http://blog.jobbole.com/58160/