2009-05-04 5 views
4

Was ist der intelligenteste Weg, um Nokogiri den gesamten Inhalt zwischen dem Start- und Stoppelement (einschließlich Start-/Stoppelement) auszuwählen?Nokogiri: Wählen Sie den Inhalt zwischen Element A und B

prüfen Beispielcode unten zu verstehen, was ich suche:

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') 

Das Ergebnis (Rückgabewert) wie dieses aussehen sollte:

<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> 

Update: Das ist mein aktuelle Lösung, obwohl ich denke, es muss etwas klüger sein:

@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 

Antwort

10

Ein Weg-zu-smart oneliner die Rekursion verwendet:

def collect_between(first, last) 
    first == last ? [first] : [first, *collect_between(first.next, last)] 
end 

Eine iterative Lösung: genannt (Short) Erklärung des asterix

Es ist:

def collect_between(first, last) 
    result = [first] 
    until first == last 
    first = first.next 
    result << first 
    end 
    result 
end 

EDIT der Splat-Operator. It „entrollt“ ein 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] 
+0

Vielen Dank für Ihre Lösungen und vielen Dank für Ihre über-smart rekursive Einzeiler! Allerdings verstehe ich nicht, was das '*' vor dem rekursiven Aufruf von collect_between() steht. Könnten Sie das näher ausführen? – Javier

+1

Ich habe eine kleine Erklärung in meiner ursprünglichen Antwort hinzugefügt. Google rum für "Splat Operator" für mehr :-) –

+0

Danke! Nur aus Neugier: Haben Sie eine Ahnung, wo der "Splat-Operator" dokumentiert ist? Konnte kein Wort in http://www.ruby-doc.org/core/ – Javier

2
# 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) 
+0

... ich freue mich wirklich auf Nokogiri 1.3.:) – Javier

+0

Bitte beachten Sie, dass NodeSet # slice und NodeSet # index jetzt in Nokogiri Master auf Github sind. Diese werden später in diesem Monat in der Version 1.3.0 erscheinen. –

2

Aus Gründen der Vollständigkeit ein XPath nur Lösung :)
es einen Schnittpunkt von zwei Sätzen aufbaut, die folgenden Geschwister des Startelements und des vorhergehenden Geschwister des Endelements.

Grundsätzlich können Sie eine Kreuzung bauen mit:

$a[count(.|$b) = count($b)] 

ein wenig auf Variablen geteilt, um die Lesbarkeit:

@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") 

Geschwister sind nicht das Element selbst, so Die Elemente start und end müssen getrennt in den Ausdruck eingefügt werden.

Edit: einfachere Lösung:

@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")