這次讓我們分析scrapy重試機制的原始碼,學習其中的思想,編寫定製化middleware,捕捉爬取失敗的URL等資訊。
scrapy簡介
Scrapy是一個為了爬取網站資料,提取結構性資料而編寫的應用框架。 可以應用在包括資料探勘,資訊處理或儲存歷史資料等一系列的程式中。
其最初是為了 頁面抓取 (更確切來說, 網路抓取 )所設計的, 也可以應用在獲取API所返回的資料(例如 Amazon Associates Web Services ) 或者通用的網路爬蟲。
一張圖可看清楚scrapy中資料的流向:
簡單瞭解一下各個部分的功能,可以看下面簡化版資料流:
總有漏網之魚
不管你的主機配置多麼吊炸天,還是網速多麼給力,在scrapy的大規模任務中,最終爬取的item數量都不會等於期望爬取的數量,也就是說總有那麼一些爬取失敗的漏網之魚,通過分析scrapy的日誌,可以知道造成失敗的原因有以下兩種情況:
- exception_count
- httperror
以上的不管是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,對爬取失敗的漏網之魚進行捕獲,方便以後做補爬。
- 在middlewares.py中 from scrapy.downloadermiddlewares.retry import RetryMiddleware, 寫一個class,繼承自RetryMiddleware;
- 對父類的
process_response()
和process_exception()
方法進行重寫; - 將該middleware加入setting.py;
- 注意事項:該中介軟體的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縮短,就會出現各種異常,但是我們現在能夠捕獲它們了: