16

我有一个 rake 任务,负责对数百万个 URL 进行批处理。因为这个过程需要很长时间,我有时会发现我尝试处理的 URL 不再有效—— 404、站点关闭等等。

当我最初写这篇文章时,基本上只有一个站点在处理过程中会不断关闭,所以我的解决方案是使用open-uri,拯救任何产生的异常,稍等片刻,然后重试。

当数据集较小时,这工作得很好,但现在时间过去了,我发现 URL 不再存在并产生 404。

使用 404 的情况,当发生这种情况时,我的脚本只是坐在那里并无限循环——显然很糟糕。

我应该如何处理页面未成功加载的情况,更重要的是这如何适合我构建的“堆栈”?

我对这个和 Rails 很陌生,所以欢迎任何关于我在这个设计中可能出错的地方的意见!

这是一些显示我所拥有的匿名代码:

调用 MyHelperModule 的 rake 任务:

# lib/tasks/my_app_tasks.rake
namespace :my_app do
  desc "Batch processes some stuff @ a later time."
    task :process_the_batch => :environment do
      # The dataset being processed
      # is millions of rows so this is a big job 
      # and should be done in batches!
      MyModel.where(some_thing: nil).find_in_batches do |my_models|
        MyHelperModule.do_the_process my_models: my_models
      end
    end
  end
end

MyHelperModule 接受my_modelsActiveRecord 并做进一步的事情。它调用SomeClass

# lib/my_helper_module.rb
module MyHelperModule
  def self.do_the_process(args = {})
    my_models = args[:my_models]

    # Parallel.each(my_models, :in_processes => 5) do |my_model|
    my_models.each do |my_model|
      # Reconnect to prevent errors with Postgres
      ActiveRecord::Base.connection.reconnect!
      # Do some active record stuff

      some_var = SomeClass.new(my_model.id)

      # Do something super interesting,
      # fun,
      # AND sexy with my_model
    end
  end
end

SomeClass将通过以下方式访问网络WebpageHelper并处理一个页面:

# lib/some_class.rb
require_relative 'webpage_helper'
class SomeClass
  attr_accessor :some_data

  def initialize(arg)
    doc = WebpageHelper.get_doc("http://somesite.com/#{arg}")
      # do more stuff
  end
end

WebpageHelper是捕获异常并在 404 的情况下启动无限循环的地方:

# lib/webpage_helper.rb
require 'nokogiri'
require 'open-uri'

class WebpageHelper
  def self.get_doc(url)
    begin
      page_content = open(url).read
      # do more stuff
    rescue Exception => ex
      puts "Failed at #{Time.now}"
      puts "Error: #{ex}"
      puts "URL: " + url
      puts "Retrying... Attempt #: #{attempts.to_s}"
      attempts = attempts + 1
      sleep(10)
      retry
    end
  end
end
4

8 回答 8

8

TL;博士

使用带外错误处理和不同的概念抓取模型来加速操作。

例外不适用于常见情况

还有许多其他答案可以解决如何为您的用例处理异常。我采取了不同的方法,说处理异常从根本上来说是错误的方法,原因有很多。

  1. 在他的书Exceptional Ruby中,Avdi Grimm 提供了一些基准,显示异常的性能比使用其他编码技术(如提前返回)慢约 156%。

  2. The Pragmatic Programmer: From Journeyman to Master中,作者指出“[E] 异常应该为意外事件保留。” 在您的情况下,404 错误是不可取的,但一点也不意外——事实上,处理 404 错误是一个核心考虑因素!

简而言之,您需要一种不同的方法。最好,替代方法应提供带外错误处理并防止您的进程在重试时阻塞。

一种选择:更快、更原子的过程

您在这里有很多选择,但我要推荐的一个是将 404 状态代码作为正常结果处理。这允许您“快速失败”,但也允许您稍后重试页面或从队列中删除 URL。

考虑这个示例模式:

ActiveRecord::Schema.define(:version => 20120718124422) do
  create_table "webcrawls", :force => true do |t|
    t.text     "raw_html"
    t.integer  "retries"
    t.integer  "status_code"
    t.text     "parsed_data"
    t.datetime "created_at",  :null => false
    t.datetime "updated_at",  :null => false
  end
