1. 程式人生 > >[Scrapy使用技巧] 如何在scrapy中捕獲並處理各種異常

[Scrapy使用技巧] 如何在scrapy中捕獲並處理各種異常

前言

    使用scrapy進行大型爬取任務的時候(爬取耗時以天為單位),無論主機網速多好,爬完之後總會發現scrapy日誌中“item_scraped_count”不等於預先的種子數量,總有一部分種子爬取失敗,失敗的型別可能有如下圖兩種(下圖為scrapy爬取結束完成時的日誌):


scrapy中常見的異常包括但不限於:download error(藍色區域), http code 403/500(橙色區域)。

不管是哪種異常,我們都可以參考scrapy自帶的retry中介軟體寫法來編寫自己的中介軟體。

正文

     使用IDE,現在scrapy專案中任意一個檔案敲上以下程式碼:

from scrapy.downloadermiddlewares.retry import RetryMiddleware

按住ctrl鍵,滑鼠左鍵點選RetryMiddleware進入該中介軟體所在的專案檔案的位置,也可以通過檢視檔案的形式找到該該中介軟體的位置,路徑是: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})

檢視原始碼我們可以發現,對於返回http code的response,該中介軟體會通過process_response方法來處理,處理辦法比較簡單,大概是判斷response.status是否在定義好的self.retry_http_codes集合中,通過向前查詢,這個集合是一個列表,定義在default_settings.py檔案中,定義如下:

RETRY_HTTP_CODES = [500, 502, 503, 504, 522, 524, 408]

也就是先判斷http code是否在這個集合中,如果在,就進入retry的邏輯,不在集合中就直接return response。這樣就已經實現對返回http code但異常的response的處理了。

但是對另一種異常的處理方式就不一樣了,剛才的異常準確的說是屬於http error,而另一種異常發生的時候則是如下圖這種實實在在的程式碼異常(不處理的話):


你可以建立一個scrapy專案,start_url中填入一個無效的url即可模擬出此類異常。比較方便的是,在RetryMiddleware中同樣提供了對這類異常的處理辦法:process_exception

通過檢視原始碼,可以分析出大概的處理邏輯:同樣先定義一個集合存放所有的異常型別,然後判斷傳入的異常是否存在於該集合中,如果在(不分析dont try)就進入retry邏輯,不在就忽略。

OK,現在已經瞭解了scrapy是如何捕捉異常了,大概的思路也應該有了,下面貼出一個實用的異常處理的中介軟體模板:

from twisted.internet import defer
from twisted.internet.error import TimeoutError, DNSLookupError, \
    ConnectionRefusedError, ConnectionDone, ConnectError, \
    ConnectionLost, TCPTimedOutError
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):
        #捕獲狀態碼為40x/50x的response
        if str(response.status).startswith('4') or str(response.status).startswith('5'):
            #隨意封裝,直接返回response,spider程式碼中根據url==''來處理response
            response = HtmlResponse(url='')
            return response
        #其他狀態碼不處理
        return response
    def process_exception(self,request,exception,spider):
        #捕獲幾乎所有的異常
        if isinstance(exception, self.ALL_EXCEPTIONS):
            #在日誌中列印異常型別
            print('Got exception: %s' % (exception))
            #隨意封裝一個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,  # 延時最低為2s
        'AUTOTHROTTLE_ENABLED': True,  # 啟動[自動限速]
        'AUTOTHROTTLE_DEBUG': True,  # 開啟[自動限速]的debug
        'AUTOTHROTTLE_MAX_DELAY': 10,  # 設定最大下載延時
        'DOWNLOAD_TIMEOUT': 15,
        'CONCURRENT_REQUESTS_PER_DOMAIN': 4  # 限制對該網站的併發請求數
    }
    def parse(self, response):
        if not response.url: #接收到url==''時
            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不能過大,如果過大就會越接近下載器(預設中介軟體執行的Order點選這裡檢視),就會優先於RetryMiddleware處理response,但這個中介軟體是用來兜底的,即當一個response 500進入中介軟體鏈時,需要先經過retry中介軟體處理,不能先由我們寫的中介軟體來處理,它不具有retry的功能,接收到500的response就直接放棄掉該request直接return了,這是不合理的。只有經過retry後仍然有異常的request才應當由我們寫的中介軟體來處理,這時候你想怎麼處理都可以,比如再次retry、return一個重新構造的response。。。怎麼玩都行。

下面來驗證一下效果如何(測試一個無效的URL),下圖為未啟用中介軟體的情況:

再啟用中介軟體檢視效果:

ok,達到預期效果:即使程式執行時丟擲異常也能被捕獲並處理。


如果你有什麼意見或建議,請給我留言,生活愉快~