¿Cómo obtener las URLs de fallo de scrapy?

Soy un novato de scrapy y es increíble el marco de rastreo que he conocido.

En mi proyecto, envié más de 90,000 solicitudes, pero fallaron algunas de ellas. Establecí el nivel de registro como INFO, y solo puedo ver algunas estadísticas pero no detalles.

2012-12-05 21:03:04+0800 [pd_spider] INFO: Dumping spider stats: {'downloader/exception_count': 1, 'downloader/exception_type_count/twisted.internet.error.ConnectionDone': 1, 'downloader/request_bytes': 46282582, 'downloader/request_count': 92383, 'downloader/request_method_count/GET': 92383, 'downloader/response_bytes': 123766459, 'downloader/response_count': 92382, 'downloader/response_status_count/200': 92382, 'finish_reason': 'finished', 'finish_time': datetime.datetime(2012, 12, 5, 13, 3, 4, 836000), 'item_scraped_count': 46191, 'request_depth_max': 1, 'scheduler/memory_enqueued': 92383, 'start_time': datetime.datetime(2012, 12, 5, 12, 23, 25, 427000)} 

¿Hay alguna manera de obtener un informe más detallado? Por ejemplo, mostrar esas URL fallidas. ¡Gracias!

Sí, esto es posible.

Agregué una lista de fail_urls a mi clase de araña y le agregué urls si el estado de la respuesta era 404 (esto deberá extenderse para cubrir otros estados de error).

Luego agregué un controlador que une la lista en una sola cadena y lo agrego a las estadísticas cuando se cierra la araña.

Basado en sus comentarios, es posible rastrear errores Twisted.

 from scrapy.spider import BaseSpider from scrapy.xlib.pydispatch import dispatcher from scrapy import signals class MySpider(BaseSpider): handle_httpstatus_list = [404] name = "myspider" allowed_domains = ["example.com"] start_urls = [ 'http://www.example.com/thisurlexists.html', 'http://www.example.com/thisurldoesnotexist.html', 'http://www.example.com/neitherdoesthisone.html' ] def __init__(self, category=None): self.failed_urls = [] def parse(self, response): if response.status == 404: self.crawler.stats.inc_value('failed_url_count') self.failed_urls.append(response.url) def handle_spider_closed(spider, reason): self.crawler.stats.set_value('failed_urls', ','.join(spider.failed_urls)) def process_exception(self, response, exception, spider): ex_class = "%s.%s" % (exception.__class__.__module__, exception.__class__.__name__) self.crawler.stats.inc_value('downloader/exception_count', spider=spider) self.crawler.stats.inc_value('downloader/exception_type_count/%s' % ex_class, spider=spider) dispatcher.connect(handle_spider_closed, signals.spider_closed) 

Salida (las estadísticas de downloader / exception_count * solo aparecerán si realmente se lanzan excepciones; las simulé al intentar ejecutar la araña después de haber apagado mi adaptador inalámbrico):

 2012-12-10 11:15:26+0000 [myspider] INFO: Dumping Scrapy stats: {'downloader/exception_count': 15, 'downloader/exception_type_count/twisted.internet.error.DNSLookupError': 15, 'downloader/request_bytes': 717, 'downloader/request_count': 3, 'downloader/request_method_count/GET': 3, 'downloader/response_bytes': 15209, 'downloader/response_count': 3, 'downloader/response_status_count/200': 1, 'downloader/response_status_count/404': 2, 'failed_url_count': 2, 'failed_urls': 'http://www.example.com/thisurldoesnotexist.html, http://www.example.com/neitherdoesthisone.html' 'finish_reason': 'finished', 'finish_time': datetime.datetime(2012, 12, 10, 11, 15, 26, 874000), 'log_count/DEBUG': 9, 'log_count/ERROR': 2, 'log_count/INFO': 4, 'response_received_count': 3, 'scheduler/dequeued': 3, 'scheduler/dequeued/memory': 3, 'scheduler/enqueued': 3, 'scheduler/enqueued/memory': 3, 'spider_exceptions/NameError': 2, 'start_time': datetime.datetime(2012, 12, 10, 11, 15, 26, 560000)} 

