如何在scrapy中捕获并处理各种异常


Posted in Python onSeptember 28, 2020

前言

    使用scrapy进行大型爬取任务的时候(爬取耗时以天为单位),无论主机网速多好,爬完之后总会发现scrapy日志中“item_scraped_count”不等于预先的种子数量,总有一部分种子爬取失败,失败的类型可能有如下图两种(下图为scrapy爬取结束完成时的日志):

如何在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中捕获并处理各种异常

你可以创建一个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 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)

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不能过大,如果过大就会越接近下载器,就会优先于RetryMiddleware处理response,但这个中间件是用来兜底的,即当一个response 500进入中间件链时,需要先经过retry中间件处理,不能先由我们写的中间件来处理,它不具有retry的功能,接收到500的response就直接放弃掉该request直接return了,这是不合理的。只有经过retry后仍然有异常的request才应当由我们写的中间件来处理,这时候你想怎么处理都可以,比如再次retry、return一个重新构造的response。

下面来验证一下效果如何(测试一个无效的URL),下图为未启用中间件的情况:

如何在scrapy中捕获并处理各种异常

再启用中间件查看效果:

如何在scrapy中捕获并处理各种异常

ok,达到预期效果:即使程序运行时抛出异常也能被捕获并处理。

到此这篇关于如何在scrapy中捕获并处理各种异常的文章就介绍到这了,更多相关scrapy 捕获处理异常内容请搜索三水点靠木以前的文章或继续浏览下面的相关文章希望大家以后多多支持三水点靠木!

Python 相关文章推荐
盘点提高 Python 代码效率的方法
Jul 03 Python
Python连接SQLServer2000的方法详解
Apr 19 Python
Python与人工神经网络:使用神经网络识别手写图像介绍
Dec 19 Python
在Python 2.7即将停止支持时,我们为你带来了一份python 3.x迁移指南
Jan 30 Python
PyQt5实现无边框窗口的标题拖动和窗口缩放
Apr 19 Python
Python 爬虫之Beautiful Soup模块使用指南
Jul 05 Python
selenium获取当前页面的url、源码、title的方法
Jun 12 Python
python连接、操作mongodb数据库的方法实例详解
Sep 11 Python
jupyter notebook 实现matplotlib图动态刷新
Apr 22 Python
Python Selenium库的基本使用教程
Jan 04 Python
python爬取微博评论的实例讲解
Jan 15 Python
Golang Web 框架Iris安装部署
Aug 14 Python
python向企业微信发送文字和图片消息的示例
Sep 28 #Python
python利用tkinter实现图片格式转换的示例
Sep 28 #Python
python在CMD界面读取excel所有数据的示例
Sep 28 #Python
python调用摄像头的示例代码
Sep 28 #Python
python 调用API接口 获取和解析 Json数据
Sep 28 #Python
记录一下scrapy中settings的一些配置小结
Sep 28 #Python
使用scrapy ImagesPipeline爬取图片资源的示例代码
Sep 28 #Python
You might like
PHP实现懒加载的方法
2015/03/07 PHP
PHP运行模式汇总
2016/11/06 PHP
PHP通过curl获取接口URL的数据方法
2018/05/31 PHP
PHP实现的服务器一致性hash分布算法示例
2018/08/09 PHP
php 使用ActiveMQ发送消息,与处理消息操作示例
2020/02/23 PHP
JS/jQuery实现默认显示部分文字点击按钮显示全部内容
2013/05/13 Javascript
关于extjs4如何获取grid修改后的数据的问题
2013/08/07 Javascript
查找iframe里元素的方法可传参
2013/09/11 Javascript
jquery parent和parents的区别分析
2013/10/02 Javascript
jQuery focus和blur事件的应用详解
2014/01/26 Javascript
使用jQuery在对象中缓存选择器的简单方法
2015/06/30 Javascript
AngularJS 工作原理详解
2016/08/18 Javascript
AngularJS 模块化详解及实例代码
2016/09/14 Javascript
Javascript 实现放大镜效果实例详解
2016/12/03 Javascript
微信小程序-横向滑动scroll-view隐藏滚动条
2017/04/20 Javascript
Javascript实现时间倒计时效果
2017/07/15 Javascript
JS实现对json对象排序并删除id相同项功能示例
2018/04/18 Javascript
Angular中sweetalert弹框的基本使用教程
2018/07/22 Javascript
ES6 对象的新功能与解构赋值介绍
2019/02/05 Javascript
Vant Weapp组件踩坑:picker的初始赋值解决
2020/11/12 Javascript
[01:12]快闪回顾DOTA2亚洲邀请赛(DAC) 静候2018新征程开启
2018/03/11 DOTA
忘记ftp密码使用python ftplib库暴力破解密码的方法示例
2014/01/22 Python
python根据经纬度计算距离示例
2014/02/16 Python
Python使用urllib模块的urlopen超时问题解决方法
2014/11/08 Python
解析Python编程中的包结构
2015/10/25 Python
Python探索之ModelForm代码详解
2017/10/26 Python
Python数据可视化实现多种图例代码详解
2020/07/14 Python
澳大利亚工具仓库:Tools Warehouse
2018/10/15 全球购物
UNIX特点都有哪些
2016/04/05 面试题
大学生自我鉴定范文模板
2014/01/21 职场文书
幼师求职自荐信范文
2014/01/26 职场文书
大学生违纪检讨书范文
2015/05/07 职场文书
社区宣传标语口号
2015/12/26 职场文书
canvas实现贪食蛇的实践
2022/02/15 Javascript
CentOS7安装MySQL8的超级详细教程(无坑!)
2022/06/10 Servers
JS轻量级函数式编程实现XDM二
2022/06/16 Javascript