16

何百万もの URL のバッチ処理を担当する rake タスクがあります。このプロセスには非常に時間がかかるため、処理しようとしている URL が無効になっていることに気付くことがあります (404、サイトのダウンなど)。

私が最初にこれを書いたとき、処理中に継続的にダウンするサイトは基本的に 1 つだけだったので、私の解決策は を使用し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経由で Web にアクセスし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

WebpageHelper404の場合、例外がキャッチされ、無限ループが開始される場所です。

# 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;DR

帯域外エラー処理と異なる概念のスクレイピング モデルを使用して、操作を高速化します。

例外は一般的な条件ではありません

ユースケースの例外を処理する方法に対処する他の多くの回答があります。ここでは、いくつかの理由から、例外を処理することは基本的に間違ったアプローチであると言って、別のアプローチをとっています。

  1. Avdi Grimm は著書Exceptional Rubyで、アーリー リターンなどの代替コーディング手法を使用する場合よりも例外のパフォーマンスが最大 156% 遅いことを示すいくつかのベンチマークを提供しています。

  2. The Pragmatic Programmer: From Journeyman to Masterで、著者は「[例外] は予期しないイベントのために予約する必要がある」と述べています。あなたの場合、404 エラーは望ましくありませんが、予期しないものではありません。実際、404 エラーの処理は重要な考慮事項です。

要するに、別のアプローチが必要です。できれば、別の方法で帯域外エラー処理を提供し、再試行時にプロセスがブロックされないようにする必要があります。

1 つの代替案: より高速でアトミックなプロセス

ここには多くのオプションがありますが、私が推奨するのは、通常の結果として 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 のスクレイピングが失敗する頻度に応じて、さまざまなアクションのしきい値を設定できます。

このアプローチには、データベースを活用して同時書き込みを管理し、プロセス間で結果を共有するという追加の利点もあります。これにより、複数のシステムまたはプロセス間で (おそらくメッセージ キューまたはチャンク データ ファイルを使用して) 作業を分割できます。

最終的な考え: スケールアップとスケールアウト

最初のスクレイプ中の再試行またはエラー処理に費やす時間を短縮すると、プロセスが大幅に高速化されます。ただし、一部のタスクは、単一マシンまたは単一プロセスのアプローチには大きすぎます。プロセスの高速化がまだニーズに対して不十分である場合は、次の 1 つまたは複数を使用して直線的ではないアプローチを検討することをお勧めします。

  • バックグラウンド プロセスのフォーク。
  • dRubyを使用して、複数のプロセスまたはマシン間で作業を分割します。
  • GNU parallelを使用して複数の外部プロセスを生成することにより、コアの使用を最大化します。
  • モノリシックでシーケンシャルなプロセスではない何か。

一般的なケースでは、アプリケーション ロジックを最適化するだけで十分ですが、そうでない場合は、より多くのプロセスにスケールアップするか、より多くのサーバーにスケールアウトします。スケールアウトは確かに手間がかかりますが、利用可能な処理オプションも拡張されます。

于 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で何をしたいかによって異なります。

あなたがそれらを飲み込みたいだけだと仮定しましょう。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

このパターンに従うと、黙って失敗します。何も起こらず、10回試行すると先に進みます。私は一般的に悪い計画(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

それはおそらくあなたが現在持っているものでそれを行うための最も簡単で最も優雅な方法です。とは言うものの、1つの大規模なレーキタスクで多くのリクエストを処理している場合、それはあまりエレガントではありません。何か問題が発生したり、システム上の単一のプロセスが長時間拘束されたりした場合は、再起動できません。メモリリーク(または無限ループ)が発生した場合は、自分がいる場所にいることに気づきます。 「先に進む」とだけ言うことはできません。おそらく、ResqueやSidekiq、Delayed Jobなどのキューイングシステムを使用する必要があります(ただし、Delayed Jobがうまく処理できるよりも多くのアイテムがキューイングされるように思われます)。より雄弁なアプローチを探しているなら、それらを掘り下げることをお勧めします。

于 2012-07-11T16:18:27.243 に答える
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 に対処するために行ったことの要点であり、非常に簡単に適用できます。

基本的にやりたいことは、次のコードをフィルターとして使用し、ログファイルを作成してエラーを保存することです。したがって、Web サイトを取得して処理する前に、まず次のことを行います。

したがって、ファイルにログファイルを作成/インスタンス化します。

@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 に答える
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

もう少し必要な場合は、エラーの種類ごとに異なる 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 のレールガイドへのリンクは次のとおりです: 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 に答える
2

スクレイピングから新しい SomeClass を作成するときに、常にオブジェクトの新しいインスタンスを返す initialize を使用する代わりに、クラス メソッドを使用してインスタンスを作成します。ここでは、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

あなたのクラス:

# 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
于 2012-07-12T12:43:36.410 に答える