【问题标题】:How do I match successive nodes with Nokogiri?如何将连续节点与 Nokogiri 匹配?
【发布时间】:2014-08-14 20:36:20
【问题描述】:

我需要使用 Nokogiri 和 CSS 或 XPath 选择器来匹配来自以下 HTML 的文本。它应该从<div> 标记开始匹配,其中class="propsBar" 并在<div> 标记的结束侧结束匹配,其中class="oddsInfoBottom"。应该这样做以识别与此模式的所有匹配项:

<div class="timeBar"></div>
<div class="propsBar"></div>
<div class="oddsInfoTop"></div>
<div class="oddsInfoBottom"></div>
<!--
 BUY POINTS 
-->
<input id="events[X2036-907-Yes-No-081414]" type="hidden" value="X2036-907-Yes-No-081414^No^Yes^Nationals (S Strasburg) @ Met…l there be a score in the 1st Inning?^8/14/2014^7:10 PM^2036" name="events[X2036-907-Yes-No-081414]"></input>
<div class="timeBar"></div>
<div class="propsBar"></div>
<div class="oddsInfoTop"></div>
<div class="oddsInfoBottom"></div>
<!--
 BUY POINTS 
-->
<input id="events[X2036-915-Yes-No-081414]" type="hidden" value="X2036-915-Yes-No-081414^No^Yes^Astros (S Feldman) @ Red Sox …l there be a score in the 1st Inning?^8/14/2014^7:10 PM^2036" name="events[X2036-915-Yes-No-081414]"></input>
<div class="timeBar"></div>
<div class="propsBar"></div>
<div class="oddsInfoTop"></div>
<div class="oddsInfoBottom"></div>
<!--
 BUY POINTS 
-->
<input id="events[X2036-917-Yes-No-081414]" type="hidden" value="X2036-917-Yes-No-081414^No^Yes^Rays (J Odorizzi) @ Rangers (…l there be a score in the 1st Inning?^8/14/2014^8:05 PM^2036" name="events[X2036-917-Yes-No-081414]"></input>
<div class="timeBar"></div>

上面的 HTML 应该返回三个匹配项。

到目前为止,我能够做到这一点的唯一方法是:

one = html.xpath("//div[@class='propsBar']")
two = html.xpath("//div[@class='oddsInfoTop']")
three = html.xpath("//div[@class='oddsInfoBottom']")

one.zip(two, three).flatten.each_slice(3).map(&:join)

这样做的缺点是只返回文本,不再作为 Nokogiri 元素。此外,我认为这样解析很危险,如果页面有不同数量的匹配one, two, three的元素,它将中断。

【问题讨论】:

  • 您尝试过哪些 XPath 查询?
  • “要求很高”?期望您提供已尝试过的示例,因为它表明您不只是在寻找其他人为您编写的代码。我们修复您的代码也比我们编写代码和您将其硬塞到您编写的任何内容中更容易。如果您走错了路,它还可以帮助我们避免问题。 Stack Overflow 不仅是一个“解决我的问题”网站,还是一个“帮助我通过最佳实践学习和成长”的网站。 meta.stackoverflow.com/a/254575/128421 是一个很好的元答案,尤其是第 2 项。

标签: html css ruby xpath nokogiri


【解决方案1】:

我会这样写:

require 'nokogiri'

doc = Nokogiri::HTML(<<EOT)
<div class="timeBar"></div>
<div class="propsBar"></div>
<div class="oddsInfoTop"></div>
<div class="oddsInfoBottom"></div>
<!--
 BUY POINTS
-->
<div class="timeBar"></div>
<div class="propsBar"></div>
<div class="oddsInfoTop"></div>
<div class="oddsInfoBottom"></div>
<!--
 BUY POINTS
-->
<div class="timeBar"></div>
<div class="propsBar"></div>
<div class="oddsInfoTop"></div>
<div class="oddsInfoBottom"></div>
<!--
 BUY POINTS
-->
<div class="timeBar"></div>
EOT

found_nodes = doc.search('div.propsBar').map{ |node|
  nodes = [node]
  loop do
    node = node.next_sibling
    nodes << node
    break if node['class'] == 'oddsInfoBottom'
  end
  nodes
}

(请注意,我删除了 &lt;input&gt; 标记,因为它们只会使输入 HTML 变得混乱。当您提供输入数据时,请删除所有噪音。)

运行将找到的节点作为数组数组返回。每个子数组包含顺序遍历兄弟链后找到的各个节点:

require 'pp'
pp found_nodes
# >> [[#(Element:0x3ff00a4936a0 {
# >>     name = "div",
# >>     attributes = [
# >>       #(Attr:0x3ff00a037c28 { name = "class", value = "propsBar" })]
# >>     }),
# >>   #(Text "\n"),
# >>   #(Element:0x3ff00a49363c {
# >>     name = "div",
# >>     attributes = [
# >>       #(Attr:0x3ff00a03629c { name = "class", value = "oddsInfoTop" })]
# >>     }),
# >>   #(Text "\n"),
# >>   #(Element:0x3ff00a4935b0 {
# >>     name = "div",
# >>     attributes = [
# >>       #(Attr:0x3ff00a4668f8 { name = "class", value = "oddsInfoBottom" })]
# >>     })],
# >>  [#(Element:0x3ff00a49354c {
# >>     name = "div",
# >>     attributes = [
# >>       #(Attr:0x3ff00a45c808 { name = "class", value = "propsBar" })]
# >>     }),
# >>   #(Text "\n"),
# >>   #(Element:0x3ff00a4934e8 {
# >>     name = "div",
# >>     attributes = [
# >>       #(Attr:0x3ff00a45b084 { name = "class", value = "oddsInfoTop" })]
# >>     }),
# >>   #(Text "\n"),
# >>   #(Element:0x3ff00a49345c {
# >>     name = "div",
# >>     attributes = [
# >>       #(Attr:0x3ff00a8710ec { name = "class", value = "oddsInfoBottom" })]
# >>     })],
# >>  [#(Element:0x3ff00a4933f8 {
# >>     name = "div",
# >>     attributes = [
# >>       #(Attr:0x3ff00a4979d0 { name = "class", value = "propsBar" })]
# >>     }),
# >>   #(Text "\n"),
# >>   #(Element:0x3ff00a493394 {
# >>     name = "div",
# >>     attributes = [
# >>       #(Attr:0x3ff00a47e188 { name = "class", value = "oddsInfoTop" })]
# >>     }),
# >>   #(Text "\n"),
# >>   #(Element:0x3ff00a493308 {
# >>     name = "div",
# >>     attributes = [
# >>       #(Attr:0x3ff00a458f00 { name = "class", value = "oddsInfoBottom" })]
# >>     })]]