Aquí hay otro ejemplo de cómo manejar y recostackr errores 404 (consultar las páginas de ayuda de github):

 from scrapy.selector import HtmlXPathSelector from scrapy.contrib.spiders import CrawlSpider, Rule from scrapy.contrib.linkextractors.sgml import SgmlLinkExtractor from scrapy.item import Item, Field class GitHubLinkItem(Item): url = Field() referer = Field() status = Field() class GithubHelpSpider(CrawlSpider): name = "github_help" allowed_domains = ["help.github.com"] start_urls = ["https://help.github.com", ] handle_httpstatus_list = [404] rules = (Rule(SgmlLinkExtractor(), callback='parse_item', follow=True),) def parse_item(self, response): if response.status == 404: item = GitHubLinkItem() item['url'] = response.url item['referer'] = response.request.headers.get('Referer') item['status'] = response.status return item 

Simplemente ejecute scrapy runspider con -o output.json y vea la lista de elementos en el archivo output.json .

Las respuestas de @Talvalin y @alecxe me ayudaron mucho, pero no parecen capturar eventos del descargador que no generan un objeto de respuesta (por ejemplo, twisted.internet.error.TimeoutError and twisted.web.http.PotentialDataLoss ) . Estos errores aparecen en el volcado de estadísticas al final de la ejecución, pero sin ninguna información meta.

Como descubrí aquí , el middleware stats.py rastrea los errores , capturados en el método process_exception clase DownloaderStats , y específicamente en la variable ex_class , que incrementa cada tipo de error según sea necesario, y luego descarga los conteos al final de la carrera.

Para hacer coincidir estos errores con la información del objeto de solicitud correspondiente, puede agregar un ID único a cada solicitud (a través de request.meta ) y luego stats.py en el método process_exception de stats.py :

 self.stats.set_value('downloader/my_errs/{0}'.format(request.meta), ex_class) 

Eso generará una cadena única para cada error basado en el descargador no acompañado por una respuesta. Luego, puede guardar el stats.py modificado como otra cosa (por ejemplo, my_stats.py ), agregarlo a downloadermiddlewares (con la prioridad correcta) y deshabilitar el stock stats.py :

 DOWNLOADER_MIDDLEWARES = { 'myproject.my_stats.MyDownloaderStats': 850, 'scrapy.downloadermiddleware.stats.DownloaderStats': None, } 

La salida al final de la ejecución se ve así (aquí, usando metainformación donde cada url de solicitud se asigna a un group_id y member_id separados por una barra inclinada, como '0/14' ):

 {'downloader/exception_count': 3, 'downloader/exception_type_count/twisted.web.http.PotentialDataLoss': 3, 'downloader/my_errs/0/1': 'twisted.web.http.PotentialDataLoss', 'downloader/my_errs/0/38': 'twisted.web.http.PotentialDataLoss', 'downloader/my_errs/0/86': 'twisted.web.http.PotentialDataLoss', 'downloader/request_bytes': 47583, 'downloader/request_count': 133, 'downloader/request_method_count/GET': 133, 'downloader/response_bytes': 3416996, 'downloader/response_count': 130, 'downloader/response_status_count/200': 95, 'downloader/response_status_count/301': 24, 'downloader/response_status_count/302': 8, 'downloader/response_status_count/500': 3, 'finish_reason': 'finished'....} 

Esta respuesta trata con errores no basados ​​en descargas.

Scrapy ignora 404 por defecto y no lo analiza. Para manejar el error 404 haz esto. Esto es muy fácil, si obtiene un código de error 404 en respuesta, puede manejarlo de una manera muy fácil … en la configuración de escritura

 HTTPERROR_ALLOWED_CODES = [404,403] 

y luego manejar el código de estado de respuesta en su función de análisis.

  def parse(self,response): if response.status == 404: #your action on error 

en la configuración y obtener respuesta en la función de análisis

A partir de la versión 0.24.6, el método sugerido por alecxe no detectará errores con las URL de inicio. Para registrar errores con las URL de inicio, debe anular parse_start_urls . Al adaptar la respuesta de alexce para este propósito, obtendrías:

 from scrapy.selector import HtmlXPathSelector from scrapy.contrib.spiders import CrawlSpider, Rule from scrapy.contrib.linkextractors.sgml import SgmlLinkExtractor from scrapy.item import Item, Field class GitHubLinkItem(Item): url = Field() referer = Field() status = Field() class GithubHelpSpider(CrawlSpider): name = "github_help" allowed_domains = ["help.github.com"] start_urls = ["https://help.github.com", ] handle_httpstatus_list = [404] rules = (Rule(SgmlLinkExtractor(), callback='parse_item', follow=True),) def parse_start_url(self, response): return self.handle_response(response) def parse_item(self, response): return self.handle_response(response) def handle_response(self, response): if response.status == 404: item = GitHubLinkItem() item['url'] = response.url item['referer'] = response.request.headers.get('Referer') item['status'] = response.status return item 

