Как ориентироваться в DOM с помощью Nokogiri - PullRequest
7 голосов
/ 18 марта 2009

Я пытаюсь заполнить переменные parent_element_h1 и parent_element_h2. Может ли кто-нибудь помочь мне использовать 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 =

Обратите внимание: элемент start_here может находиться где угодно внутри документа. Данные HTML являются лишь примером. Тем не менее, заголовки <h1> и <h2> могут быть родным братом start_here или потомком родного брата start_here.

Следующий рекурсивный метод является хорошей отправной точкой, но он не работает на <h1>, потому что это дочерний элемент от 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')

После принятия ответа я придумал мое собственное решение . Это работает как шарм, и я думаю, что это довольно круто.

Ответы [ 6 ]

10 голосов
/ 18 марта 2009

Подход, который я бы выбрал (если я понимаю вашу проблему), заключается в использовании XPath или CSS для поиска вашего элемента "start_here" и родительского элемента, в котором вы хотите искать. Затем рекурсивно обойдите дерево, начиная с родительского, останавливаясь при нажатии на элемент «start_here» и удерживая последний элемент, который соответствует вашему стилю на этом пути.

Что-то вроде:

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) 

Где FindPriorTo - простой класс для обработки рекурсии:

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

Если этот подход недостаточно масштабируемый, то вы можете оптимизировать вещи, переписав recursive_search, чтобы не использовать рекурсию, а также передать оба стиля, которые вы ищете, и отследить последний найденный, так что вам не нужно обходить дерево в дополнительное время.

Я бы также сказал, что попробуйте подключить Node к обезьяне, чтобы зацепить его при разборе документа, но похоже, что все это написано на C. Возможно, вам лучше использовать что-то иное, чем Nokogiri с собственным Ruby SAX-парсер (может быть REXML ), или, если ваша реальная задача - скорость, выполните поиск в C / C ++, используя Xerces или аналогичный. Я не знаю, насколько хорошо они будут работать с парсингом HTML.

3 голосов
/ 12 марта 2014

Я столкнулся с этим на несколько лет позже, я полагаю, но был вынужден опубликовать, потому что все остальные решения слишком сложны.

Это одно утверждение с 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>

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

2 голосов
/ 01 апреля 2009

Может быть, это сделает это. Я не уверен в производительности и, может быть, есть такие случаи, о которых я не задумывался.

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 голосов
/ 22 апреля 2009

Это мое собственное решение (спасибо моему коллеге за помощь в этом!), Использующее рекурсивный метод для анализа всех элементов, независимо от того, является ли он родным или дочерним по отношению к другому.

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

Вы можете скопировать / вставить его, запустив, как в сценарии ruby.

0 голосов
/ 25 марта 2009

Вы можете искать потомков Nokogiri HTML::Element с помощью селекторов CSS. Вы можете пройти через предков с помощью метода .parent.

parent_element_h1 = value.css("h1").first.parent
parent_element_h2 = value.css("h2").first.parent
0 голосов
/ 18 марта 2009

Если вы не знаете отношения между элементами, вы можете искать их следующим образом (в любом месте документа):


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

Если, однако, вам нужно отправить форму, вы должны использовать mechanize:


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