请记住,解析后,文档是节点的链表。如果原始 XML 或 HTML 中有换行符,则会有一个 Text 节点,其中至少包含一个换行符(“\n”)。因为它是一个列表,我们可以分别使用next_siblingprevious_sibling 向前和向后移动。这使得真的很容易抓取小块,即使它们不是包含您想要的内容的块标签。

如果您希望返回的值类似于 searchcssxpath 方法的输出,则内部变量 nodes 需要从 Array 更改为 NodeSet

found_nodes = doc.search('div.propsBar').map{ |node|
  nodes = Nokogiri::XML::NodeSet.new(doc, [node])
  loop do
    node = node.next_sibling
    nodes << node
    break if node['class'] == 'oddsInfoBottom'
  end
  nodes
}

require 'pp'
pp found_nodes.map(&:to_html)

运行结果:

# >> ["<div class=\"propsBar\"></div>\n<div class=\"oddsInfoTop\"></div>\n<div class=\"oddsInfoBottom\"></div>",
# >>  "<div class=\"propsBar\"></div>\n<div class=\"oddsInfoTop\"></div>\n<div class=\"oddsInfoBottom\"></div>",
# >>  "<div class=\"propsBar\"></div>\n<div class=\"oddsInfoTop\"></div>\n<div class=\"oddsInfoBottom\"></div>"]

最后,请注意我使用了 CSS 选择器而不是 XPath。我更喜欢它们,因为它们通常更具可读性和简洁性。 XPath 更强大,因为它是为剖析 XML 而设计的,它通常可以完成我们在 Ruby 中必须完成的所有繁重工作,而 CSS 选择器只会让我们接近我们想要的。使用可以为您完成工作的任何一个,并考虑更易于阅读和维护的内容。

【讨论】:

  • 这行得通。我会接受这个答案。我认为你的作品和 7stud 的作品一样,但至少对我来说,你的作品更简洁、直观。谢谢。
【解决方案2】:

我需要使用 Nokogiri、CSS 选择器或 Xpath 来匹配来自 跟随 HTML。它应该从一个标签开始匹配 class="propsBar" 并在标签的结尾处结束匹配 其中 class="oddsInfoBottom"

但它们都是一样的,例如:

<div class="propsBar"></div>
<div class="oddsInfoTop"></div>
<div class="oddsInfoBottom"></div>

好的,开始吧:

require 'nokogiri'

doc = Nokogiri::HTML(File.read("xml3.xml"))

doc.css('div.propsBar').each do |div|
  puts div.to_html
  current_node = div

  while current_node = current_node.next_element
    puts current_node.to_html

    if current_node.has_attribute?'class'
      if current_node['class'].match /\b oddsInfoBottom \b/xm
        puts "-" * 10
        break  #Go get a new starting tag
      end
    end
  end
end

--output:--
<div class="propsBar"></div>
<div class="oddsInfoTop"></div>
<div class="oddsInfoBottom"></div>
----------
<div class="propsBar"></div>
<div class="oddsInfoTop"></div>
<div class="oddsInfoBottom"></div>
----------
<div class="propsBar"></div>
<div class="oddsInfoTop"></div>
<div class="oddsInfoBottom"></div>
----------

但这有只返回文本的缺点,不再作为 Nokogiri 元素。

require 'nokogiri'

doc = Nokogiri::HTML(File.read("xml3.xml"))

groups = []
this_group = []

doc.css('div.propsBar').each do |tag|
  this_group << tag
  current_tag = tag

  while current_tag = current_tag.next_element
    this_group << current_tag

    if current_tag.has_attribute?'class'
      if current_tag['class'].match /\b oddsInfoBottom \b/xm
        groups << this_group
        this_group = []
        break
      end
    end
  end

end


groups.each do |group|
  group.each do |tag|
    puts tag.to_html
  end
  puts '-' * 10
end

--output:--
<div class="propsBar"></div>
<div class="oddsInfoTop"></div>
<div class="oddsInfoBottom"></div>
----------
<div class="propsBar"></div>
<div class="oddsInfoTop"></div>
<div class="oddsInfoBottom"></div>
----------
<div class="propsBar"></div>
<div class="oddsInfoTop"></div>
<div class="oddsInfoBottom"></div>
----------

【讨论】:

    【解决方案3】:

    使用+:

    doc.search('.propsBar').each do |props_bar|
      odds_info_top = props_bar.at('+ .oddsInfoTop')
      puts props_bar.text, odds_info_top.text
    end
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2018-08-20
      • 1970-01-01
      • 2019-11-03
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2012-12-28
      相关资源
      最近更新 更多