end

这里的想法是,您只需将整个抓取视为一个原子过程。例如:

  • 你拿到页面了吗?

    太好了,存储原始页面和成功状态代码。您甚至可以稍后解析原始 HTML,以便尽快完成您的抓取。

  • 你收到404了吗?

    好的,存储错误页面和状态码。快速前进!

当您的过程完成对 URL 的抓取后,您可以使用 ActiveRecord 查找来查找最近返回 404 状态的所有 URL,以便您可以采取适当的措施。也许您想重试该页面,记录一条消息,或者只是从要抓取的 URL 列表中删除该 URL——“适当的操作”取决于您。

通过跟踪您的重试次数,您甚至可以区分暂时性错误和更永久的错误。这允许您为不同的操作设置阈值,具体取决于给定 URL 的抓取失败频率。

这种方法还具有利用数据库来管理并发写入和在进程之间共享结果的额外好处。这将允许您在多个系统或进程之间分配工作(可能使用消息队列或分块数据文件)。

最后的想法:扩大规模和扩大规模

在初始抓取期间花费更少的时间重试或错误处理应该会显着加快您的流程。但是,有些任务对于单机或单进程方法来说太大了。如果您的流程加速仍然不足以满足您的需求,您可能需要考虑使用以下一项或多项的线性度较低的方法:

  • 分叉后台进程。
  • 使用dRuby在多个进程或机器之间分配工作。
  • 通过使用GNU 并行生成多个外部进程来最大化核心使用率。
  • 其他不是单一的顺序过程的东西。

优化应用程序逻辑应该足以满足常见情况,但如果不是,则可以扩展到更多进程或扩展到更多服务器。横向扩展肯定会做更多的工作,但也会扩展您可用的处理选项。

于 2012-07-18T13:31:30.890 回答
5

Curb有一种更简单的方法可以做到这一点,并且可以成为更好(更快)的选择,而不是open-uri.

