Как мой скребок "стоп" обрабатывает ошибки 404? - программирование
Подтвердить что ты не робот

Как мой скребок "стоп" обрабатывает ошибки 404?

У меня есть задача rake, которая отвечает за пакетную обработку на миллионах URL-адресов. Поскольку этот процесс занимает много времени, я иногда обнаруживаю, что URL-адреса, которые я пытаюсь обрабатывать, более недействительны - 404s, сайт вниз, что угодно.

Когда я изначально написал это, в основном был только один сайт, который постоянно обрабатывался во время обработки, поэтому мое решение заключалось в использовании open-uri, спасении созданных исключений, немного подождать, а затем повторить попытку.

Это работало отлично, когда набор данных был меньше, но теперь так много времени, что я нахожу URL-адреса, больше не существует и создаю 404.

Используя случай 404, когда это произойдет, мой script просто сидит там и петли бесконечно - очевидно, плохо.

Как я должен обрабатывать случаи, когда страница не загружается успешно, и что еще более важно, как это вписывается в "стек", который я создал?

Я новичок в этом, и Rails, поэтому любые мнения о том, где я, возможно, ошибся в этом дизайне, приветствуются!

Вот некоторый анонимный код, который показывает, что у меня есть:

Задача rake, которая вызывает вызов MyHelperModule:

# 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
4b9b3361

Ответ 1

TL; DR

Использовать обработку ошибок вне диапазона и другую концептуальную модель соскабливания для ускорения операций.

Исключения не для общих условий

Существует ряд других ответов, в которых рассматриваются способы обработки исключений для вашего прецедента. Я придерживаюсь другого подхода, говоря, что обработка исключений принципиально неправильным подходом здесь по ряду причин.

  • В своей книге "Исключительный рубин" Авди Гримм дает некоторые ориентиры, показывающие эффективность исключений на ~ 156% медленнее, чем использование альтернативных методов кодирования, таких как ранние результаты.

  • В прагматическом программисте: от Journeyman to Master, авторы заявляют, что "[E] xceptions должны быть зарезервированы для неожиданных событий". В вашем случае 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, чтобы найти все URL-адреса, которые недавно вернули статус 404, чтобы вы могли предпринять соответствующие действия. Возможно, вы хотите повторить страницу, зарегистрировать сообщение или просто удалить URL из списка URL-адресов, чтобы очистить - "подходящее действие" зависит от вас.

Отслеживая количество повторных попыток, вы можете даже различать временные ошибки и более постоянные ошибки. Это позволяет устанавливать пороговые значения для разных действий в зависимости от частоты сбоев скремблирования для заданного URL.

Этот подход также имеет дополнительное преимущество в использовании базы данных для управления одновременной записью и совместного использования результатов между процессами. Это позволит вам распределять работу (возможно, с очередью сообщений или файлами с чередующимися данными) между несколькими системами или процессами.

Заключительные мысли: масштабирование и выход

Расходы меньше времени на повторные попытки или обработку ошибок во время начальной очистки должны значительно ускорить процесс. Однако некоторые задачи слишком велики для одношагового или однопроцессорного подхода. Если ускорение процесса еще недостаточно для ваших нужд, вы можете рассмотреть менее линейный подход, используя одно или несколько из следующих действий:

  • Формирование фоновых процессов.
  • Использование dRuby для разделения работы между несколькими процессами или машинами.
  • Максимизация использования ядра путем нереста нескольких внешних процессов с использованием параллельной GNU.
  • Что-то еще, что не является монолитным, последовательным процессом.

Оптимизация логики приложения должна быть достаточной для общего случая, но если нет, масштабирование до большего количества процессов или выход на большее количество серверов. Масштабирование, безусловно, будет большим количеством работы, но также расширит доступные для вас варианты обработки.

Ответ 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

Ответ 4

У меня на самом деле есть задача грабли, которая делает что-то удивительно похожее. Вот суть того, что я сделал, чтобы справиться с 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

Ответ 5

Все зависит от того, что вы хотите делать с 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

Это, наверное, самый простой и изящный способ сделать это с тем, что у вас есть. Тем не менее, если вы справляетесь с тем, что многие запросы в одном массиве грабли, это не очень элегантно. Вы не можете перезапустить его, если что-то пойдет не так, оно связывает один процесс в вашей системе в течение длительного времени и т.д. Если вы закончите с утечками памяти (или бесконечными циклами!), Вы окажетесь в том месте, где вы не может просто сказать "двигаться дальше". Вероятно, вы должны использовать какую-то систему массового обслуживания, такую ​​как Resque или Sidekiq, или Delayed Job (хотя похоже, что у вас больше предметов, которые вы закончили бы в очереди, чем Delayed Job с удовольствием справится). Я бы рекомендовал копаться в них, если вы ищете более красноречивый подход.

Ответ 6

Вместо инициализации, которая всегда возвращает новый экземпляр объекта, при создании нового 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

Ответ 7

Относительно проблемы, которую вы испытываете, вы можете сделать следующее:


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

Ответ 8

Я думаю, что все комментарии по этому вопросу точны и правильны. На этой странице много полезной информации. Вот моя попытка собрать эту огромную щедрость. Это сказано +1 всем ответам.

Если вас интересует только 404, используя OpenURI, вы можете обрабатывать только те типы исключений

# 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}"

Есть много способов очереди. Я думаю, что одни из лучших - фэй и реск. Вот ссылка на оба: 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 => "[email protected]"
def self.mail(*args)
 ...
# then later 
rescue Exception => e
  case e.message
    when /404/ && attempts == 3
      SomeClassMailer.mail(:to => "[email protected]", :subject => "Failure ! #{attempts}")