Esta es una actualización sobre esta pregunta. Me encontré con un problema similar y necesité usar las señales desagradables para llamar a una función en mi canalización. He editado el código de @Talvalin, pero quería hacer una respuesta solo para mayor claridad.

Básicamente, debe agregarse en sí mismo como un argumento para handle_spider_closed. También debe llamar al despachador en init para que pueda pasar la instancia de araña (self) al método de manejo.

 from scrapy.spider import Spider from scrapy.xlib.pydispatch import dispatcher from scrapy import signals class MySpider(Spider): handle_httpstatus_list = [404] name = "myspider" allowed_domains = ["example.com"] start_urls = [ 'http://www.example.com/thisurlexists.html', 'http://www.example.com/thisurldoesnotexist.html', 'http://www.example.com/neitherdoesthisone.html' ] def __init__(self, category=None): self.failed_urls = [] # the dispatcher is now called in init dispatcher.connect(self.handle_spider_closed,signals.spider_closed) def parse(self, response): if response.status == 404: self.crawler.stats.inc_value('failed_url_count') self.failed_urls.append(response.url) def handle_spider_closed(self, spider, reason): # added self self.crawler.stats.set_value('failed_urls',','.join(spider.failed_urls)) def process_exception(self, response, exception, spider): ex_class = "%s.%s" % (exception.__class__.__module__, exception.__class__.__name__) self.crawler.stats.inc_value('downloader/exception_count', spider=spider) self.crawler.stats.inc_value('downloader/exception_type_count/%s' % ex_class, spider=spider) 

Espero que esto ayude a cualquiera con el mismo problema en el futuro.

Además de algunas de estas respuestas, si desea realizar un seguimiento de los errores de Twisted, echaría un vistazo al uso del parámetro errback del objeto Request, en el que puede configurar una función de callback para que se llame con el error Twisted en una falla de solicitud. Además de la url, este método puede permitirle rastrear el tipo de falla.

A continuación, puede registrar las direcciones URL utilizando: failure.request.url (donde failure es el objeto Twisted Failure pasado en errback ).

 # these would be in a Spider def start_requests(self): for url in self.start_urls: yield scrapy.Request(url, callback=self.parse, errback=self.handle_error) def handle_error(self, failure): url = failure.request.url logging.error('Failure type: %s, URL: %s', failure.type, url) 

Los documentos de Scrapy dan un ejemplo completo de cómo se puede hacer esto, excepto que las llamadas al registrador de Scrapy ahora están depreciadas , así que adapté mi ejemplo para usar el registro incorporado de Python):

https://doc.scrapy.org/en/latest/topics/request-response.html#topics-request-response-ref-errbacks

Puedes capturar urls fallidas de dos maneras.

  1. Definir solicitud scrapy con errback

     class TestSpider(scrapy.Spider): def start_requests(self): yield scrapy.Request(url, callback=self.parse, errback=self.errback) def errback(self, failure): '''handle failed url (failure.request.url)''' pass 
  2. Usa el sistema signal.item_dropped

     class TestSpider(scrapy.Spider): def __init__(self): crawler.signals.connect(self.request_dropped, signal=signals.request_dropped) def request_dropped(self, request, spider): '''handle failed url (request.url)''' pass 

[! Aviso] La solicitud de Scrapy con errback no puede detectar un error de rebash automático, como error de conexión, RETRY_HTTP_CODES en la configuración.

Básicamente, Scrapy ignora el error 404 por defecto, se definió en httperror middleware.

Entonces, agregue HTTPERROR_ALLOW_ALL = verdadero a su archivo de configuración.

Después de esto, puede acceder a response.status a través de su función de análisis .

Puedes manejarlo así.

 def parse(self,response): if response.status==404: print(response.status) else: do something