错误遏制报告(您可以从中拯救并做一些事情:

http://curb.rubyforge.org/classes/Curl/Err.html

遏制宝石: https ://github.com/taf2/curb

示例代码:

def browse(url)
  c = Curl::Easy.new(url)
  begin
    c.connect_timeout = 3
    c.perform
    return c.body_str
  rescue Curl::Err::NotFoundError
    handle_not_found_error(url)
  end
end

def handle_not_found_error(url)
  puts "This is a 404!"
end
于 2012-07-11T16:59:59.210 回答
3

你可以提高 404 的:

rescue Exception => ex
  raise ex if ex.message['404']
  # retry for non-404s
end
于 2012-07-09T04:39:19.997 回答
3

我实际上有一个 rake 任务,它做了一些非常相似的事情。这是我处理 404 的要点,你可以很容易地应用它。

基本上你想要做的是使用下面的代码作为过滤器并创建一个日志文件来存储你的错误。因此,在您抓取网站并对其进行处理之前,您首先要执行以下操作:

因此,在您的文件中创建/实例化一个日志文件:

@logfile = File.open("404_log_#{Time.now.strftime("%m/%d/%Y")}.txt","w")
# #{Time.now.strftime("%m/%d/%Y")} Just includes the date into the log in case you want
# to run diffs on your log files.

然后将您的 WebpageHelper 类更改为如下所示:

class WebpageHelper
  def self.get_doc(url)
    response = Net::HTTP.get_response(URI.parse(url))
    if (response.code.to_i == 404) notify_me(url)
    else
    page_content = open(url).read
    # do more stuff
    end
  end
end

这样做是对页面执行 ping 操作以获取响应代码。我包含的 if 语句是检查响应代码是否为 404,如果它运行 notify_me 方法,否则像往常一样运行您的命令。我只是随意创建了那个 notify_me 方法作为例子。在我的系统上,我将它写入 txt 文件,它会在完成后通过电子邮件发送给我。您可以使用类似的方法查看其他响应代码。

通用日志记录方法:

def notify_me(url)
  puts "Failed at #{Time.now}"
  puts "URL: " + url
  @logfile.puts("There was a 404 error for the site #{url} at #{Time.now}.")
end
于 2012-07-09T04:47:23.697 回答
3

这一切都取决于你想用 404 做什么。

让我们假设您只想吞下它们。pguardiario 的部分回应是一个好的开始:您可以引发错误,然后重试几次......

# lib/webpage_helper.rb
require 'nokogiri'
require 'open-uri'

class WebpageHelper
  def self.get_doc(url)
    attempt_number = 0
    begin
      attempt_number = attempt_number + 1
      page_content = open(url).read
      # do more stuff
    rescue Exception => ex
      puts "Failed at #{Time.now}"
      puts "Error: #{ex}"
      puts "URL: " + url
      puts "Retrying... Attempt #: #{attempts.to_s}"
      sleep(10)
      retry if attempt_number < 10 # Try ten times.
    end
  end
end

如果你遵循这个模式,它只会默默地失败。什么都不会发生,它会在尝试十次后继续前进。我通常会认为这是一个糟糕的计划(tm)。与其默默地失败,我会在救援条款中使用类似的东西:

    rescue Exception => ex
      if attempt_number < 10 # Try ten times.
        retry 
      else
        raise "Unable to contact #{url} after ten tries."
      end
    end

然后在 MyHelperModule#do_the_process 中抛出类似的东西(你必须更新你的数据库以获得错误和 error_message 列):

    my_models.each do |my_model|
      # ... cut ...

      begin
        some_var = SomeClass.new(my_model.id)
      rescue Exception => e
        my_model.update_attributes(errors: true, error_message: e.message)
        next
      end

      # ... cut ...
    end

这可能是使用您目前拥有的最简单和最优雅的方式。也就是说,如果你在一个大规模的 rake 任务中处理这么多的请求,那不是很优雅。如果出现问题,您无法重新启动它,它会长时间占用系统上的单个进程等。如果最终出现任何内存泄漏(或无限循环!),您会发现自己处于一个不能只说“继续”。您可能应该使用某种排队系统,例如 Resque 或 Sidekiq,或 Delayed Job(尽管听起来您最终排队的项目比 Delayed Job 乐意处理的要多)。如果您正在寻找一种更有说服力的方法,我建议您深入研究这些方法。

于 2012-07-11T16:18:27.243 回答
2

在从抓取创建新的 SomeClass 时,我不会使用总是返回对象的新实例的初始化,而是使用类方法来创建实例。除了 nokogiri 抛出的异常之外,我在这里没有使用异常,因为听起来没有其他东西应该进一步冒泡,因为您只希望记录这些异常,否则会被忽略。你提到记录异常——你只是记录标准输出的内容吗?我会像你一样回答...

    # lib/my_helper_module.rb
module MyHelperModule
  def self.do_the_process(args = {})
    my_models = args[:my_models]

    # Parallel.each(my_models, :in_processes => 5) do |my_model|
    my_models.each do |my_model|
      # Reconnect to prevent errors with Postgres
      ActiveRecord::Base.connection.reconnect!

      some_object = SomeClass.create_from_scrape(my_model.id)

    if some_object
      # Do something super interesting if you were able to get a scraping
      # otherwise nothing happens (except it is noted in our logging elsewhere)
    end

  end
end

你的SomeClass:

# lib/some_class.rb
require_relative 'webpage_helper'
class SomeClass
  attr_accessor :some_data

  def initialize(doc)
    @doc = doc
  end

  # could shorten this, but you get the idea...
  def self.create_from_scrape(arg)
    doc = WebpageHelper.get_doc("http://somesite.com/#{arg}")
    if doc
      return SomeClass.new(doc)
    else
      return nil
    end      
  end

end

您的网页助手:

# lib/webpage_helper.rb
require 'nokogiri'
require 'open-uri'

class WebpageHelper
  def self.get_doc(url)
    attempts = 0 # define attempts first in non-block local scope before using it
    begin
      page_content = open(url).read
      # do more stuff
    rescue Exception => ex
      attempts += 1
      puts "Failed at #{Time.now}"
      puts "Error: #{ex}"
      puts "URL: " + url
      if attempts < 3 
        puts "Retrying... Attempt #: #{attempts.to_s}"
        sleep(10)
        retry
      else
        return nil
      end
    end

  end
end
于 2012-07-12T12:43:36.410 回答
2

关于您遇到的问题,您可以执行以下操作:


class WebpageHelper
  def self.get_doc(url)
    retried = false
    begin
      page_content = open(url).read
      # do more stuff
    rescue OpenURI::HTTPError => ex
      unless ex.io.status.first.to_i == 404
        log_error ex.message
        sleep(10)
        unless retried
          retried = true
          retry
        end
      end
    # FIXME: needs some refactoring
    rescue Exception => ex
      puts "Failed at #{Time.now}"
      puts "Error: #{ex}"
      puts "URL: " + url
      puts "Retrying... Attempt #: #{attempts.to_s}"
      attempts = attempts + 1
      sleep(10)
      retry
    end
  end
end

但我会重写整个事情,以便与 Typhoeus 进行并行处理:

https://github.com/typhoeus/typhoeus

我将分配一个回调块来处理返回的数据,从而将页面的获取和处理解耦。

类似的东西:



def on_complete(response)
end

def on_failure(response)
end

def run
  hydra = Typhoeus::Hydra.new
  reqs = urls.collect do |url|
    Typhoeus::Request.new(url).tap { |req|
      req.on_complete = method(:on_complete).to_proc }
      hydra.queue(req)
    }
  end
  hydra.run
  # do something with all requests after all requests were performed, if needed
