【问题标题】:Selecting Paragraphs in Groups with xPath in Ruby在 Ruby 中使用 xPath 选择组中的段落
【发布时间】:2012-11-03 09:09:53
【问题描述】:

我目前正在使用 Ruby 和 xPath 进行一个小型网络抓取项目。不幸的是,该网站的结构非常糟糕,这导致我遇到了一个小问题:

<h3>Relevant Headline</h3>
<p class="class_a class_b">Content starts in this paragraph...</p>
<p class="class_a ">...but this content belongs to the preceding paragraph</p>
<p class="class_a class_b">Content starts in this paragraph...</p>
<p class="class_a ">...but this content belongs to the preceding paragraph</p>
<h3>Some other Headline</h3>

如您所见,有 2 个 h3-Tags 框住了多个 p-tags。我希望选择所有带框的 p-tags。我已经找到了以下 xPath 来做到这一点:

h3[contains(text(),"Relevant")]/following-sibling::p[1 = count(preceding-sibling::h3[1] | ../h3[contains(text(),"Relevant")])]

但现在困难来了:上面的两段属于同一段。带有 class_b 的段落(第一个)开始一个新的数据条目,下一个(第二个)属于该条目。 3和4是一样的。问题是:有时 3 个段落属于一起,有时 4 个段落属于一起,但大多数时候有一对段落属于一起。

如何在 Ruby 中按组选择这些内部段落并将它们组合成一个字符串?

【问题讨论】:

  • 你在项目中使用了什么 gem?解决方案必须是纯 xpath 吗?
  • 我切换到 xpath 是因为我找到了用于选择上面两个标题之间的段落的 xpath 解决方案。我更喜欢用 nokogiri 及其 css 方法进行刮擦。但是如果我的问题需要 xpath,我会使用它(即使我很难理解,至少对我来说是这样;))

标签: ruby xpath screen-scraping web-scraping nokogiri


【解决方案1】:

如果您不介意结合使用 xpath 和 nokogiri,您可以这样做:

paragraph_text = Array.new
doc.xpath('//p[preceding-sibling::h3[1][contains(text(), "Relevant")]]').each do |p|
    if p.attribute('class').text.include?('class_b')
        paragraph_text << p.content
    else
        paragraph_text[-1] += p.text
    end
end
puts paragraph_text
#=> ["Content starts in this paragraph......but this content belongs to the preceding paragraph",  "Content starts in this paragraph......but this content belongs to the preceding paragraph"]

基本上 xpath 用于获取段落标签。然后,使用 nokogiri/ruby 遍历段落并制定字符串。

【讨论】:

  • 嗨贾斯汀,谢谢你的回答,这对我很有帮助。我不明白“paragraph_text[-1]”是什么意思?数组中的索引 [-1] 是什么?
  • 在数组中,[-1] 获取最后一个元素。这与通常通过索引获取元素的方式相同。负值在某种意义上意味着倒退。在这里的代码上下文中,它表示将文本添加到带有“class_b”的最后一个字符串。
【解决方案2】:

可以使用 xpath 完成,但我认为使用 slice_before 将它们分组更容易:

doc.search('*').slice_before{|n| n.name == 'h3'}.each do |h3_group|
  h3_group.slice_before{|n| n[:class] && n[:class]['class_b']}.to_a[1..-1].each do |p_group|
    puts p_group.map(&:text) * ' '
  end
end

更新

使用 css 的另一种选择:

doc.search('p.class_b').each do |p|
  str, next_node = p.text, p
  while next_node = next_node.at('+ p:not([class*=class_b])')
    str += " #{next_node.text}"
  end
  puts str
end

【讨论】:

  • 您好 pguardiario,也感谢您的回答。这对我来说比贾斯汀的方法更具可读性。我不知道 slice_before 方法。这里还有一个问题:'to_a[1..-1]' 是做什么的?
  • 因为 slice_before 返回可枚举,to_a 使它成为一个数组,所以我们可以选择一个范围。 [1..-1] 表示跳过第一个元素,即 h3。
猜你喜欢
  • 1970-01-01
  • 2016-09-16
  • 1970-01-01
  • 2021-05-10
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2018-03-28
  • 1970-01-01
相关资源
最近更新 更多