2009-03-18 12 views
7

Estoy tratando de completar las variables parent_element_h1 y parent_element_h2. ¿Alguien puede ayudarme a usar Nokogiri para obtener la información que necesito sobre esas variables?Cómo navegar el DOM usando Nokogiri

require 'rubygems' 
require 'nokogiri' 

value = Nokogiri::HTML.parse(<<-HTML_END) 
    "<html> 
    <body> 
     <p id='para-1'>A</p> 
     <div class='block' id='X1'> 
     <h1>Foo</h1> 
     <p id='para-2'>B</p> 
     </div> 
     <p id='para-3'>C</p> 
     <h2>Bar</h2> 
     <p id='para-4'>D</p> 
     <p id='para-5'>E</p> 
     <div class='block' id='X2'> 
     <p id='para-6'>F</p> 
     </div> 
    </body> 
    </html>" 
HTML_END 

parent = value.css('body').first 

# start_here is given: A Nokogiri::XML::Element of the <div> with the id 'X2 
start_here = parent.at('div.block#X2') 

# this should be a Nokogiri::XML::Element of the nearest, previous h1. 
# in this example it's the one with the value 'Foo' 
parent_element_h1 = 

# this should be a Nokogiri::XML::Element of the nearest, previous h2. 
# in this example it's the one with the value 'Bar' 
parent_element_h2 = 

Tenga en cuenta: El elemento start_here podría estar en cualquier lugar dentro del documento. Los datos HTML son solo un ejemplo. Dicho esto, los encabezados <h1> y <h2> podrían ser un hermano de start_here o un hijo de un hermano de start_here.

El siguiente método recursivo es un buen punto de partida, pero no funciona en <h1> porque es un hijo de un hermano de start_here:

def search_element(_block,_style) 
    unless _block.nil? 
    if _block.name == _style 
     return _block 
    else 
     search_element(_block.previous,_style) 
    end 
    else 
    return false 
    end 
end 

parent_element_h1 = search_element(start_here,'h1') 
parent_element_h2 = search_element(start_here,'h2') 

Después de aceptar una respuesta, se me ocurrió con my own solution. Funciona a las mil maravillas y creo que es genial.

Respuesta

3

Me encontré con esto unos años demasiado tarde, supongo, pero me sentí obligado a publicar porque todas las demás soluciones son demasiado complicadas.

Es una sola declaración con XPath:

start = doc.at('div.block#X2') 

start.at_xpath('(preceding-sibling::h1 | preceding-sibling::*//h1)[last()]') 
#=> <h2>Foo</h2>  

start.at_xpath('(preceding-sibling::h2 | preceding-sibling::*//h2)[last()]') 
#=> <h2>Bar</h2> 

Esto da cabida a cualquiera de los hermanos anteriores directos o hijos de los hermanos anteriores. Independientemente de cuál coincida, el predicado last() garantiza que obtenga la coincidencia previa más cercana.

10

El enfoque que tomaría (si entiendo su problema) es usar XPath o CSS para buscar su elemento "start_here" y el elemento principal que desea buscar. Luego, camine recursivamente por el árbol comenzando por el padre, deténgase cuando toque el elemento "start_here" y sosteniendo el último elemento que coincida con su estilo en el camino.

Algo así como:

parent = value.search("//body").first 
div = value.search("//div[@id = 'X2']").first 

find = FindPriorTo.new(div) 

assert_equal('Foo', find.find_from(parent, 'h1').text) 
assert_equal('Bar', find.find_from(parent, 'h2').text) 

Dónde FindPriorTo es una clase simple de manejar la recursividad:

class FindPriorTo 
    def initialize(stop_element) 
    @stop_element = stop_element 
    end 

    def find_from(parent, style) 
    @should_stop = nil 
    @last_style = nil 

    recursive_search(parent, style) 
    end 

    def recursive_search(parent, style) 
    parent.children.each do |ch| 
     recursive_search(ch, style) 
     return @last_style if @should_stop 

     @should_stop = (ch == @stop_element) 
     @last_style = ch if ch.name == style 
    end 

    @last_style  
    end 

end 

Si este método no es lo suficientemente escalable, entonces usted podría ser capaz de optimizar las cosas por reescribiendo el recursive_search para no usar la recursión, y también pasar en los dos estilos que está buscando y realizar un seguimiento de los últimos encontrados, por lo que no tiene que recorrer el árbol un tiempo extra.

También diría que intente con el parche de parche para engancharlo cuando se está analizando el documento, pero parece que todo está escrito en C. Tal vez sea mejor utilizar algo que no sea Nokogiri que tiene un nativo Analizador de Ruby SAX (quizás REXML), o si la velocidad es su verdadera preocupación, haga la porción de búsqueda en C/C++ usando Xerces o similar. Sin embargo, no sé qué tan bien se ocuparán de analizar el HTML.

