XPath axis, get all following nodes until

后端 未结 7 1316
醉话见心
醉话见心 2020-12-01 11:18

I have the following example of HTML:


Foo bar

lorem

ipsum

etc

相关标签:
7条回答
  • 2020-12-01 11:32

    XPath 2.0 has the operator << (with $node1 << $node2 being true if $node1 precedes $node2) so that way you can use //h2[. = 'Foo bar']/following-sibling::p[. << //h2[. = 'Bar baz']]. I don't know however what nokogiri is respectively whether it supports XPath 2.0.

    0 讨论(0)
  • 2020-12-01 11:37

    This XPATH 1.0 statement selects all of the <p> that are siblings that follow an <h2> who's string value is equal to "Foo bar", that are also followed by an <h2> sibling element who's first preceding sibling <h2> has a string value of "Foo bar".

    //p[preceding-sibling::h2[.='Foo bar']]
     [following-sibling::h2[
      preceding-sibling::h2[1][.='Foo bar']]]
    
    0 讨论(0)
  • 2020-12-01 11:38

    In XPath 2.0 (I know this doesn't help you...) the simplest solution is probably

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

    But like other solutions presented, this is likely (in the absence of an optimizer that recognizes the pattern) to be linear in the number of elements beyond the second h2, whereas you would really like a solution whose performance depends only on the number of elements selected. I've always felt it would be nice to have an until operator:

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

    In its absence an XSLT or XQuery solution using recursion is likely to perform better when the number of nodes to be selected is small compared with the number of following siblings.

    0 讨论(0)
  • 2020-12-01 11:39

    Use:

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

    In case it is guaranteed that every h2 has a distinct value, this may be simplified to:

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

    This means: Select all p elements that are following siblings of the h2 (first or only one in the document) whose string value is 'Foo bar' and also the first preceding sibling h2 for all these p elements is exactly the h2(first or only one in the document) whose string value is'Foo bar'`.

    Here we use a method of finding whether two nodes are identical:

    count($n1 | $n2) = 1
    

    is true() exactly when the nodes $n1 and $n2 are the same node.

    This expression can be generalized:

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

    selects all "immediate following siblings" of any node specified by $x.

    0 讨论(0)
  • 2020-12-01 11:47

    Just because it's not between the answers, the classic XPath 1.0 set exclusion:

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

    For this case:

    (//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)]
    

    Note: This would be the negation of Kaysian Method.

    0 讨论(0)
  • 2020-12-01 11:49
    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>
    

    I suspected that it might be more efficient to just walk the DOM using next_sibling until you hit the end:

    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>
    

    However, this is NOT faster. In a few simple tests, I found that xpath-only (the first solution) is about 2x as fast as this looping test, even when there are a very large number of paragraphs after the stop node. When there are many nodes to capture (and few after the stop) it performs even better, in the 6x-10x range.

    0 讨论(0)
提交回复
热议问题