Question

Quel est le moyen le plus intelligent de laisser Nokogiri sélectionner tout le contenu entre les éléments start et stop (y compris l'élément start- / stop)?

Consultez l'exemple de code ci-dessous pour comprendre ce que je recherche:

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

Le résultat (valeur renvoyée) devrait ressembler à ceci :

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

Mise à jour: il s'agit de ma solution actuelle, même si je pense qu'il doit y avoir quelque chose de plus intelligent:

@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
Était-ce utile?

La solution

Un oneliner trop intelligent qui utilise la récursivité:

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

Une solution itérative:

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

EDIT: explication (courte) de l'astérisque

C'est ce qu'on appelle l'opérateur splat. Il " se déroule " un tableau:

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] 

Autres conseils

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

Par souci d'exhaustivité, une solution XPath uniquement :)

Il construit une intersection de deux ensembles, les frères et soeurs suivants de l’élément début et les frères et soeurs précédents de l’élément final.

  

En principe, vous pouvez créer une intersection avec:

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

Un peu divisé sur les variables pour la lisibilité:

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

Les frères et sœurs n'incluant pas l'élément lui-même, les éléments de début et de fin doivent être inclus dans l'expression séparément.

Modifier: Solution plus simple:

@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")
Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top