+0

El problema es que no sé si el encabezado es un hermano o un hijo de un hermano. Su solución asume que sé si es un hermano o un hijo de un hermano. Además de eso, mis datos de ejemplo son mucho más cortos que mis datos reales: 'my_tag' puede estar en cualquier lugar dentro del documento. – Javier

+0

puede usar '//' en lugar de '/ html/body /' o incluso '/ html/body // div' en XPath cuando no esté seguro de la relación hermano/hijo. http://www.w3schools.com/Xpath/ –

+0

Creo que mi pregunta no fue lo suficientemente específica, edité la pregunta y espero que ahora esté claro lo que estoy buscando (revisen los comentarios sobre las variables que estoy tratando de llenar con datos). – Javier

-1

Si no conoce la relación entre los elementos, puede buscar para ellos de esta manera (en cualquier parte del documento):


# html code 
text = "insert your html here" 
# get doc object 
doc = Nokogiri::HTML(text) 
# get elements with the specified tag 
elements = doc.search("//your_tag") 

Si, sin embargo, es necesario enviar un formulario, se debe utilizar mecanizar:


# create mech object 
mech = WWW::Mechanize.new 
# load site 
mech.get("address") 
# select a form, in this case, I select the first form. You can select the one you need 
# from the array 
form = mech.page.forms.first 
# you fill the fields like this: form.name_of_the_field 
form.element_name = value 
form.other_element = other_value 
+0

Esto no resuelve mi problema, pero he editado mi pregunta para que sea más específica. Tenga en cuenta el comentario sobre las dos variables que estoy tratando de completar. – Javier

+0

En resumen: Esto no funcionaría porque coincidiría más que con el h1 o h2 anterior más cercano. – Javier

-1

Usted puede buscar en los descendientes de un Nokogiri HTML::Element usando selectores CSS. Puede atravesar ancestros con el método .parent.

parent_element_h1 = value.css("h1").first.parent 
parent_element_h2 = value.css("h2").first.parent 
+0

Esto no devuelve el resultado que estoy buscando. Por favor, lea la pregunta de nuevo. – Javier

2

Quizás esto lo haga. No estoy seguro sobre el rendimiento y si puede haber algunos casos en los que no haya pensado.

def find(root, start, tag) 
    ps, res = start, nil 
    until res or (ps == root) 
     ps = ps.previous || ps.parent 
     res = ps.css(tag).last 
     res ||= ps.name == tag ? ps : nil 
    end 
    res || "Not found!" 
end 

parent_element_h1 = find(parent, start_here, 'h1') 
0

Ésta es mi propia solución (felicitaciones a mi compañero de trabajo para ayudar a mí en este caso!) Utilizando un método recursivo para analizar todos los elementos independientemente de ser un hermano o un hijo de otro hermano.

require 'rubygems' 
require 'nokogiri' 

value = Nokogiri::HTML.parse(<<-HTML_END) 
    "<html> 
    <body> 
     <p id='para-1'>A</p> 
     <div class='block' id='X1'> 
     <h1>Foo</h1> 
     <p id='para-2'>B</p> 
     </div> 
     <p id='para-3'>C</p> 
     <h2>Bar</h2> 
     <p id='para-4'>D</p> 
     <p id='para-5'>E</p> 
     <div class='block' id='X2'> 
     <p id='para-6'>F</p> 
     </div> 
    </body> 
    </html>" 
HTML_END 

parent = value.css('body').first 

# start_here is given: A Nokogiri::XML::Element of the <div> with the id 'X2 
@start_here = parent.at('div.block#X2') 

# Search for parent elements of kind "_style" starting from _start_element 
def search_for_parent_element(_start_element, _style) 
    unless _start_element.nil? 
    # have we already found what we're looking for? 
    if _start_element.name == _style 
     return _start_element 
    end 
    # _start_element is a div.block and not the _start_element itself 
    if _start_element[:class] == "block" && _start_element[:id] != @start_here[:id] 
     # begin recursion with last child inside div.block 
     from_child = search_for_parent_element(_start_element.children.last, _style) 
     if(from_child) 
     return from_child 
     end 
    end 
    # begin recursion with previous element 
    from_child = search_for_parent_element(_start_element.previous, _style) 
    return from_child ? from_child : false 
    else 
    return false 
    end 
end 

# this should be a Nokogiri::XML::Element of the nearest, previous h1. 
# in this example it's the one with the value 'Foo' 
puts parent_element_h1 = search_for_parent_element(@start_here,"h1") 

# this should be a Nokogiri::XML::Element of the nearest, previous h2. 
# in this example it's the one with the value 'Bar' 
puts parent_element_h2 = search_for_parent_element(@start_here,"h2") 

Puede copiarlo y pegarlo y ejecutarlo como si fuera un script de ruby.

Cuestiones relacionadas