Nokogiri: seleziona il contenuto tra l'elemento A e B.
-
03-07-2019 - |
Domanda
Qual è il modo più intelligente per consentire a Nokogiri di selezionare tutti i contenuti tra l'elemento start e stop (incluso l'elemento start / stop)?
Controlla il codice di esempio di seguito per capire cosa sto cercando:
require 'rubygems'
require 'nokogiri'
value = Nokogiri::HTML.parse(<<-HTML_END)
"<html>
<body>
<p id='para-1'>A</p>
<div class='block' id='X1'>
<p class="this">Foo</p>
<p id='para-2'>B</p>
</div>
<p id='para-3'>C</p>
<p class="that">Bar</p>
<p id='para-4'>D</p>
<p id='para-5'>E</p>
<div class='block' id='X2'>
<p id='para-6'>F</p>
</div>
<p id='para-7'>F</p>
<p id='para-8'>G</p>
</body>
</html>"
HTML_END
parent = value.css('body').first
# START element
@start_element = parent.at('p#para-3')
# STOP element
@end_element = parent.at('p#para-7')
Il risultato (valore restituito) dovrebbe apparire così :
<p id='para-3'>C</p>
<p class="that">Bar</p>
<p id='para-4'>D</p>
<p id='para-5'>E</p>
<div class='block' id='X2'>
<p id='para-6'>F</p>
</div>
<p id='para-7'>F</p>
Aggiornamento: questa è la mia soluzione attuale, anche se penso che ci debba essere qualcosa di più intelligente:
@my_content = ""
@selected_node = true
def collect_content(_start)
if _start == @end_element
@my_content << _start.to_html
@selected_node = false
end
if @selected_node == true
@my_content << _start.to_html
collect_content(_start.next)
end
end
collect_content(@start_element)
puts @my_content
Soluzione
Un oneliner fin troppo intelligente che usa la ricorsione:
def collect_between(first, last)
first == last ? [first] : [first, *collect_between(first.next, last)]
end
Una soluzione iterativa:
def collect_between(first, last)
result = [first]
until first == last
first = first.next
result << first
end
result
end
EDIT: (breve) spiegazione dell'asterisco
Si chiama operatore splat. "Srotola" " un array:
array = [3, 2, 1]
[4, array] # => [4, [3, 2, 1]]
[4, *array] # => [4, 3, 2, 1]
some_method(array) # => some_method([3, 2, 1])
some_method(*array) # => some_method(3, 2, 1)
def other_method(*array); array; end
other_method(1, 2, 3) # => [1, 2, 3]
Altri suggerimenti
# monkeypatches for Nokogiri::NodeSet
# note: versions of these functions will be in Nokogiri 1.3
class Nokogiri::XML::NodeSet
unless method_defined?(:index)
def index(node)
each_with_index { |member, j| return j if member == node }
end
end
unless method_defined?(:slice)
def slice(start, length)
new_set = Nokogiri::XML::NodeSet.new(self.document)
length.times { |offset| new_set << self[start + offset] }
new_set
end
end
end
#
# solution #1: picking elements out of node children
# NOTE that this will also include whitespacy text nodes between the <p> elements.
#
possible_matches = parent.children
start_index = possible_matches.index(@start_element)
stop_index = possible_matches.index(@end_element)
answer_1 = possible_matches.slice(start_index, stop_index - start_index + 1)
#
# solution #2: picking elements out of a NodeSet
# this will only include elements, not text nodes.
#
possible_matches = value.xpath("//body/*")
start_index = possible_matches.index(@start_element)
stop_index = possible_matches.index(@end_element)
answer_2 = possible_matches.slice(start_index, stop_index - start_index + 1)
Per completezza, una soluzione solo XPath :)
Costruisce un'intersezione di due insiemi, i seguenti fratelli dell'elemento iniziale e i fratelli precedenti dell'elemento finale.
Fondamentalmente puoi costruire un incrocio con:
$a[count(.|$b) = count($b)]
Un po 'diviso in variabili per leggibilità:
@start_element = "//p[@id='para-3']"
@end_element = "//p[@id='para-7']"
@set_a = "#@start_element/following-sibling::*"
@set_b = "#@end_element/preceding-sibling::*"
@my_content = value.xpath("#@set_a[ count(.|#@set_b) = count(#@set_b) ]
| #@start_element | #@end_element")
I fratelli non includono l'elemento stesso, quindi gli elementi iniziale e finale devono essere inclusi nell'espressione separatamente.
Modifica: Soluzione più semplice:
@start_element = "p[@id='para-3']"
@end_element = "p[@id='para-7']"
@my_content = value.xpath("//*[preceding-sibling::#@start_element and
following-sibling::#@end_element]
| //#@start_element | //#@end_element")