Ось XPath, получить все следующие узлы до

У меня есть следующий пример 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" неизвестен, поэтому, к сожалению, я не могу использовать ответ, предоставленный bougyman). Теперь я могу, конечно, используя что-то вроде //h2[text()='Foo bar']/following::p но это, конечно, захватит все пункты после этого заголовка. Поэтому у меня есть возможность пройти через набор узлов и вставить абзацы в массив, пока текст не совпадет с текстом следующего следующего заголовка, но давайте будем честными, это никогда не было так круто, как возможность делать это в XPath.

Есть ли способ сделать это, что я скучаю?

7 ответов

Решение

Используйте:

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

Это значит: выбрать все p элементы, которые следуют за братьями и сестрами h2 (первый или единственный в документе), строковое значение которого 'Foo bar' а также первый предшествующий брат h2 для всех этих p элементы именно Н2(first or only one in the document) whose string value is'Foo bar'`.

Здесь мы используем метод определения идентичности двух узлов:

count($n1 | $n2) = 1

является true() именно тогда, когда узлы $n1 а также $n2 это один и тот же узел.

Это выражение можно обобщить:

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

выбирает все "ближайшие родственники" любого узла, указанного в $ x.

Этот оператор XPATH 1.0 выбирает все <p> которые являются братьями и сестрами, которые следуют за <h2> строковое значение who равно "Foo bar", за которым также следует <h2> элемент родного брата, кто первый предшествующий родной брат <h2> имеет строковое значение "Foo bar".

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

В XPath 2.0 (я знаю, это вам не поможет...) возможно, самое простое решение

h2[. = 'Foo bar']/follow-sibling::* кроме h2[. = 'Bar baz']/(.| Follow-sibling::*)

Но, как и в других представленных решениях, скорее всего (при отсутствии оптимизатора, который распознает шаблон), оно будет линейным по количеству элементов после второго h2, тогда как вам действительно нужно решение, производительность которого зависит только от количества элементов. выбран. Я всегда чувствовал, что было бы неплохо иметь оператор пока:

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

В его отсутствие решение XSLT или XQuery, использующее рекурсию, вероятно, будет работать лучше, когда число узлов, которые будут выбраны, невелико по сравнению с количеством следующих братьев и сестер.

Просто потому, что это не между ответами, классическое исключение набора 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)]

Примечание: это было бы отрицанием метода Кайса.

XPath 2.0 имеет оператора <<$node1 << $node2 быть правдой, если $node1 предшествует $node2) так что вы можете использовать //h2[. = 'Foo bar']/following-sibling::p[. << //h2[. = 'Bar baz']], Однако я не знаю, что такое nokogiri и поддерживает ли он XPath 2.0.

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>

Я подозревал, что может быть эффективнее просто ходить по DOM, используя next_sibling пока вы не дойдете до конца:

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.

Как насчет сопоставления на втором? Если вам нужен только верхний раздел, выберите второй и возьмите все, что находится над ним.
doc.xpath("//h2[text()='Bar baz']/preceding-sibling::p").map { |m| m.text }=> ["lorem", "ipsum", "etc"]

или, если вы не знаете второй, перейдите на другой уровень с:doc.xpath("//h2[text()='Foo bar']/following-sibling::h2/preceding-sibling::p").map { |it| it.text }=> ["lorem", "ipsum", "etc"]

Другие вопросы по тегам