Ось XPath, получить все следующие узлы до - PullRequest
13 голосов
/ 22 января 2011

У меня есть следующий пример 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 ]

18 голосов
/ 22 января 2011

Использование :

(//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 .

3 голосов
/ 22 января 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)]

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

3 голосов
/ 22 января 2011

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

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

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

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

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

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

2 голосов
/ 22 января 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"]

2 голосов
/ 22 января 2011

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

...