Как ориентироваться в DOM с помощью Nokogiri
-
19-08-2019 - |
Вопрос
Я пытаюсь заполнить переменные 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>
могут быть родным братом <=> или дочерним элементом родного брата <=>.
Следующий рекурсивный метод является хорошей отправной точкой, но он не работает на <=>, потому что это дочерний элемент одного из братьев <=>:
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')
<Ч>
После принятия ответа я предложил свое собственное решение . Это работает как шарм, и я думаю, что это довольно круто.
Решение
Я столкнулся с этим на несколько лет позже, я полагаю, но был вынужден опубликовать, потому что все остальные решения слишком сложны.
Это одно утверждение в 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()
гарантирует, что вы получите самое близкое предыдущее соответствие.
Другие советы
Подход, который я бы выбрал (если я понимаю вашу проблему), заключается в использовании XPath или CSS для поиска вашего " start_here " элемент и родительский элемент, который вы хотите найти под. Затем рекурсивно обойдите дерево, начиная с родительского, и остановитесь, когда вы нажмете кнопку & Quot; 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.
Может быть, это сделает это. Я не уверен насчет производительности, и могут ли быть случаи, о которых я не задумывался.
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')
Это мое собственное решение (спасибо моему коллеге за помощь в этом!), использующее рекурсивный метод для анализа всех элементов, независимо от того, является ли он родным или дочерним по отношению к другому. Р>
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.
Если вы не знаете взаимосвязи между элементами, вы можете искать их следующим образом (в любом месте документа):
# 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
Вы можете искать потомков Nokogiri HTML::Element
с помощью селекторов CSS. Вы можете перемещаться по предкам с помощью метода .parent
.
parent_element_h1 = value.css("h1").first.parent
parent_element_h2 = value.css("h2").first.parent