如何在scrapy中捕獲并處理各種異常
前言
使用scrapy進(jìn)行大型爬取任務(wù)的時(shí)候(爬取耗時(shí)以天為單位),無(wú)論主機(jī)網(wǎng)速多好,爬完之后總會(huì)發(fā)現(xiàn)scrapy日志中“item_scraped_count”不等于預(yù)先的種子數(shù)量,總有一部分種子爬取失敗,失敗的類型可能有如下圖兩種(下圖為scrapy爬取結(jié)束完成時(shí)的日志):
scrapy中常見的異常包括但不限于:download error(藍(lán)色區(qū)域), http code 403/500(橙色區(qū)域)。
不管是哪種異常,我們都可以參考scrapy自帶的retry中間件寫法來(lái)編寫自己的中間件。
正文
使用IDE,現(xiàn)在scrapy項(xiàng)目中任意一個(gè)文件敲上以下代碼:
from scrapy.downloadermiddlewares.retry import RetryMiddleware
按住ctrl鍵,鼠標(biāo)左鍵點(diǎn)擊RetryMiddleware進(jìn)入該中間件所在的項(xiàng)目文件的位置,也可以通過(guò)查看文件的形式找到該中間件的位置,路徑是:site-packages/scrapy/downloadermiddlewares/retry.RetryMiddleware
該中間件的源代碼如下:
class RetryMiddleware(object): # IOError is raised by the HttpCompression middleware when trying to # decompress an empty response EXCEPTIONS_TO_RETRY = (defer.TimeoutError, TimeoutError, DNSLookupError, ConnectionRefusedError, ConnectionDone, ConnectError, ConnectionLost, TCPTimedOutError, ResponseFailed, IOError, TunnelError) def __init__(self, settings): if not settings.getbool('RETRY_ENABLED'): raise NotConfigured self.max_retry_times = settings.getint('RETRY_TIMES') self.retry_http_codes = set(int(x) for x in settings.getlist('RETRY_HTTP_CODES')) self.priority_adjust = settings.getint('RETRY_PRIORITY_ADJUST') @classmethod def from_crawler(cls, crawler): return cls(crawler.settings) def process_response(self, request, response, spider): if request.meta.get('dont_retry', False): return response if response.status in self.retry_http_codes: reason = response_status_message(response.status) return self._retry(request, reason, spider) or response return response def process_exception(self, request, exception, spider): if isinstance(exception, self.EXCEPTIONS_TO_RETRY) \ and not request.meta.get('dont_retry', False): return self._retry(request, exception, spider) def _retry(self, request, reason, spider): retries = request.meta.get('retry_times', 0) + 1 retry_times = self.max_retry_times if 'max_retry_times' in request.meta: retry_times = request.meta['max_retry_times'] stats = spider.crawler.stats if retries <= retry_times: logger.debug("Retrying %(request)s (failed %(retries)d times): %(reason)s", {'request': request, 'retries': retries, 'reason': reason}, extra={'spider': spider}) retryreq = request.copy() retryreq.meta['retry_times'] = retries retryreq.dont_filter = True retryreq.priority = request.priority + self.priority_adjust if isinstance(reason, Exception): reason = global_object_name(reason.__class__) stats.inc_value('retry/count') stats.inc_value('retry/reason_count/%s' % reason) return retryreq else: stats.inc_value('retry/max_reached') logger.debug("Gave up retrying %(request)s (failed %(retries)d times): %(reason)s", {'request': request, 'retries': retries, 'reason': reason}, extra={'spider': spider})
查看源碼我們可以發(fā)現(xiàn),對(duì)于返回http code的response,該中間件會(huì)通過(guò)process_response方法來(lái)處理,處理辦法比較簡(jiǎn)單,大概是判斷response.status是否在定義好的self.retry_http_codes集合中,通過(guò)向前查找,這個(gè)集合是一個(gè)列表,定義在default_settings.py文件中,定義如下:
RETRY_HTTP_CODES = [500, 502, 503, 504, 522, 524, 408]
也就是先判斷http code是否在這個(gè)集合中,如果在,就進(jìn)入retry的邏輯,不在集合中就直接return response。這樣就已經(jīng)實(shí)現(xiàn)對(duì)返回http code但異常的response的處理了。
但是對(duì)另一種異常的處理方式就不一樣了,剛才的異常準(zhǔn)確的說(shuō)是屬于HTTP請(qǐng)求error(超時(shí)),而另一種異常發(fā)生的時(shí)候則是如下圖這種實(shí)實(shí)在在的代碼異常(不處理的話):
你可以創(chuàng)建一個(gè)scrapy項(xiàng)目,start_url中填入一個(gè)無(wú)效的url即可模擬出此類異常。比較方便的是,在RetryMiddleware中同樣提供了對(duì)這類異常的處理辦法:process_exception
通過(guò)查看源碼,可以分析出大概的處理邏輯:同樣先定義一個(gè)集合存放所有的異常類型,然后判斷傳入的異常是否存在于該集合中,如果在(不分析dont try)就進(jìn)入retry邏輯,不在就忽略。
OK,現(xiàn)在已經(jīng)了解了scrapy是如何捕捉異常了,大概的思路也應(yīng)該有了,下面貼出一個(gè)實(shí)用的異常處理的中間件模板:
from twisted.internet import defer from twisted.internet.error import TimeoutError, DNSLookupError, \ ConnectionRefusedError, ConnectionDone, ConnectError, \ ConnectionLost, TCPTimedOutError from scrapy.http import HtmlResponse from twisted.web.client import ResponseFailed from scrapy.core.downloader.handlers.http11 import TunnelError class ProcessAllExceptionMiddleware(object): ALL_EXCEPTIONS = (defer.TimeoutError, TimeoutError, DNSLookupError, ConnectionRefusedError, ConnectionDone, ConnectError, ConnectionLost, TCPTimedOutError, ResponseFailed, IOError, TunnelError) def process_response(self,request,response,spider): #捕獲狀態(tài)碼為40x/50x的response if str(response.status).startswith('4') or str(response.status).startswith('5'): #隨意封裝,直接返回response,spider代碼中根據(jù)url==''來(lái)處理response response = HtmlResponse(url='') return response #其他狀態(tài)碼不處理 return response def process_exception(self,request,exception,spider): #捕獲幾乎所有的異常 if isinstance(exception, self.ALL_EXCEPTIONS): #在日志中打印異常類型 print('Got exception: %s' % (exception)) #隨意封裝一個(gè)response,返回給spider response = HtmlResponse(url='exception') return response #打印出未捕獲到的異常 print('not contained exception: %s'%exception)
spider解析代碼示例:
class TESTSpider(scrapy.Spider): name = 'TEST' allowed_domains = ['TTTTT.com'] start_urls = ['http://www.TTTTT.com/hypernym/?q='] custom_settings = { 'DOWNLOADER_MIDDLEWARES': { 'scrapy.downloadermiddlewares.useragent.UserAgentMiddleware': None, 'TESTSpider.middlewares.ProcessAllExceptionMiddleware': 120, }, 'DOWNLOAD_DELAY': 1, # 延時(shí)最低為2s 'AUTOTHROTTLE_ENABLED': True, # 啟動(dòng)[自動(dòng)限速] 'AUTOTHROTTLE_DEBUG': True, # 開啟[自動(dòng)限速]的debug 'AUTOTHROTTLE_MAX_DELAY': 10, # 設(shè)置最大下載延時(shí) 'DOWNLOAD_TIMEOUT': 15, 'CONCURRENT_REQUESTS_PER_DOMAIN': 4 # 限制對(duì)該網(wǎng)站的并發(fā)請(qǐng)求數(shù) } def parse(self, response): if not response.url: #接收到url==''時(shí) print('500') yield TESTItem(key=response.meta['key'], _str=500, alias='') elif 'exception' in response.url: print('exception') yield TESTItem(key=response.meta['key'], _str='EXCEPTION', alias='')
Note:該中間件的Order_code不能過(guò)大,如果過(guò)大就會(huì)越接近下載器,就會(huì)優(yōu)先于RetryMiddleware處理response,但這個(gè)中間件是用來(lái)兜底的,即當(dāng)一個(gè)response 500進(jìn)入中間件鏈時(shí),需要先經(jīng)過(guò)retry中間件處理,不能先由我們寫的中間件來(lái)處理,它不具有retry的功能,接收到500的response就直接放棄掉該request直接return了,這是不合理的。只有經(jīng)過(guò)retry后仍然有異常的request才應(yīng)當(dāng)由我們寫的中間件來(lái)處理,這時(shí)候你想怎么處理都可以,比如再次retry、return一個(gè)重新構(gòu)造的response。
下面來(lái)驗(yàn)證一下效果如何(測(cè)試一個(gè)無(wú)效的URL),下圖為未啟用中間件的情況:
再啟用中間件查看效果:
ok,達(dá)到預(yù)期效果:即使程序運(yùn)行時(shí)拋出異常也能被捕獲并處理。
到此這篇關(guān)于如何在scrapy中捕獲并處理各種異常的文章就介紹到這了,更多相關(guān)scrapy 捕獲處理異常內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Python+pandas計(jì)算數(shù)據(jù)相關(guān)系數(shù)的實(shí)例
今天小編就為大家分享一篇Python+pandas計(jì)算數(shù)據(jù)相關(guān)系數(shù)的實(shí)例,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2018-07-07Python中用于轉(zhuǎn)換字母為小寫的lower()方法使用簡(jiǎn)介
這篇文章主要介紹了Python中用于轉(zhuǎn)換字母為小寫的lower()方法使用,是Python學(xué)習(xí)中的基礎(chǔ)知識(shí),需要的朋友可以參考下2015-05-05關(guān)于tf.TFRecordReader()函數(shù)的用法解析
今天小編就為大家分享一篇關(guān)于tf.TFRecordReader()函數(shù)的用法解析,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2020-02-02Python datetime時(shí)間格式化去掉前導(dǎo)0
Python datetime時(shí)間格式化去掉前導(dǎo)0,在format string的%與flag之間,添加一個(gè)“-”即可2014-07-07Python3爬蟲使用Fidder實(shí)現(xiàn)APP爬取示例
這篇文章主要介紹了Python3爬蟲使用Fidder實(shí)現(xiàn)APP爬取示例,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2018-11-11python如何實(shí)現(xiàn)讀取并顯示圖片(不需要圖形界面)
這篇文章主要介紹了python如何實(shí)現(xiàn)讀取并顯示圖片,文中示例代碼非常詳細(xì),幫助大家更好的理解和學(xué)習(xí),感興趣的朋友可以了解下2020-07-07