中间件是Scrapy里面的一个核心概念。使用中间件可以在爬虫的请求发起之前或者请求返回之后对数据进行定制化修改,从而开发出适应不同情况的爬虫。
“中间件”这个中文名字和前面章节讲到的“中间人”只有一字之差。它们做的事情确实也非常相似。中间件和中间人都能在中途劫持数据,做一些修改再把数据传递出去。不同点在于,中间件是开发者主动加进去的组件,而中间人是被动的,一般是恶意地加进去的环节。中间件主要用来辅助开发,而中间人却多被用来进行数据的窃取、伪造甚至攻击。
在Scrapy中有两种中间件:下载器中间件(Downloader Middleware)和爬虫中间件(Spider Middleware)。
一、下载器中间件
Scrapy的官方文档中,对下载器中间件的解释如下。
下载器中间件是介于Scrapy的request/response处理的钩子框架,是用于全局修改Scrapy request和response的一个轻量、底层的系统。
这个介绍看起来非常绕口,但其实用容易理解的话表述就是:更换代理IP,更换Cookies,更换User-Agent,自动重试。
如果完全没有中间件,爬虫的流程如图:
1. 开发代理中间件
在爬虫开发中,更换代理IP是非常常见的情况,有时候每一次访问都需要随机选择一个代理IP来进行。
中间件本身是一个Python的类,只要爬虫每次访问网站之前都先“经过”这个类,它就能给请求换新的代理IP,这样就能实现动态改变代理。
在创建一个Scrapy工程以后,工程文件夹下会有一个middlewares.py文件,打开以后其内容如图:
Scrapy自动生成的这个文件名称为middlewares.py,名字后面的s表示复数,说明这个文件里面可以放很多个中间件。Scrapy自动创建的这个中间件是一个爬虫中间件,现在先来创建一个自动更换代理IP的中间件。
在middlewares.py中添加下面一段代码:
要修改请求的代理,就需要在请求的meta里面添加一个Key为proxy,Value为代理IP的项。
由于用到了random和settings,所以需要在middlewares.py开头导入它们:
在下载器中间件里面有一个名为“process_request()”的方法,这个方法中的代码会在每次爬虫访问网页之前执行。
打开settings.py,首先添加几个代理IP:
需要注意的是,代理IP是有类型的,需要先看清楚是HTTP型的代理IP还是HTTPS型的代理IP。如果用错了,就会导致无法访问。
2. 激活中间件
中间件写好以后,需要去settings.py中启动。在settings.py中找到下面这一段被注释的语句:
解除注释并修改,从而引用ProxyMiddleware。修改为:
这其实就是一个字典,字典的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:
为什么说“原则上”应该禁用呢?先查看Scrapy自带的代理中间件的源代码,如图:
从图可以看出,如果Scrapy发现这个请求已经被设置了代理,那么这个中间件就会什么也不做,直接返回。因此虽然Scrapy自带的这个代理中间件顺序为750,比开发者自定义的代理中间件的顺序543大,但是它并不会覆盖开发者自己定义的代理信息,所以即使不禁用系统自带的这个代理中间件也没有关系。
完整地激活自定义中间件的settings.py的部分内容如图:
代理中间件的可用代理列表不一定非要写在settings.py里面,也可以将它们写到数据库或者Redis中。一个可行的自动更换代理的爬虫系统,应该有如下的3个功能。
(1)有一个小爬虫ProxySpider去各大代理网站爬取免费代理并验证,将可以使用的代理IP保存到数据库中。
(2)在ProxyMiddlerware的process_request中,每次从数据库里面随机选择一条代理IP地址使用。
(3)周期性验证数据库中的无效代理,及时将其删除。
由于免费代理极其容易失效,因此如果有一定开发预算的话,建议购买专业代理机构的代理服务,高速而稳定。
3. 开发UA中间件
开发UA中间件和开发代理中间件几乎一样,它也是从settings.py配置好的UA列表中随机选择一项,加入到请求头中。代码如下:
比IP更好的是,UA不会存在失效的问题,所以只要收集几十个UA,就可以一直使用。常见的UA如下: 配置好UA以后,在settings.py下载器中间件里面激活它。
4. 开发Cookies中间件
对于需要登录的网站,可以使用Cookies来保持登录状态。那么如果单独写一个小程序,用Selenium持续不断地用不同的账号登录网站,就可以得到很多不同的Cookies。由于Cookies本质上就是一段文本,所以可以把这段文本放在Redis里面。这样一来,当Scrapy爬虫请求网页时,可以从Redis中读取Cookies并给爬虫换上。这样爬虫就可以一直保持登录状态。
首先开发一个小程序,通过Selenium登录这个页面,并将网站返回的Headers保存到Redis中。这个小程序的代码如图:
这段代码的作用是使用Selenium和ChromeDriver填写用户名和密码,实现登录练习页面,然后将登录以后的Cookies转换为JSON格式的字符串并保存到Redis中。
接下来,再写一个中间件,用来从Redis中读取Cookies,并把这个Cookies给Scrapy使用:
如果有某网站的100个账号,那么单独写一个程序,持续不断地用Selenium和ChromeDriver或者Selenium和PhantomJS登录,获取Cookies,并将Cookies存放到Redis中。爬虫每次访问都从Redis中读取一个新的Cookies来进行爬取,就大大降低了被网站发现或者封锁的可能性。
这种方式不仅适用于登录,也适用于验证码的处理。
5. 在中间件中集成Selenium
对于一些很麻烦的异步加载页面,手动寻找它的后台API代价可能太大。这种情况下可以使用Selenium和ChromeDriver或者Selenium和PhantomJS来实现渲染网页。那么,如何把Scrapy与Selenium结合起来呢?这个时候又要用到中间件了。
创建一个SeleniumMiddleware,其代码如下:
这个中间件的作用,就是对名为“seleniumSpider”的爬虫请求的网址,使用ChromeDriver先进行渲染,然后用返回的渲染后的HTML代码构造一个Response对象。如果是其他的爬虫,就什么都不做。在上面的代码中,等待页面渲染完成是通过time.sleep(2)来实现的,当然也可以使用等待某个元素出现的方法来实现。
有了这个中间件以后,就可以像访问普通网页那样直接处理需要异步加载的页面。
6. 在中间件里重试
在爬虫的运行过程中,可能会因为网络问题或者是网站反爬虫机制生效等原因,导致一些请求失败。在某些情况下,少量的数据丢失是无关紧要的,例如在几亿次请求里面失败了十几次,损失微乎其微,没有必要重试。但还有一些情况,每一条请求都至关重要,容不得有一次失败。此时就需要使用中间件来进行重试。
有的网站的反爬虫机制被触发了,它会自动将请求重定向到一个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”才能访问。遇到这种情况,与其花费大量的时间和精力去追踪时间切换点的变化规律,不如简单粗暴,直接先用今天去试,再用昨天的日期去试,反正最多两次,总有一个是正确的。
7. 在中间件里处理异常
在默认情况下,一次请求失败了,Scrapy会立刻原地重试,再失败再重试,如此3次。如果3次都失败了,就放弃这个请求。这种重试逻辑存在一些缺陷。以代理IP为例,代理存在不稳定性,特别是免费的代理,差不多10个里面只有3个能用。而现在市面上有一些收费代理IP提供商,购买他们的服务以后,会直接提供一个固定的网址。把这个网址设为Scrapy的代理,就能实现每分钟自动以不同的IP访问网站。如果其中一个IP出现了故障,那么需要等一分钟以后才会更换新的IP。在这种场景下,Scrapy自带的重试逻辑就会导致3次重试都失败。
这种场景下,如果能立刻更换代理就立刻更换;如果不能立刻更换代理,比较好的处理方法是延迟重试。而使用Scrapy_redis就能实现这一点。爬虫的请求来自于Redis,请求失败以后的URL又放回Redis的末尾。一旦一个请求原地重试3次还是失败,那么就把它放到Redis的末尾,这样Scrapy需要把Redis列表前面的请求都消费以后才会重试之前的失败请求。这就为更换IP带来了足够的时间。
8. 下载器中间件功能总结
能在中间件中实现的功能,都能通过直接把代码写到爬虫中实现。使用中间件的好处在于,它可以把数据爬取和其他操作分开。在爬虫的代码里面专心写数据爬取的代码;在中间件里面专心写突破反爬虫、登录、重试和渲染AJAX等操作。
对团队来说,这种写法能实现多人同时开发,提高开发效率;对个人来说,写爬虫的时候不用考虑反爬虫、登录、验证码和异步加载等操作。另外,写中间件的时候不用考虑数据怎样提取。一段时间只做一件事,思路更清晰。
--------------------------------------
没有自由的秩序和没有秩序的自由,同样具有破坏性。