end

于 2012-07-13T19:19:14.347 回答
2

我认为每个人对这个问题的评论都是正确的。这个页面上有很多很好的信息。这是我收集这个非常丰厚的赏金的尝试。话虽如此,对所有答案都 +1。

如果您只关心使用 OpenURI 的 404,您可以只处理这些类型的异常

# lib/webpage_helper.rb
rescue OpenURI::HTTPError => ex
  # handle OpenURI HTTP Error!
rescue Exception => e
  # similar to the original
  case e.message
      when /404/ then puts '404!'
      when /500/ then puts '500!'
      # etc ... 
  end
end

如果您想要更多,您可以对每种错误类型进行不同的执行处理。

# lib/webpage_helper.rb
rescue OpenURI::HTTPError => ex
  # do OpenURI HTTP ERRORS
rescue Exception::SyntaxError => ex
  # do Syntax Errors
rescue Exception => ex
  # do what we were doing before

我也喜欢其他帖子中关于尝试次数的内容。确保它不是无限循环。

我认为经过多次尝试后,rails 要做的事情是记录、排队和/或发送电子邮件。

要登录,您可以使用

webpage_logger = Log4r::Logger.new("webpage_helper_logger")
# somewhere later
# ie 404
  case e.message
  when /404/ 
    then 
      webpage_logger.debug "debug level error #{attempts.to_s}"
      webpage_logger.info "info level error #{attempts.to_s}"
      webpage_logger.fatal "fatal level error #{attempts.to_s}"

排队的方式有很多种。我认为最好的一些是faye和resque。这是两者的链接:http: //faye.jcoglan.com/ https://github.com/defunkt/resque/

队列就像一条线一样工作。信不信由你,英国人称之为“队列”(你知道的越多)。因此,使用排队服务器,您可以排列许多请求,当您尝试发送请求的服务器返回时,您可以在队列中使用您的请求锤击该服务器。从而迫使他们的服务器再次停机,但希望随着时间的推移,他们会升级他们的机器,因为他们一直在崩溃。

最后是电子邮件,rails 也可以救援(不是 resque)......这是 ActionMailer 上 Rails 指南的链接:http: //guides.rubyonrails.org/action_mailer_basics.html

你可以有这样的邮件

class SomeClassMailer <  ActionMailer::Base
  default :from => "notifications@example.com"
def self.mail(*args)
 ...
# then later 
rescue Exception => e
  case e.message
    when /404/ && attempts == 3
      SomeClassMailer.mail(:to => "broken@example.com", :subject => "Failure ! #{attempts}")
于 2012-07-18T05:00:49.860 回答