【问题标题】:XPath axis, get all following nodes untilXPath 轴,获取所有后续节点,直到
【发布时间】:2016-01-06 16:04:23
【问题描述】:

我有以下 HTML 示例:

<!-- lots of html -->
<h2>Foo bar</h2>
<p>lorem</p>
<p>ipsum</p>
<p>etc</p>

<h2>Bar baz</h2>
<p>dum dum dum</p>
<p>poopfiddles</p>
<!-- lots more html ... -->

我希望提取“Foo bar”标题之后的所有段落,直到到达“Bar baz”标题(“Bar baz”标题的文本未知,所以很遗憾我无法使用答案布吉曼提供)。现在我当然可以使用//h2[text()='Foo bar']/following::p 之类的东西,但这当然会抓住这个标题后面的 all 段落。所以我可以选择遍历节点集并将段落推送到数组中,直到文本与下一个标题匹配,但老实说,这从来没有像在 XPath 中那样酷。

有没有我想念的方法来做到这一点?

【问题讨论】:

  • 好问题,+1。请参阅我对单个 XPath 表达式的回答,该表达式选择指定节点的所有“紧跟同级”。我还提供了一个更通用的 XPath 表达式,可用于查找任何节点的“紧随其后的兄弟姐妹”。提供了广泛的解释。

标签: ruby xpath nokogiri


【解决方案1】:

使用

