【问题标题】:How should my scraping "stack" handle 404 errors?我的抓取“堆栈”应该如何处理 404 错误?
【发布时间】:2012-07-08 11:33:21
【问题描述】:

我有一个 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_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

【问题讨论】:

  • 那么你想用 404 做什么。默默地忽略它们,记录它们,或者别的什么?
  • 我想注销它们,忽略,然后继续。

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


【解决方案1】:

TL;DR

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

例外不适用于常见情况

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

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

    李>
  2. 实用程序员:从熟练工到大师中,作者指出“[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 parallel 生成多个外部进程来最大化核心使用率。
  • 其他不是单一的顺序过程。

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

【讨论】:

  • 这是一个了不起的答案,我很抱歉这么久才回复,导致您错过了赏金。我认为你是绝对正确的。我一直在考虑这个错误,您建议的这种方法也会减少被抓取网站的负载。涉及到参考数据,刮擦可能必须重新拉动页面,如果我可以在本地获取它们,那就完美了。这也将使我能够区分不同的数据源及其页面内容。谢谢!
【解决方案2】:

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

【讨论】:

    【解决方案3】:

    你可以提高 404 的:

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

    【讨论】:

    • 我有点迷茫,一旦它被提出我应该做什么。就像我会在哪里处理它,我会做什么......等等。
    • 如果我在WebpageHelper 中处理这个问题,就不会使用它,比如SomeClassMyHelperModule 正在迭代的模块需要某种指示,事情已经向南了吗?
    • 你误会了什么。引发的异常表明事情已经恶化。
    • 你是对的,我是。那么在 MyHelperModule 中发生的循环是否需要监视异常?
    【解决方案4】:

    这完全取决于你想用 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 乐意处理的要多)。如果您正在寻找更有说服力的方法,我建议您深入研究这些方法。

    【讨论】:

    • 克里斯,谢谢。我还认为使用raise 似乎是个好主意,但我不确定它会去哪里以及rescue 它会去哪里。您认为使用自定义/特定类型的异常是否谨慎?
    • @SizzlePants:培养StandardError 的自定义子代而不是泛型通常是一种好习惯,因为在拯救另一个错误的同时引发一个错误通常会有所收获。此外,它允许您不打算出现的错误浮出水面,而不是被忽视。此外,通常会创建一个更高级别的伞类,用于捕获特定于您的应用程序(或 gem)的所有错误。示例:gem "Foo" 可能将 class Foo::Error &lt; StandardError 定义为伞形,并将 class Foo::TimeoutError &lt; Foo::Error 定义为要引发的实际错误。
    【解决方案5】:

    我实际上有一个 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
    

    【讨论】:

    • 谢谢。一旦遇到 404(或我可能想要的任何代码),我对我应该做什么有点迷茫。例如,我不知道如何优雅地使这个在堆栈中涓涓细流而不引起问题。几乎感觉我需要一个真正的例外来抓住然后做出选择,但我真的不确定这会去哪里。
    • 我希望澄清更有意义。基本上,您需要通过首先使用 Net::HTTP.get_response 方法检查页面生成的响应类型来使用代码中的异常来处理损坏的链接。所以这告诉你,如果它是 404,你可以忽略忽略该页面并转到下一个页面。您可以添加额外的 elsif (response.code.to_i == ERROR_CODE) 来处理更多类型的错误。
    • 我是这种方法的忠实拥护者,尤其是因为它可以避免异常并明确处理预期的结果(好或坏)。
    • 例外,在这种情况下,是一件好事。请注意,我们在这里都在谈论错误代码。如果是错误,则抛出异常。
    • 我认为这是 Colin 的观点,也是我的观点。在需要求助于异常处理之前处理从页面返回的代码。虽然,公平地说,我认为这可能只是一个偏好问题。
    【解决方案6】:

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

    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】:

      我认为每个人在这个问题上的 cmets 都是正确的。这个页面上有很多很好的信息。这是我收集这个非常丰厚的赏金的尝试。话虽如此,对所有答案 +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}")
      

      【讨论】:

        【解决方案8】:

        在从抓取创建新的 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
        

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2017-06-10
          • 2016-10-16
          • 2016-05-23
          • 1970-01-01
          • 2023-03-07
          • 1970-01-01
          相关资源
          最近更新 更多