捕獲異常URL--scrapy 原始碼分析之retry中介軟體

hantmac發表於2019-03-29

這次讓我們分析scrapy重試機制的原始碼,學習其中的思想,編寫定製化middleware,捕捉爬取失敗的URL等資訊。

scrapy簡介

Scrapy是一個為了爬取網站資料,提取結構性資料而編寫的應用框架。 可以應用在包括資料探勘,資訊處理或儲存歷史資料等一系列的程式中。

其最初是為了 頁面抓取 (更確切來說, 網路抓取 )所設計的, 也可以應用在獲取API所返回的資料(例如 Amazon Associates Web Services ) 或者通用的網路爬蟲。

一張圖可看清楚scrapy中資料的流向:

捕獲異常URL--scrapy 原始碼分析之retry中介軟體

簡單瞭解一下各個部分的功能,可以看下面簡化版資料流:

捕獲異常URL--scrapy 原始碼分析之retry中介軟體

總有漏網之魚

不管你的主機配置多麼吊炸天,還是網速多麼給力,在scrapy的大規模任務中,最終爬取的item數量都不會等於期望爬取的數量,也就是說總有那麼一些爬取失敗的漏網之魚,通過分析scrapy的日誌,可以知道造成失敗的原因有以下兩種情況:

  1. exception_count
  2. httperror

捕獲異常URL--scrapy 原始碼分析之retry中介軟體

以上的不管是exception還是httperror, scrapy中都有對應的retry機制,在settings.py檔案中我們可以設定有關重試的引數,等執行遇到異常和錯誤時候,scrapy就會自動處理這些問題,其中最關鍵的部分就是重試中介軟體,下面讓我們看一下scrapy的retry middleware。

RetryMiddle原始碼分析

在scrapy專案的middlewares.py檔案中 敲如下程式碼:

from scrapy.downloadermiddlewares.retry import RetryMiddleware
複製程式碼

按住ctrl鍵(Mac是command鍵),滑鼠左鍵點選RetryMiddleware進入該中介軟體所在的專案檔案的位置,也可以通過檢視檔案的形式找到該該中介軟體的位置,路徑是:

site-packages/scrapy/downloadermiddlewares/retry.RetryMiddleware
複製程式碼

原始碼如下:

class RetryMiddleware(object):

    # IOError is raised by the HttpCompression middleware when trying to
    # decompress an empty response
    # 需要重試的異常狀態,可以看出,其中有些是上面log中的異常
    EXCEPTIONS_TO_RETRY = (defer.TimeoutError, TimeoutError, DNSLookupError,
                           ConnectionRefusedError, ConnectionDone, ConnectError,
                           ConnectionLost, TCPTimedOutError, ResponseFailed,
                           IOError, TunnelError)

    def __init__(self, settings):
      # 讀取 settings.py 中關於重試的配置資訊,如果沒有配置重試的話,直接跳過
        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)
# 如果response的狀態碼,是我們要重試的
    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是否在retry_http_codes集合中,這個集合是讀取的配置檔案:

RETRY_ENABLED = True                  # 預設開啟失敗重試,一般關閉
RETRY_TIMES = 3                         # 失敗後重試次數,預設兩次
RETRY_HTTP_CODES = [500, 502, 503, 504, 522, 524, 408]    # 碰到這些驗證碼,才開啟重試
複製程式碼

對於httperror的處理也是同樣的道理,定義了一個 EXCEPTIONS_TO_RETRY的列表,裡面存放所有的異常型別,然後判斷傳入的異常是否存在於該集合中,如果在就進入retry邏輯,不在就忽略。

原始碼思想的應用

瞭解scrapy如何處理異常後,就可以利用這種思想,寫一個middleware,對爬取失敗的漏網之魚進行捕獲,方便以後做補爬。

  1. 在middlewares.py中 from scrapy.downloadermiddlewares.retry import RetryMiddleware, 寫一個class,繼承自RetryMiddleware;
  2. 對父類的process_response()process_exception()方法進行重寫;
  3. 將該middleware加入setting.py;
  4. 注意事項:該中介軟體的Order_code不能過大,如果過大就會越接近下載器,就會優先於RetryMiddleware處理response,但這個中介軟體是用來處理最終的錯誤的,即當一個response 500進入中介軟體鏈路時,需要先經過retry中介軟體處理,不能先由我們寫的中介軟體來處理,它不具有retry的功能,接收到500的response就直接放棄掉該request直接return了,這是不合理的。只有經過retry後仍然有異常的request才應當由我們寫的中介軟體來處理,這時候你想怎麼處理都可以,比如再次retry、return一個重新構造的response,但是如果你為了加快爬蟲速度,不設定retry也是可以的。

Talk is cheap, show the code:

class GetFailedUrl(RetryMiddleware):
    def __init__(self, settings):
        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')

    def process_response(self, request, response, spider):
        if response.status in self.retry_http_codes:
        # 將爬取失敗的URL存下來,你也可以存到別的儲存
            with open(str(spider.name) + ".txt", "a") as f:
                f.write(response.url + "\n")
            return response
        return response

    def process_exception(self, request, exception, spider):
    # 出現異常的處理
        if isinstance(exception, self.EXCEPTIONS_TO_RETRY):
            with open(str(spider.name) + ".txt", "a") as f:
                f.write(str(request) + "\n")
            return None
複製程式碼

setting.py中新增該中介軟體:

DOWNLOADER_MIDDLEWARES = {
   'myspider.middlewares.TabelogDownloaderMiddleware': 543,
    'myspider.middlewares.RandomProxy': 200,
    'myspider.middlewares.GetFailedUrl': 220,
}
複製程式碼

為了測試,我們故意寫錯URL,或者將download_delay縮短,就會出現各種異常,但是我們現在能夠捕獲它們了:

捕獲異常URL--scrapy 原始碼分析之retry中介軟體

相關文章