【问题标题】:Why do I get a blank record for every table row?为什么我得到每个表格行的空白记录?
【发布时间】:2012-01-11 16:09:52
【问题描述】:

感谢另一个 SO 问题/答案,我有以下代码:

page = agent.page.search("table tbody tr").each do |row|
  time        = row.css("td:nth-child(1)").text.strip
  source      = row.css("td:nth-child(2)").text.strip
  destination = row.css("td:nth-child(3)").text.strip
  duration    = row.css("td:nth-child(4)").text.strip
  Call.create!(:time => time, :source => source, :destination => destination, :duration => duration)
end

它运行良好,当我运行 rake 任务时,它会正确地将数据放入 Rails 应用程序中正确的表行中,但是,由于某种原因,在为一行成功创建记录后,它也会创建一个空白记录。

我想不通。从代码的外观来看,它在每一行中发出create! 命令。

您可以在https://gist.github.com/1574942 和查看完整的 rake 任务 导致此代码的另一个问题是“Parse html into Rails without new record every time?”。

【问题讨论】:

  • 我怀疑 HTML 中的某些内容会导致中间循环,但不会填充值,例如没有内容的其他行。
  • 我认为您可能是对的,我查看了远程网页上的 HTML,他们正在为每个分配了一个类的表行添加一个包装 。我想知道是否有任何方法可以让脚本跳过空行?
  • 添加您看到的 HTML 示例,其中包含两行和单元格。那我们就可以解决问题了。没有它,我们只是在猜测。
  • 保护它。请参阅下面的答案。

标签: html parsing screen-scraping nokogiri


【解决方案1】:

根据评论:

我认为您可能是对的,我查看了远程网页上的 HTML,他们正在为每个分配了一个类的表格行添加一个环绕。我想知道是否有任何方法可以让脚本跳过空行?

如果您看到如下 HTML 结构:

<table>
  <tbody>
    <tr>
      <tr>
        <td>time</td>
        <td>source</td>
        <td>destination</td>
        <td>duration</td>
      </tr>
    </tr>
  </tbody>
</table>

那么这就说明问题了:

require 'nokogiri'
require 'pp'

html = '<table><tbody><tr><tr><td>time</td><td>source</td><td>destination</td><td>duration</td></tr></tr></tbody></table>'
doc = Nokogiri::HTML(html)
page = doc.search("table tbody tr").each do |row|
  time        = row.css("td:nth-child(1)").text.strip
  source      = row.css("td:nth-child(2)").text.strip
  destination = row.css("td:nth-child(3)").text.strip
  duration    = row.css("td:nth-child(4)").text.strip
  hash = {
    :time        => time,
    :source      => source,
    :destination => destination,
    :duration    => duration 
  }
  pp hash
end

输出:

{:time=>"", :source=>"", :destination=>"", :duration=>""}
{:time=>"time",
 :source=>"source",
 :destination=>"destination",
 :duration=>"duration"}

您得到空白行的原因是 HTML 格式错误。外部&lt;tr&gt; 不应该在那里。修复很简单,也适用于正确的 HTML。

另外,内部的css 访问也不是很正确,但为什么会这样是微妙的。我会解决的。

为了解决第一个问题,我们将添加一个条件测试:

page = doc.search("table tbody tr").each do |row|

变成:

page = doc.search("table tbody tr").each do |row|
  next if (!row.at('td'))

运行后,现在的输出是:

{:time=>"time",
 :source=>"source",
 :destination=>"destination",
 :duration=>"duration"}

这确实是解决问题所需的全部内容,但是代码中的某些事情正在以艰难的方式做事,需要一些“解释”,但首先是代码更改:

发件人:

time        = row.css("td:nth-child(1)").text.strip
source      = row.css("td:nth-child(2)").text.strip
destination = row.css("td:nth-child(3)").text.strip
duration    = row.css("td:nth-child(4)").text.strip

改为:

time, source, destination, duration = row.search('td').map{ |td| td.text.strip }

运行该代码会输出您想要的结果:

{:time=>"time",
 :source=>"source",
 :destination=>"destination",
 :duration=>"duration"}

所以事情仍然很糟糕。

这是您原始代码的问题:

csssearch 的别名。 Nokogiri 为两者返回一个 NodeSet。 text 将从空的 NodeSet 中返回一个空字符串,对于每个查看外部 &lt;tr&gt;row.css("td:nth-child(...)").text.strip 调用,您都会获得该字符串。所以,Nokogiri 没有默默地做你想做的事,因为它在语法上和逻辑上都是正确的,因为你告诉它要做什么;它只是未能达到您的期望。

使用at 或其别名之一,如css_at,查找第一个匹配的访问器。因此,理论上我们可以继续使用 row.at("td:nth-child(1)").text.strip 并为每个访问器分配多个分配,这会立即表明您的 HTML 存在问题,因为 text 会炸毁。但这还不够禅意。

相反,我们可以使用 map 遍历 NodeSet 中返回的单元格,让它收集所需的单元格内容并剥离它们,然后对变量进行并行赋值:

time, source, destination, duration = row.search('td').map{ |td| td.text.strip }

再次运行:

require 'nokogiri'
require 'pp'

html = '<table><tbody><tr><tr><td>time</td><td>source</td><td>destination</td><td>duration</td></tr></tr></tbody></table>'
doc = Nokogiri::HTML(html)
page = doc.search("table tbody tr").each do |row|
  next if (!row.at('td'))

  time, source, destination, duration = row.search('td').map{ |td| td.text.strip }

  hash = {
    :time        => time,
    :source      => source,
    :destination => destination,
    :duration    => duration 
  }
  pp hash
end

给我:

{:time=>"time",
 :source=>"source",
 :destination=>"destination",
 :duration=>"duration"}

将它改造成你的代码,你会得到:

page = agent.page.search("table tbody tr").each do |row|
  next if (!row.at('td'))
  time, source, destination, duration = row.search('td').map{ |td| td.text.strip }
  Call.create!(:time => time, :source => source, :destination => destination, :duration => duration)
end

你可能不需要page =

【讨论】:

  • 哇,很棒的文章 - 谢谢!我现在会仔细阅读并尝试您的建议。
猜你喜欢
相关资源
最近更新 更多
热门标签