彻底搞懂Scrapy的中间件(一):https://www.cnblogs.com/xieqiankun/p/know_middleware_of_scrapy_1.html
彻底搞懂Scrapy的中间件(二):https://www.cnblogs.com/xieqiankun/p/know_middleware_of_scrapy_2.html
彻底搞懂Scrapy的中间件(三):https://www.cnblogs.com/xieqiankun/p/know_middleware_of_scrapy_3.html
在 Scrapy 中捕获并处理各种异常
[ Scrapy使用技巧 ] 如何在 Scrapy 中捕获并处理各种异常:https://blog.csdn.net/sc_lilei/article/details/80702449
重写 scrapy 中间件之 RetryMiddleware:https://blog.csdn.net/qq_33854211/article/details/78535963
彻底搞懂Scrapy的中间件(一)
中间件是Scrapy里面的一个核心概念。使用中间件可以在爬虫的请求发起之前或者请求返回之后对数据进行定制化修改,从而开发出适应不同情况的爬虫。
“中间件”这个中文名字和前面章节讲到的“中间人”只有一字之差。它们做的事情确实也非常相似。中间件和中间人都能在中途劫持数据,做一些修改再把数据传递出去。不同点在于,中间件是开发者主动加进去的组件,而中间人是被动的,一般是恶意地加进去的环节。中间件主要用来辅助开发,而中间人却多被用来进行数据的窃取、伪造甚至攻击。
在Scrapy中有两种中间件:下载器中间件(Downloader Middleware)和爬虫中间件(Spider Middleware)。
下载器中间件
Scrapy 的官方文档中,对下载器中间件的解释如下。
下载器中间件是介于Scrapy的request/response处理的钩子框架,是用于全局修改 Scrapy 的 request 和 response 的一个轻量、底层的系统。
这个介绍看起来非常绕口,但其实用容易理解的话表述就是:更换代理IP,更换Cookies,更换User-Agent,自动重试。
如果完全没有中间件,爬虫的流程如下图所示。
使用了中间件以后,爬虫的流程如下图所示。
开发代理中间件
Scrapy 设置代理终极宝典:https://zhuanlan.zhihu.com/p/79067223
scrapy 切换代理 针对特定响应状态码,使用代理重新请求
HttpProxyMiddleware(HTTP 代理中间件):https://github.com/kohn/HttpProxyMiddleware
scrapy 爬虫的自动代理中间件:https://github.com/cocoakekeyu/autoproxy获取免费代理, 主要抓的是大陆的高匿代理:https://github.com/kohn/HttpProxyMiddleware/blob/master/fetch_free_proxyes.py
scrapy 爬虫代理 --- 利用 crawlera 神器,无需再寻找代理IP:http://blog.csdn.net/xiao4816/article/details/50650075
在爬虫开发中,更换代理IP是非常常见的情况,有时候每一次访问都需要随机选择一个代理IP来进行。
中间件本身是一个Python的类,只是爬虫每次访问网站之前都要先“经过”这个类,它就能给请求换新的代理IP,这样就能实现动态改变代理。
创建 scrapy 工程:scrapy startproject <工程名>
创建完 Scrapy 工程以后( 这里创建的工程名为 AdvanceSpider ),工程文件夹下会有一个 middlewares.py 文件,打开以后其内容如下图所示:
Scrapy 自动生成的这个文件名称为 middlewares.py,名字后面的 s 表示复数,说明这个文件里面可以放很多个中间件。
middlewares.py 中有 2 个 Python类,每个类都代表一个中间件,代码如下:
# -*- coding: utf-8 -*-# Define here the models for your spider middleware
#
# See documentation in:
# https://docs.scrapy.org/en/latest/topics/spider-middleware.htmlfrom scrapy import signalsclass AdvancespiderSpiderMiddleware(object):# Not all methods need to be defined. If a method is not defined,# scrapy acts as if the spider middleware does not modify the# passed objects.@classmethoddef from_crawler(cls, crawler):# This method is used by Scrapy to create your spiders.s = cls()crawler.signals.connect(s.spider_opened, signal=signals.spider_opened)return sdef process_spider_input(self, response, spider):# Called for each response that goes through the spider# middleware and into the spider.# Should return None or raise an exception.return Nonedef process_spider_output(self, response, result, spider):# Called with the results returned from the Spider, after# it has processed the response.# Must return an iterable of Request, dict or Item objects.for i in result:yield idef process_spider_exception(self, response, exception, spider):# Called when a spider or process_spider_input() method# (from other spider middleware) raises an exception.# Should return either None or an iterable of Request, dict# or Item objects.passdef process_start_requests(self, start_requests, spider):# Called with the start requests of the spider, and works# similarly to the process_spider_output() method, except# that it doesn’t have a response associated.# Must return only requests (not items).for r in start_requests:yield rdef spider_opened(self, spider):spider.logger.info('Spider opened: %s' % spider.name)class AdvancespiderDownloaderMiddleware(object):# Not all methods need to be defined. If a method is not defined,# scrapy acts as if the downloader middleware does not modify the# passed objects.@classmethoddef from_crawler(cls, crawler):# This method is used by Scrapy to create your spiders.s = cls()crawler.signals.connect(s.spider_opened, signal=signals.spider_opened)return sdef process_request(self, request, spider):# Called for each request that goes through the downloader# middleware.# Must either:# - return None: continue processing this request# - or return a Response object# - or return a Request object# - or raise IgnoreRequest: process_exception() methods of# installed downloader middleware will be calledreturn Nonedef process_response(self, request, response, spider):# Called with the response returned from the downloader.# Must either;# - return a Response object# - return a Request object# - or raise IgnoreRequestreturn responsedef process_exception(self, request, exception, spider):# Called when a download handler or a process_request()# (from other downloader middleware) raises an exception.# Must either:# - return None: continue processing this exception# - return a Response object: stops process_exception() chain# - return a Request object: stops process_exception() chainpassdef spider_opened(self, spider):spider.logger.info('Spider opened: %s' % spider.name)
可以看到,自动生成的 middlewares.py 有 2 个中间件
- AdvancespiderSpiderMiddleware 代表 爬虫中间件。(会在 彻底搞懂Scrapy的中间件(三)里面解释说明。)
- AdvancespiderDownloaderMiddleware 代表 下载器中间件。
通过查看 Scrapy 源码可以发现,总共就 2 种 类型的中间件:爬虫中间件 和 下载器中间件。
官方中间件文档说明:https://doc.scrapy.org/en/latest/topics/downloader-middleware.html
在 middlewares.py 中添加下面一段代码( 可以直接修改 AdvancespiderDownloaderMiddleware ,也可以再单独写一个类 ,这里是单独写一个 Python 类 作为中间件 ):
图示:
示例代码:
class ProxyMiddleware(object):def __init__(self):self.settings = get_project_settings()def process_request(self, request, spider):proxy = random.choice(self.settings['PROXIES'])request.meta['proxy'] = proxy
要修改请求的代理,就需要在请求的 meta 里面添加一个 Key 为 proxy,Value 为代理 IP 的项。
由于用到了 random 和 settings,所以需要在 middlewares.py 开头导入它们:
import random
from scrapy.utils.project import get_project_settings
在下载器中间件里面有一个名为 process_request()
的方法,这个方法中的代码会在每次爬虫访问网页之前执行。
打开 settings.py,首先添加几个代理 IP:
PROXIES = ['https://114.217.243.25:8118','https://125.37.175.233:8118','http://1.85.116.218:8118'
]
需要注意的是,代理 IP 是有类型的,需要先看清楚是 HTTP 型的代理 IP 还是 HTTPS 型的代理 IP。
如果用错了,就会导致无法访问。
激活中间件
中间件写好以后,需要去 settings.py 中启动。在 settings.py 中找到下面这一段被注释的语句:
# Enable or disable downloader middlewares
# See http://scrapy.readthedocs.org/en/latest/topics/downloader-middleware.html
#DOWNLOADER_MIDDLEWARES = {
# 'AdvanceSpider.middlewares.MyCustomDownloaderMiddleware': 543,
#}
解除注释并修改,从而引用 ProxyMiddleware。修改为:
DOWNLOADER_MIDDLEWARES = {'AdvanceSpider.middlewares.ProxyMiddleware': 543,
}
这其实就是一个字典,字典的 Key 就是用点分隔的中间件路径,后面的数字表示这种中间件的顺序。由于中间件是按顺序运行的,因此如果遇到后一个中间件依赖前一个中间件的情况,中间件的顺序就至关重要。
如何确定后面的数字应该怎么写呢?最简单的办法就是从 543 开始,逐渐加一,这样一般不会出现什么大问题。如果想把中间件做得更专业一点,那就需要知道 Scrapy 自带中间件的顺序,如图下图所示:
数字越小的中间件越先执行,例如 Scrapy 自带的第1个中间件 RobotsTxtMiddleware
,它的作用是首先查看 settings.py 中ROBOTSTXT_OBEY
这一项的配置是 True
还是 False
。如果是 True
,表示要遵守 Robots.txt 协议,它就会检查将要访问的网址能不能被运行访问,如果不被允许访问,那么直接就取消这一次请求,接下来的和这次请求有关的各种操作全部都不需要继续了。
开发者自定义的中间件,会被按顺序插入到 Scrapy 自带的中间件中。爬虫会按照从 100~900 的顺序依次运行所有的中间件。直到所有中间件全部运行完成,或者遇到某一个中间件而取消了这次请求。
Scrapy 其实自带了 UA 中间件(UserAgentMiddleware)、代理中间件(HttpProxyMiddleware)和 重试中间件(RetryMiddleware)。所以,从 “原则上” 说,要自己开发这3个中间件,需要先禁用 Scrapy 里面自带的这 3 个中间件。要禁用Scrapy 的中间件,需要在 settings.py 里面将这个中间件的顺序设为 None:
DOWNLOADER_MIDDLEWARES = {'AdvanceSpider.middlewares.ProxyMiddleware': 543,'scrapy.contrib.downloadermiddleware.useragent.UserAgentMiddleware': None,'scrapy.contrib.downloadermiddleware.httpproxy.HttpProxyMiddleware': None
}
为什么说“原则上”应该禁用呢?先查看 Scrapy 自带的代理中间件的源代码,如下图所示:
从上图可以看出,如果 Scrapy 发现这个请求已经被设置了代理,那么这个中间件就会什么也不做,直接返回。因此虽然 Scrapy 自带的这个代理中间件顺序为 750,比开发者自定义的代理中间件的顺序 543 大,但是它并不会覆盖开发者自己定义的代理信息,所以即使不禁用系统自带的这个代理中间件也没有关系。
完整地激活自定义中间件的 settings.py 的部分内容如下图所示。
配置好以后运行爬虫,爬虫会在每次请求前都随机设置一个代理。
要测试代理中间件的运行效果,可以使用下面这个练习页面:http://exercise.kingname.info/exercise_middleware_ip
这个页面会返回爬虫的IP地址,直接在网页上打开,如下图所示。
这个练习页支持翻页功能,在网址后面加上“/页数”即可翻页。
例如第100页的网址为:http://exercise.kingname.info/exercise_middleware_ip/100
使用了代理中间件为每次请求更换代理的运行结果,如下图所示。
代理中间件的可用代理列表不一定非要写在 settings.py 里面,也可以将它们写到 数据库 或者 Redis 中。一个可行的自动更换代理的爬虫系统,应该有如下的3个功能。
- 有一个小爬虫ProxySpider去各大代理网站爬取免费代理并验证,将可以使用的代理IP保存到数据库中。
- 在 ProxyMiddlerware 的 process_request 中,每次从数据库里面随机选择一条代理IP地址使用。
- 周期性验证数据库中的无效代理,及时将其删除。由于免费代理极其容易失效,因此如果有一定开发预算的话,建议购买专业代理机构的代理服务,高速而稳定。
开发UA中间件
开发UA中间件和开发代理中间件几乎一样,它也是从 settings.py 配置好的 UA 列表中随机选择一项,加入到请求头中。代码如下:
class UAMiddleware(object):def process_request(self, request, spider):ua = random.choice(settings['USER_AGENT_LIST'])request.headers['User-Agent'] = ua
比IP更好的是,UA不会存在失效的问题,所以只要收集几十个UA,就可以一直使用。常见的UA如下:
USER_AGENT_LIST = [
"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.101 Safari/537.36","Dalvik/1.6.0 (Linux; U; Android 4.2.1; 2013022 MIUI/JHACNBL30.0)","Mozilla/5.0 (Linux; U; Android 4.4.2; zh-cn; HUAWEI MT7-TL00 Build/HuaweiMT7-TL00) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1","AndroidDownloadManager","Apache-HttpClient/UNAVAILABLE (java 1.4)","Dalvik/1.6.0 (Linux; U; Android 4.3; SM-N7508V Build/JLS36C)","Android50-AndroidPhone-8000-76-0-Statistics-wifi","Dalvik/1.6.0 (Linux; U; Android 4.4.4; MI 3 MIUI/V7.2.1.0.KXCCNDA)","Dalvik/1.6.0 (Linux; U; Android 4.4.2; Lenovo A3800-d Build/LenovoA3800-d)","Lite 1.0 ( http://litesuits.com )","Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 5.1; Trident/4.0; .NET4.0C; .NET4.0E; .NET CLR 2.0.50727)","Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/38.0.2125.122 Safari/537.36 SE 2.X MetaSr 1.0","Mozilla/5.0 (Linux; U; Android 4.1.1; zh-cn; HTC T528t Build/JRO03H) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30; 360browser(securitypay,securityinstalled); 360(android,uppayplugin); 360 Aphone Browser (2.0.4)",
]
配置好UA以后,在 settings.py下载器中间件里面激活它,
使用 UA 练习页来验证 UA 是否每一次都不一样。
练习页的地址为:http://exercise.kingname.info/exercise_middleware_ua
UA练习页和代理练习页一样,也是可以无限制翻页的。
示例爬虫代码:
# -*- coding: utf-8 -*-
import scrapyclass ExampleSpider(scrapy.Spider):name = 'example'allowed_domains = ['example.com']start_urls = []def __init__(self):super(ExampleSpider, self).__init__()url = 'http://exercise.kingname.info/exercise_middleware_ua'for i in range(5):self.start_urls.append(url)def parse(self, response):print(response.text)passif __name__ == '__main__':from scrapy import cmdlinecmdline.execute('scrapy crawl example'.split())
运行结果如下图所示:
开发 Cookies中间件
对于需要登录的网站,可以使用 Cookies 来保持登录状态。那么如果单独写一个小程序,用 Selenium 持续不断地用不同的账号登录网站,就可以得到很多不同的 Cookies。由于 Cookies 本质上就是一段文本,所以可以把这段文本放在 Redis 里面。这样一来,当Scrapy 爬虫请求网页时,可以从 Redis 中读取 Cookies 并给爬虫换上。这样爬虫就可以一直保持登录状态。
以下面这个练习页面为例:http://exercise.kingname.info/exercise_login_success
如果直接用Scrapy访问,得到的是登录界面的源代码,如下图所示。
现在,使用中间件,可以实现完全不改动这个loginSpider.py里面的代码,就打印出登录以后才显示的内容。
首先开发一个小程序,通过Selenium登录这个页面,并将网站返回的Headers保存到Redis中。这个小程序的代码如下图所示。
这段代码的作用是使用 Selenium 和 ChromeDriver 填写用户名和密码,实现登录练习页面,然后将登录以后的 Cookies 转换为JSON格式的字符串并保存到Redis中。
接下来,再写一个中间件,用来从Redis中读取Cookies,并把这个 Cookies 给 Scrapy 使用:
class LoginMiddleware(object):def __init__(self):self.client = redis.StrictRedis()def process_request(self, request, spider):if spider.name == 'loginSpider':cookies = json.loads(self.client.lpop('cookies').decode())request.cookies = cookies
设置了这个中间件以后,爬虫里面的代码不需要做任何修改就可以成功得到登录以后才能看到的HTML,如图12-12所示。
如果有某网站的100个账号,那么单独写一个程序,持续不断地用Selenium和ChromeDriver或者Selenium 和PhantomJS登录,获取Cookies,并将Cookies存放到Redis中。爬虫每次访问都从Redis中读取一个新的Cookies来进行爬取,就大大降低了被网站发现或者封锁的可能性。
这种方式不仅适用于登录,也适用于验证码的处理。
这一篇就讲到这里,在下一篇,我们将会介绍如何在下载器中间件中集成Selenium,进行请求重试和处理异常。
本文节选自我的新书《Python爬虫开发 从入门到实战》完整目录可以在京东查询到 https://item.jd.com/12436581.html
彻底搞懂 Scrapy 的中间件(二)
上面介绍了下载器中间件的一些简单应用。
现在再来通过案例说说如何使用下载器中间件 集成 Selenium、重试 和 处理请求异常。
在中间件中集成 Selenium
对于一些很麻烦的异步加载页面,手动寻找它的后台API代价可能太大。这种情况下可以使用Selenium和ChromeDriver或者Selenium和PhantomJS来实现渲染网页。
这是前面的章节已经讲到的内容。那么,如何把Scrapy与Selenium结合起来呢?这个时候又要用到中间件了。
创建一个SeleniumMiddleware,其代码如下:
from scrapy.http import HtmlResponse
class SeleniumMiddleware(object):def __init__(self):self.driver = webdriver.Chrome('./chromedriver')def process_request(self, request, spider):if spider.name == 'seleniumSpider':self.driver.get(request.url)time.sleep(2)body = self.driver.page_sourcereturn HtmlResponse(self.driver.current_url,body=body,encoding='utf-8',request=request)
这个中间件的作用,就是对名为“seleniumSpider”的爬虫请求的网址,使用ChromeDriver先进行渲染,然后用返回的渲染后的HTML代码构造一个Response对象。如果是其他的爬虫,就什么都不做。在上面的代码中,等待页面渲染完成是通过time.sleep(2)来实现的,当然读者也可以使用前面章节讲到的等待某个元素出现的方法来实现。
有了这个中间件以后,就可以像访问普通网页那样直接处理需要异步加载的页面,如下图所示。
在中间件里重试
在爬虫的运行过程中,可能会因为网络问题或者是网站反爬虫机制生效等原因,导致一些请求失败。在某些情况下,少量的数据丢失是无关紧要的,例如在几亿次请求里面失败了十几次,损失微乎其微,没有必要重试。但还有一些情况,每一条请求都至关重要,容不得有一次失败。此时就需要使用中间件来进行重试。
有的网站的反爬虫机制被触发了,它会自动将请求重定向到一个xxx/404.html
页面。那么如果发现了这种自动的重定向,就没有必要让这一次的请求返回的内容进入数据提取的逻辑,而应该直接丢掉或者重试。
还有一种情况,某网站的请求参数里面有一项,Key为date,Value为发起请求的这一天的日期或者发起请求的这一天的前一天的日期。例如今天是“2017-08-10”,但是这个参数的值是今天早上10点之前,都必须使用“2017-08-09”,在10点之后才能使用“2017-08-10”,否则,网站就不会返回正确的结果,而是返回“参数错误”这4个字。然而,这个日期切换的时间点受到其他参数的影响,有可能第1个请求使用“2017-08-10”可以成功访问,而第2个请求却只有使用“2017-08-09”才能访问。遇到这种情况,与其花费大量的时间和精力去追踪时间切换点的变化规律,不如简单粗暴,直接先用今天去试,再用昨天的日期去试,反正最多两次,总有一个是正确的。
以上的两种场景,使用重试中间件都能轻松搞定。
打开练习页面
http://exercise.kingname.info/exercise_middleware_retry.html。
这个页面实现了翻页逻辑,可以上一页、下一页地翻页,也可以直接跳到任意页数,如下图所示。
现在需要获取1~9页的内容,那么使用前面章节学到的内容,通过Chrome浏览器的开发者工具很容易就能发现翻页实际上是一个POST请求,提交的参数为“date”,它的值是日期“2017-08-12”,如下图所示。
使用Scrapy写一个爬虫来获取1~9页的内容,运行结果如下图所示。
从上图可以看到,第5页没有正常获取到,返回的结果是参数错误。于是在网页上看一下,发现第5页的请求中body里面的date对应的日期是“2017-08-11”,如下图所示。
如果测试的次数足够多,时间足够长,就会发现以下内容。
- 同一个时间点,不同页数提交的参数中,date对应的日期可能是今天的也可能是昨天的。
- 同一个页数,不同时间提交的参数中,date对应的日期可能是今天的也可能是昨天的。
由于日期不是今天,就是昨天,所以针对这种情况,写一个重试中间件是最简单粗暴且有效的解决办法。中间件的代码如下图所示。
这个中间件只对名为“middlewareSpider”的爬虫有用。由于middlewareSpider爬虫默认使用的是“今天”的日期,所以如果被网站返回了“参数错误”,那么正确的日期就必然是昨天的了。所以在这个中间件里面,第119行,直接把原来请求的body换成了昨天的日期,这个请求的其他参数不变。让这个中间件生效以后,爬虫就能成功爬取第5页了,如下图所示。
爬虫本身的代码,数据提取部分完全没有做任何修改,如果不看中间件代码,完全感觉不出爬虫在第5页重试过。
除了检查网站返回的内容外,还可以检查返回内容对应的网址。将上面练习页后台网址的第1个参数“para”改为404,暂时禁用重试中间件,再跑一次爬虫。其运行结果如下图所示。
此时,对于参数不正确的请求,网站会自动重定向到以下网址对应的页面:
http://exercise.kingname.info/404.html
由于Scrapy自带网址自动去重机制,因此虽然第3页、第6页和第7页都被自动转到了404页面,但是爬虫只会爬一次404页面,剩下两个404页面会被自动过滤。
对于这种情况,在重试中间件里面判断返回的网址即可解决,如下图12-21所示。
在代码的第115行,判断是否被自动跳转到了404页面,或者是否被返回了“参数错误”。如果都不是,说明这一次请求目前看起来正常,直接把response返回,交给后面的中间件来处理。如果被重定向到了404页面,或者被返回“参数错误”,那么进入重试的逻辑。如果返回了“参数错误”,那么进入第126行,直接替换原来请求的body即可重新发起请求。
如果自动跳转到了404页面,那么这里有一点需要特别注意:此时的请求,request这个对象对应的是向404页面发起的GET请求,而不是原来的向练习页后台发起的请求。所以,重新构造新的请求时必须把URL、body、请求方式、Headers全部都换一遍才可以。
由于request对应的是向404页面发起的请求,所以resquest.url对应的网址是404页面的网址。因此,如果想知道调整之前的URL,可以使用如下的代码:
request.meta['redirect_urls']
这个值对应的是一个列表。请求自动跳转了几次,这个列表里面就有几个URL。这些URL是按照跳转的先后次序依次append进列表的。由于本例中只跳转了一次,所以直接读取下标为0的元素即可,也就是原始网址。
重新激活这个重试中间件,不改变爬虫数据抓取部分的代码,直接运行以后可以正确得到1~9页的全部内容,如下图所示。
在中间件里处理异常
在默认情况下,一次请求失败了,Scrapy会立刻原地重试,再失败再重试,如此3次。如果3次都失败了,就放弃这个请求。这种重试逻辑存在一些缺陷。以代理IP为例,代理存在不稳定性,特别是免费的代理,差不多10个里面只有3个能用。而现在市面上有一些收费代理IP提供商,购买他们的服务以后,会直接提供一个固定的网址。把这个网址设为Scrapy的代理,就能实现每分钟自动以不同的IP访问网站。如果其中一个IP出现了故障,那么需要等一分钟以后才会更换新的IP。在这种场景下,Scrapy自带的重试逻辑就会导致3次重试都失败。
这种场景下,如果能立刻更换代理就立刻更换;如果不能立刻更换代理,比较好的处理方法是延迟重试。而使用Scrapy_redis就能实现这一点。爬虫的请求来自于Redis,请求失败以后的URL又放回Redis的末尾。一旦一个请求原地重试3次还是失败,那么就把它放到Redis的末尾,这样Scrapy需要把Redis列表前面的请求都消费以后才会重试之前的失败请求。这就为更换IP带来了足够的时间。
重新打开代理中间件,这一次故意设置一个有问题的代理,于是可以看到Scrapy控制台打印出了报错信息,如下图所示。
从上图可以看到Scrapy自动重试的过程。由于代理有问题,最后会抛出方框框住的异常,表示TCP超时。在中间件里面如果捕获到了这个异常,就可以提前更换代理,或者进行重试。这里以更换代理为例。首先根据上图中方框框住的内容导入TCPTimeOutError这个异常:
from twisted.internet.error import TCPTimedOutError
修改前面开发的重试中间件,添加一个process_exception()方法。这个方法接收3个参数,分别为request、exception和spider,如下图所示。
process_exception() 方法只对名为 “exceptionSpider” 的爬虫生效,如果请求遇到了 TCPTimeOutError,那么就首先调用remove_broken_proxy() 方法把失效的这个代理IP移除,然后返回这个请求对象request。返回以后,Scrapy会重新调度这个请求,就像它第一次调度一样。由于原来的ProxyMiddleware依然在工作,于是它就会再一次给这个请求更换代理IP。又由于刚才已经移除了失效的代理IP,所以ProxyMiddleware会从剩下的代理IP里面随机找一个来给这个请求换上。
特别提醒:图片中的remove_broken_proxy() 函数体里面写的是 pass,但是在实际开发过程中,读者可根据实际情况实现这个方法,写出移除失效代理的具体逻辑。
下载器中间件功能总结
能在中间件中实现的功能,都能通过直接把代码写到爬虫中实现。使用中间件的好处在于,它可以把数据爬取和其他操作分开。在爬虫的代码里面专心写数据爬取的代码;在中间件里面专心写突破反爬虫、登录、重试和渲染AJAX等操作。
对团队来说,这种写法能实现多人同时开发,提高开发效率;对个人来说,写爬虫的时候不用考虑反爬虫、登录、验证码和异步加载等操作。另外,写中间件的时候不用考虑数据怎样提取。一段时间只做一件事,思路更清晰。
彻底搞懂 Scrapy的中间件(三)
在前面两篇文章介绍了下载器中间件的使用,这篇文章将会介绍爬虫中间件(Spider Middleware)的使用。
爬虫中间件
爬虫中间件的用法与下载器中间件非常相似,只是它们的作用对象不同。下载器中间件的作用对象是请求request和返回response;爬虫中间件的作用对象是爬虫,更具体地来说,就是写在spiders文件夹下面的各个文件。它们的关系,在Scrapy的数据流图上可以很好地区分开来,如下图所示。
其中,4、5 表示下载器中间件,6、7 表示爬虫中间件。
爬虫中间件 会在以下几种情况被调用。
- 当运行到
yield scrapy.Request()
或者yield item
的时候,爬虫中间件的process_spider_output()
方法被调用。 - 当爬虫本身的代码出现了
Exception
的时候,爬虫中间件的process_spider_exception()
方法被调用。 - 当爬虫里面的某一个回调函数
parse_xxx()
被调用之前,爬虫中间件的process_spider_input()
方法被调用。 - 当运行到
start_requests()
的时候,爬虫中间件的process_start_requests()
方法被调用。
在中间件处理爬虫本身的异常
在爬虫中间件里面可以处理爬虫本身的异常。例如编写一个爬虫,爬取UA练习页面http://exercise.kingname.info/exercise_middleware_ua ,故意在爬虫中制造一个异常,如图12-26所示。
由于网站返回的只是一段普通的字符串,并不是 JSON 格式的字符串,因此使用 JSON去 解析,就一定会导致报错。这种报错和下载器中间件里面遇到的报错不一样。下载器中间件里面的报错一般是由于外部原因引起的,和代码层面无关。而现在的这种报错是由于代码本身的问题导致的,是代码写得不够周全引起的。
为了解决这个问题,除了仔细检查代码、考虑各种情况外,还可以通过开发爬虫中间件来跳过或者处理这种报错。在middlewares.py 中编写一个类:
class ExceptionCheckSpider(object):def process_spider_exception(self, response, exception, spider):print(f'返回的内容是:{response.body.decode()}\n报错原因:{type(exception)}')return None
这个类仅仅起到记录 Log 的作用。在使用 JSON 解析网站返回内容出错的时候,将网站返回的内容打印出来。
process_spider_exception()
这个方法,它可以返回 None
,也可以运行 yield item
语句或者像爬虫的代码一样,使用 yield scrapy.Request()
发起新的请求。如果运行了 yield item
或者 yield scrapy.Request()
,程序就会绕过爬虫里面原有的代码。
例如,对于有异常的请求,不需要进行重试,但是需要记录是哪一个请求出现了异常,此时就可以在爬虫中间件里面检测异常,然后生成一个只包含标记的 item。还是以抓取 http://exercise.kingname.info/exercise_middleware_retry.html 这个练习页的内容为例,但是这一次不进行重试,只记录哪一页出现了问题。先看爬虫的代码,这一次在 meta 中把页数带上,如下图所示。
爬虫里面如果发现了参数错误,就使用 raise 这个关键字人工抛出一个自定义的异常。在实际爬虫开发中,读者也可以在某些地方故意不使用 try ... except 捕获异常,而是让异常直接抛出。例如 XPath 匹配处理的结果,直接读里面的值,不用先判断列表是否为空。这样如果列表为空,就会被抛出一个 IndexError,于是就能让爬虫的流程进入到爬虫中间件的process_spider_exception()
中。
在 items.py 里面创建了一个 ErrorItem 来记录哪一页出现了问题,如下图所示。
接下来,在爬虫中间件中将出错的页面和当前时间存放到ErrorItem里面,并提交给pipeline,保存到MongoDB中,如下图所示。
这样就实现了记录错误页数的功能,方便在后面对错误原因进行分析。由于这里会把 item 提交给 pipeline,所以不要忘记在settings.py 里面打开 pipeline,并配置好 MongoDB。储存错误页数到 MongoDB 的代码如下图所示。
激活 爬虫中间件
爬虫中间件的激活方式与下载器中间件非常相似,在 settings.py 中,在下载器中间件配置项的上面就是爬虫中间件的配置项,它默认也是被注释了的,解除注释,并把自定义的爬虫中间件添加进去即可,如下图所示。
Scrapy也有几个自带的爬虫中间件,它们的名字和顺序如下图所示。
下载器中间件的数字越小越接近 Scrapy 引擎,数字越大越接近爬虫。如果不能确定自己的自定义中间件应该靠近哪个方向,那么就在 500~700 之间选择最为妥当。
爬虫中间件 输入/输出
在爬虫中间件里面还有两个不太常用的方法,
分别为process_spider_input(response, spider)
和process_spider_output(response, result, spider)
。
process_spider_input(response, spider)
在下载器中间件处理完成后,马上要进入某个回调函数parse_xxx()前调用。process_spider_output(response, result, output)
是在爬虫运行yield item
或者yield scrapy.Request()
的时候调用。在这个方法处理完成以后,数据如果是 item,就会被交给 pipeline;如果是请求,就会被交给调度器,然后下载器中间件才会开始运行。所以在这个方法里面可以进一步对 item 或者 请求 做一些修改。这个方法的参数 result 就是爬虫爬出来的 item 或者scrapy.Request()
。由于 yield 得到的是一个生成器,生成器是可以迭代的,所以 result 也是可以迭代的,可以使用 for 循环来把它展开。
对 item 进行处理
def process_spider_output(response, result, spider):for item in result:if isinstance(item, scrapy.Item):# 这里可以对即将被提交给pipeline的item进行各种操作print(f'item将会被提交给pipeline')yield item
或者对 请求 进行监控和修改:
def process_spider_output(response, result, spider):for request in result:if not isinstance(request, scrapy.Item):# 这里可以对请求进行各种修改print('现在还可以对请求对象进行修改。。。。')request.meta['request_start_time'] = time.time()yield request