Ось 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.

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


person Lee Jarvis    schedule 22.01.2011    source источник
comment
Хороший вопрос, +1. См. мой ответ для одного выражения XPath, которое выбирает всех ближайших следующих братьев и сестер указанного узла. Я также привожу более общее выражение XPath, которое можно использовать для поиска непосредственно следующих братьев и сестер любого узла. Дано развернутое объяснение.   -  person Dimitre Novatchev    schedule 22.01.2011


Ответы (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 является точно h2(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.

person Dimitre Novatchev    schedule 22.01.2011
comment
вздох Почему я вообще отвечаю на вопросы xpath в вашем присутствии? Я надеялся, что вы спите ;) Мой концептуально проще (для меня), но я уверен, что ваш более эффективен. +1 - person Phrogz; 22.01.2011
comment
@phrogz: Мне очень жаль, что я проснулся в 6 утра в субботу утром, и мне больше нечего было делать :) - person Dimitre Novatchev; 22.01.2011
comment
@Dimitre Все в порядке, мои дети разбудили меня в 7, поэтому я утешаюсь тем, что у меня на час больше, чем у тебя. :D - person Phrogz; 22.01.2011
comment
@phrogz: Что касается сравнения эффективности наших ответов, я думаю, что вы в целом правы в том, что мой может быть более эффективным, однако все это зависит от оптимизатора, используемого конкретной реализацией XPath. - person Dimitre Novatchev; 22.01.2011
comment
@phrogz: Хорошо это или плохо, но моя дочь сейчас первокурсница в универе и обычно спит намного дольше, чем я :) - person Dimitre Novatchev; 22.01.2011
comment
Очень хорошо, я пытался написать что-то подобное, но наткнулся на пару недостатков, которых избегает ваш ответ. Главные плюсы как для себя, так и для Phrogz! Спасибо - person Lee Jarvis; 22.01.2011
comment
@Dimitre: +1 Хороший ответ. В качестве младшего: в этом случае, поскольку метка проверяется на братьев и сестер, вы можете использовать ../h2[. = 'Foo bar'] вместо абсолютного выражения //h2[. = 'Foo bar'], кроме того, это хорошо для ясности. Также есть опечатка в последнем preceding-sibling::node отсутствует (). - person ; 22.01.2011
comment
@Alejandro: Спасибо, что заметили это. Кроме того, ваше предложение использовать ../h2 вместо //h2 является значительным улучшением производительности. Исправлено сейчас. - person Dimitre Novatchev; 22.01.2011

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

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

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

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

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

person Michael Kay    schedule 22.01.2011

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

//p[preceding-sibling::h2[.='Foo bar']]
 [following-sibling::h2[
  preceding-sibling::h2[1][.='Foo bar']]]
person Mads Hansen    schedule 22.01.2011
comment
@Mads-Hansen: Ваше выражение XPath не выбирает то, что вы говорите, что оно делает. Ваше утверждение станет истинным, если вы замените строку text() строковым значением или если вы измените само выражение и замените '.' с 'text()' - что я не рекомендую. - person Dimitre Novatchev; 22.01.2011
comment
Да, хотя я не думал, что элементы заголовка HTML имеют смешанный контент. Для целей этого примера строковое значение <h2>, которое имеет только текстовый узел, совпадает с text(). - person Mads Hansen; 22.01.2011

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

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

person Community    schedule 22.01.2011

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

person Martin Honnen    schedule 22.01.2011
comment
К сожалению, это не так, хотя выглядит очень круто. Тем не менее, спасибо за ответ, голосуйте. - person Lee Jarvis; 22.01.2011

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

person Phrogz    schedule 22.01.2011

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

person bougyman    schedule 22.01.2011
comment
К сожалению, я не могу использовать второй текст заголовка в качестве селектора, потому что он не уникален, а текст может быть любым, поэтому я должен использовать первый заголовок. - person Lee Jarvis; 22.01.2011
comment
Я думаю, что второе предложение должно работать достаточно хорошо. Спасибо! - person Lee Jarvis; 22.01.2011
comment
Ах, мой плохой, ужасный пример... перед первым заголовком также будут абзацы, что означает, что ваш второй пример также захватит их :( - person Lee Jarvis; 22.01.2011