Подтвердить что ты не робот

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

Есть ли способ сделать это, что мне не хватает?

4b9b3361

Ответ 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'])]

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

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

count($n1 | $n2) = 1

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

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

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

выбирает всех "непосредственных подчиненных братьев" любого node, указанного в $x.

Ответ 2

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

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

Ответ 3

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

h2 [. = 'Foo bar ']/следующий-сиблинг:: * кроме  h2 [. = 'Бар baz ']/(. | next-sibling:: *)

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

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

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

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

Ответ 6

как насчет соответствия на втором? Если вам нужен только верхний раздел, сравните второй и захватите все выше него .
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" ]

Ответ 7

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 раза быстрее, чем этот цикл, даже если после остановки node имеется очень большое количество абзацев. Когда есть много узлов для захвата (и несколько после остановки), он работает еще лучше, в диапазоне 6x-10x.