我如何抓取“堆叠”处理404错误?

时间:2012-07-09 03:31:29

标签: ruby-on-rails ruby exception open-uri

我有一个rake任务,负责对数百万个URL进行批处理。因为这个过程需要很长时间,所以我有时会发现我正在尝试处理的URL不再有效 - 404,网站已关闭,无论如何。

当我最初写这篇文章时,基本上只有一个网站会在处理过程中不断下降,所以我的解决方案就是使用open-uri,挽救所产生的任何异常,等待一段时间,然后重试。

当数据集较小时,这种情况很好但是现在已经有很多时间了,我发现网址不再存在并产生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_models并使用ActiveRecord做更多事情。它调用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

8 个答案:

答案 0 :(得分:7)

TL; DR

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

异常不适用于常见情况

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

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

  2. The Pragmatic Programmer:From Journeyman to Master 中,作者声明“[E] xceptions应该保留用于意外事件。”在您的情况下,404错误是不可取的,但并不是意料之外的 - 事实上,处理404错误是核心考虑因素!

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

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

    这里有很多选项,但我要推荐的是处理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
    

    这里的想法是你只需将整个 scrape视为一个原子过程。例如:

    • 你收到了这个页面吗?

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

    • 你有404?

      没问题,存储错误页面和状态代码。快点继续!

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

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

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

    最后的想法:扩大规模

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

    • 分工背景流程。
    • 使用dRuby在多个进程或计算机之间拆分工作。
    • 使用GNU parallel生成多个外部流程,最大限度地提高核心使用率。
    • 其他不是单一的,顺序的过程。

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

答案 1 :(得分: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

答案 2 :(得分:3)

你可以举起404:

rescue Exception => ex
  raise ex if ex.message['404']
  # retry for non-404s
end

答案 3 :(得分: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

答案 4 :(得分: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,或延迟工作(虽然听起来你有更多的项目,你最终排队比延迟工作乐意处理)。如果你正在寻找一种更有说服力的方法,我建议深入研究这些方法。

答案 5 :(得分:2)

我不是使用初始化,它总是返回一个对象的新实例,当从抓取创建一个新的SomeClass时,我使用类方法来创建实例。我没有在nokogiri投掷之外使用异常,因为它听起来没有别的应该进一步冒泡,因为你只是希望记录这些,但是否则会被忽略。你提到记录异常 - 你只是记录了什么去了stdout?我会回答你好像......

    # 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

您的WebPageHelper:

# 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

答案 6 :(得分: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

答案 7 :(得分: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

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

# 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

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

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

要记录您可以使用

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/

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

最后发送电子邮件,铁路也救援(不是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}")