はじめ
に大規模なクロールタスクにスクレイピーを使用すると(クロール時間は日単位)、ホストのネットワーク速度がどれほど優れていても、クロール後、スクレイピーログの "item_scraped_count"が事前にシードされたシードの数と等しくなく、常に一部があることがわかります。シードのクロールが失敗します。次の図に示すように、2種類の失敗があります(次の図は、断片的なクロールが完了したときのログを示しています)。
スクレイピーの一般的な例外には、ダウンロードエラー(青色の領域)、httpコード403/500(オレンジの領域)などがあります。
どのような例外があっても、独自のミドルウェアを作成するために、scrapy独自のリトライミドルウェアの書き込み方法を参照できます。
テキスト
はIDEを使用しており、スクレイピープロジェクト内のすべてのファイルは次のコードで入力されています。
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コードを返す応答の場合、ミドルウェアはprocess_responseメソッドによって処理されることがわかります。処理方法は比較的単純です。おそらく、response.statusが定義済みのself.retry_http_codesコレクションにあるかどうかを判断することです。このコレクションは、default_settings.pyファイルで定義されたリストであり、次のように定義されています。
RETRY_HTTP_CODES = [500、502、503、504、522、524、408]
は、最初にhttpコードがこのセットにあるかどうかを判断し、ある場合は再試行のロジックを入力し、セットにない場合は直接応答を返します。このように、HTTPコードに対する応答ですが、異常な応答が実装されています。
ただし、別の例外の処理は異なります。前述の例外は正確にHTTPリクエストエラー(タイムアウト)であり、別の例外が発生すると、以下に示すように実際のコード例外になります(処理されない場合):
このような例外をシミュレートするには、スクレイピープロジェクトを作成し、start_urlに無効なURLを入力します。RetryMiddlewareもこのタイプの例外の処理メソッドを提供する方が便利です:process_exception
ソースコードを確認することで、おおよその処理ロジックを分析できます。まず、すべての種類の例外を格納するコレクションを定義し、次に着信例外がコレクション内に存在するかどうかを判断します。そうする場合(分析しないでください)、再試行ロジックを入力します。無視してください。
これで、scrapyが例外をキャッチする方法を理解できたので、一般的な考え方もそこにあるはずです。例外処理の実用的なミドルウェアテンプレートを以下に示します。
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):
#捕获状态码为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)
スパイダー解析コード例:
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='')
注:このミドルウェアのOrder_codeは大きすぎてはいけません。大きすぎると、ダウンローダーに近くなります(注文はデフォルトのミドルウェアによって実行されます。ここをクリックして表示します)。RetryMiddlewareよりも優先して応答を処理しますが、このミドルウェアは下部に使用されます。つまり、応答500がミドルウェアチェーンに入ると、最初に再試行ミドルウェアで処理する必要があり、最初に作成したミドルウェアでは処理できません。これには再試行の機能がありません。応答500を受信すると、要求は直接破棄され、要求が直接返されます。これは無理です。再試行を行った後も、異常な要求があった場合は、作成したミドルウェアで処理する必要がありますが、現時点では、再試行や再構築された応答を返すなど、必要な処理を行うことができます。
動作を確認してみましょう(無効なURLのテスト)次の図は、有効になっていないミドルウェアを示しています。
次に、ミドルウェアを有効にして効果を確認します。