Как мой скребок "стоп" обрабатывает ошибки 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
Ответы
Ответ 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}")