(//h2[. = 'Foo bar'])[1]/following-sibling::p
   [1 = count(preceding-sibling::h2[1] | (//h2[. = 'Foo bar'])[1])]

如果保证每个h2 都有一个不同的值,则可以简化为:

//h2[. = 'Foo bar']/following-sibling::p
   [1 = count(preceding-sibling::h2[1] | ../h2[. = 'Foo bar'])]

这意味着:选择字符串值为'Foo bar'h2(文档中的第一个或唯一一个)的兄弟姐妹以及前面的第一个兄弟姐妹的所有p元素所有这些p 元素的h2 正是h2(first or only one in the document) whose string value is'Foo bar'`。

这里我们使用一种判断两个节点是否相同的方法

count($n1 | $n2) = 1

当节点 $n1$n2 是同一个节点时是 true()

这个表达式可以推广

$x/following-sibling::p
       [1 = count(preceding-sibling::node()[name() = name($x)][1] | $x)]

选择 $x 指定的任何节点的所有“紧跟同级”

【讨论】:

  • sigh 为什么我还要和你一起回答 xpath 的问题呢?我曾希望你睡着了;)我的在概念上更简单(对我来说),但我相信你的性能更高。 +1
  • @phrogz:我真的很抱歉我在星期六早上 6 点醒来,我没有更好的事情可做:)
  • @Dimitre 没关系,我的孩子们 7 点把我叫醒,所以我比你多一个小时,这让我感到安慰。 :D
  • @phrogz:至于我们答案的效率比较,我认为您通常是对的,我的可能更有效——但这一切都取决于特定 XPath 实现使用的优化器被使用。
  • @phrogz:不管好坏,我女儿现在是大学新生,通常比我睡得更久:)
【解决方案2】:

在 XPath 2.0 中(我知道这对你没有帮助......)最简单的解决方案可能是

h2[. ='富 bar']/following-sibling::* 除外 h2[. ='酒吧 baz']/(.|following-sibling::*)

但与其他解决方案一样,这很可能(在没有识别模式的优化器的情况下)在第二个 h2 之后的元素数量上是线性的,而您真的希望一个解决方案的性能仅取决于选择的元素数量。我一直觉得有一个 until 运算符会很好:

h2[. = 'Foo bar']/(following-sibling::* until . = 'Bar baz')

如果没有使用递归的 XSLT 或 XQuery 解决方案,当要选择的节点数量少于后续兄弟节点的数量时,其性能可能会更好。

【讨论】:

    【解决方案3】:

    XPATH 1.0 语句选择所有&lt;p&gt; 的兄弟姐妹,这些&lt;h2&gt; 的字符串值等于“Foo bar”,后面还跟有&lt;h2&gt;第一个在前的兄弟&lt;h2&gt; 的兄弟元素的字符串值为“Foo bar”。

    //p[preceding-sibling::h2[.='Foo bar']]
     [following-sibling::h2[
      preceding-sibling::h2[1][.='Foo bar']]]
    

    【讨论】:

    • @Mads-Hansen:您的 XPath 表达式没有选择您所说的内容。如果您将字符串“text()”替换为“字符串值”,或者如果您修改表达式本身并替换“。”,则您的陈述将变为真。使用 'text()' -- 我不推荐。
    • 是的,虽然我不认为 HTML Heading 元素具有混合内容。在本示例中,只有一个文本节点的&lt;h2&gt; 的字符串值与text() 相同。
    【解决方案4】:

    正因为不在答案之间,经典的 XPath 1.0 设置排除:

    A - B = $A[count(.|$B)!=count($B)]

    对于这种情况:

    (//h2[.='Foo bar']
        /following-sibling::p)
           [count(.|../h2[.='Foo bar']
                         /following-sibling::h2[1]
                            /following-sibling::p)
            != count(../h2[.='Foo bar']
                         /following-sibling::h2[1]
                            /following-sibling::p)]
    

    注意:这是对凯斯方法的否定。

    【讨论】:

      【解决方案5】:

      XPath 2.0 具有运算符 &lt;&lt;(如果 $node1 位于 $node2 之前,则 $node1 &lt;&lt; $node2 为真),因此您可以使用 //h2[. = 'Foo bar']/following-sibling::p[. &lt;&lt; //h2[. = 'Bar baz']]。但是我不知道 nokogiri 是什么,它是否支持 XPath 2.0。

      【讨论】:

      • 不幸的是它没有,不过看起来很酷。尽管如此,还是感谢您的回复,请点赞。
      【解决方案6】:
      require 'nokogiri'
      
      doc = Nokogiri::XML <<ENDXML
      <root>
        <h2>Foo</h2>
        <p>lorem</p>
        <p>ipsum</p>
        <p>etc</p>
      
        <h2>Bar</h2>
        <p>dum dum dum</p>
        <p>poopfiddles</p>
      </root>
      ENDXML
      
      a = doc.xpath( '//h2[text()="Foo"]/following::p[not(preceding::h2[text()="Bar"])]' )
      puts a.map{ |n| n.to_s }
      #=> <p>lorem</p>
      #=> <p>ipsum</p>
      #=> <p>etc</p>
      

      我怀疑使用 next_sibling 遍历 DOM 直到结束可能会更有效:

      node = doc.at_xpath('//h2[text()="Foo bar"]').next_sibling
      stop = doc.at_xpath('//h2[text()="Bar baz"]')
      a = []
      while node && node!=stop
        a << node unless node.type == 3 # skip text nodes
        node = node.next_sibling
      end
      
      puts a.map{ |n| n.to_s }
      #=> <p>lorem</p>
      #=> <p>ipsum</p>
      #=> <p>etc</p>
      

      但是,这不会更快。在一些简单的测试中,我发现 xpath-only(第一个解决方案)的速度大约是这个循环测试的 2 倍,即使在停止节点之后有非常多的段落也是如此。当有很多节点要捕获(停止后很少)时,它的性能会更好,在 6x-10x 范围内。

      【讨论】:

        【解决方案7】:

        如何匹配第二个?如果你只想要顶部,匹配第二个并抓住它上面的所有东西 .
        doc.xpath("//h2[text()='Bar baz']/preceding-sibling::p").map { |m| m.text } => [“lorem”、“ipsum”、“等”]

        或者,如果您不知道第二个,请通过以下方式进入另一个级别: doc.xpath("//h2[text()='Foo bar']/following-sibling::h2/preceding-sibling::p").map { |it| it.text } => [“lorem”、“ipsum”、“等”]

        【讨论】:

        • 很遗憾,我不能使用第二个标题文本作为选择器,因为它不是唯一的,而且文本可以是任何内容,所以我必须使用第一个标题。
        • 我认为第二个建议应该足够好。谢谢!
        • 啊我的糟糕,糟糕的例子..在第一个标题之前也会有段落,这意味着你的第二个例子也会抓住这些:(
        猜你喜欢
        • 1970-01-01
        • 2023-03-27
        • 2012-12-03
        • 2014-08-17
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2017-02-24
        • 1970-01-01
        相关资源
        最近更新 更多