中间件是Scrapy里面的一个核心概念。使用中间件可以在爬虫的请求发起之前或者请求返回之后对数据进行定制化修改,从而开发出适应不同情况的爬虫。
“中间件”这个中文名字和前面章节讲到的“中间人”只有一字之差。它们做的事情确实也非常相似。中间件和中间人都能在中途劫持数据,做一些修改再把数据传递出去。不同点在于,中间件是开发者主动加进去的组件,而中间人是被动的,一般是恶意地加进去的环节。中间件主要用来辅助开发,而中间人却多被用来进行数据的窃取、伪造甚至攻击。
在Scrapy中有两种中间件:下载器中间件(Downloader Middleware)和爬虫中间件(Spider Middleware)。
这一篇主要讲解下载器中间件的第一部分。
下载器中间件
Scrapy的官方文档中,对下载器中间件的解释如下。
下载器中间件是介于Scrapy的request/response处理的钩子框架,是用于全局修改Scrapy request和response的一个轻量、底层的系统。
这个介绍看起来非常绕口,但其实用容易理解的话表述就是:更换代理IP,更换Cookies,更换User-Agent,自动重试。
如果完全没有中间件,爬虫的流程如下图所示。
使用了中间件以后,爬虫的流程如下图所示。
下载器中间件是介于Scrapy的request/response处理的钩子框架,是用于全局修改Scrapy request和response的一个轻量、底层的系统。
激活Downloader Middleware
要激活下载器中间件组件,将其加入到 DOWNLOADER_MIDDLEWARES 设置中。 该设置是一个字典(dict),键为中间件类的路径,值为其中间件的顺序(order)。像下面这样
代码语言:javascript复制
-
DOWNLOADER_MIDDLEWARES = {
-
'myproject.middlewares.CustomDownloaderMiddleware': 543,
-
}
DOWNLOADER_MIDDLEWARES 设置会与Scrapy定义的 DOWNLOADER_MIDDLEWARES_BASE 设置合并(但不是覆盖), 而后根据顺序(order)进行排序,最后得到启用中间件的有序列表: 第一个中间件是最靠近引擎的,最后一个中间件是最靠近下载器的。
由于每个中间件执行不同的动作,你的中间件可能会依赖于之前(或者之后)执行的中间件,因此顺序是很重要的。
如果你想禁止内置的(在 DOWNLOADER_MIDDLEWARES_BASE 中设置并默认启用的)中间件, 你必须在项目的 DOWNLOADER_MIDDLEWARES 设置中定义该中间件,并将其值赋为 None 。 例如,如果您想要关闭user-agent中间件:
代码语言:javascript复制
-
DOWNLOADER_MIDDLEWARES = {
-
'myproject.middlewares.CustomDownloaderMiddleware': 543,
-
'scrapy.downloadermiddlewares.useragent.UserAgentMiddleware': None,
-
}
自定义Downloader Middleware
如果你想自定义一个属于你的下载器中间件,只需要编写一个下载器中间件类,继承自scrapy.downloadermiddlewares.DownloaderMiddleware
,而后在里面重写以下的几个方法。
- process_request(request, spider)
当每个request通过下载中间件时,该方法被调用。
process_request() 必须返回其中之一: 返回 None 、返回一个 Response 对象、返回一个 Request 对象或raise IgnoreRequest 。
- 如果其返回 None ,Scrapy将继续处理该request,执行其他的中间件的相应方法,直到合适的下载器处理函数(download handler)被调用, 该request被执行(其response被下载)。
- 如果其返回 Response 对象,Scrapy将不会调用 任何 其他的 process_request() 或 process_exception() 方法,或相应地下载函数; 其将返回该response。 已安装的中间件的 process_response() 方法则会在每个response返回时被调用。
- 如果其返回 Request 对象,Scrapy则停止调用 process_request方法并重新调度返回的request。当新返回的request被执行后, 相应地中间件链将会根据下载的response被调用。
- 如果其raise一个 IgnoreRequest 异常,则安装的下载中间件的 process_exception() 方法会被调用。如果没有任何一个方法处理该异常, 则request的errback(Request.errback)方法会被调用。如果没有代码处理抛出的异常, 则该异常被忽略且不记录(不同于其他异常那样)。
参数: request(Request 对象)–处理的request spider(Spider 对象)–该request对应的spider
- process_response(request, response, spider)
process_request() 必须返回以下之一: 返回一个 Response 对象、 返回一个 Request 对象或raise一个 IgnoreRequest 异常。
- 如果其返回一个 Response (可以与传入的response相同,也可以是全新的对象), 该response会被在链中的其他中间件的 process_response() 方法处理。
- 如果其返回一个 Request 对象,则中间件链停止, 返回的request会被重新调度下载。处理类似于 process_request() 返回request所做的那样。
- 如果其抛出一个 IgnoreRequest 异常,则调用request的errback(Request.errback)。 如果没有代码处理抛出的异常,则该异常被忽略且不记录(不同于其他异常那样)。
参数: request (Request 对象) – response所对应的request response (Response 对象) – 被处理的response spider (Spider 对象) – response所对应的spider
- process_exception(request, exception, spider)
当下载处理器(download handler)或 process_request() (下载中间件)抛出异常(包括IgnoreRequest异常)时,Scrapy调用 process_exception() 。
process_exception() 应该返回以下之一: 返回 None 、 一个 Response 对象、或者一个 Request 对象。
- 如果其返回 None ,Scrapy将会继续处理该异常,接着调用已安装的其他中间件的 process_exception() 方法,直到所有中间件都被调用完毕,则调用默认的异常处理。
- 如果其返回一个 Response 对象,则已安装的中间件链的 process_response() 方法被调用。Scrapy将不会调用任何其他中间件的 process_exception() 方法。
- 如果其返回一个 Request 对象, 则返回的request将会被重新调用下载。这将停止中间件的 process_exception() 方法执行,就如返回一个response的那样。
参数: request (是 Request 对象) – 产生异常的request exception (Exception 对象) – 抛出的异常 spider (Spider 对象) – request对应的spider
内置Downloader Middleware介绍
- CookiesMiddleware
该中间件使得爬取需要cookie(例如使用session)的网站成为了可能。 其追踪了web server发送的cookie,并在之后的request中发送回去, 就如浏览器所做的那样。
以下设置可以用来配置cookie中间件:
COOKIES_ENABLED默认为True
COOKIES_DEBUG默认为False
Scrapy通过使用 cookiejar Request meta key来支持单spider追踪多cookie session。 默认情况下其使用一个cookie jar(session),不过我们可以传递一个标示符来使用多个。
比如:
-
for i, url in enumerate(urls):
-
yield scrapy.Request("http://www.example.com", meta={'cookiejar': i},
-
callback=self.parse_page)
需要注意的是 cookiejar meta key不是”黏性的(sticky)”。 我们需要在之后的request请求中接着传递。 比如:
-
def parse_page(self, response):
-
# do some processing
-
return scrapy.Request("http://www.example.com/otherpage",
-
meta={'cookiejar': response.meta['cookiejar']},
-
callback=self.parse_other_page)
-
- DefaultHeadersMiddleware 该中间件设置 DEFAULT_REQUEST_HEADERS 指定的默认request header。
- DownloadTimeoutMiddleware 该中间件设置 DOWNLOAD_TIMEOUT 指定的request下载超时时间.
- HttpAuthMiddleware 该中间件完成某些使用 Basic access authentication (或者叫HTTP认证)的spider生成的请求的认证过程。
- HttpCacheMiddleware 该中间件为所有HTTP request及response提供了底层(low-level)缓存支持。 其由cache存储后端及cache策略组成。
- HttpCompressionMiddleware 该中间件提供了对压缩(gzip, deflate)数据的支持
- ChunkedTransferMiddleware 该中间件添加了对 chunked transfer encoding 的支持。
- HttpProxyMiddleware 该中间件提供了对request设置HTTP代理的支持。您可以通过在 Request 对象中设置 proxy 元数据来开启代理。
- RedirectMiddleware 该中间件根据response的状态处理重定向的request。通过该中间件的(被重定向的)request的url可以通过 Request.meta 的 redirect_urls 键找到。
- MetaRefreshMiddleware 该中间件根据meta-refresh html标签处理request重定向。
- RetryMiddleware 该中间件将重试可能由于临时的问题,例如连接超时或者HTTP 500错误导致失败的页面。 爬取进程会收集失败的页面并在最后,spider爬取完所有正常(不失败)的页面后重新调度。 一旦没有更多需要重试的失败页面,该中间件将会发送一个信号(retry_complete), 其他插件可以监听该信号。
- RobotsTxtMiddleware 该中间件过滤所有robots.txt eclusion standard中禁止的request。 确认该中间件及 ROBOTSTXT_OBEY 设置被启用以确保Scrapy尊重robots.txt。
- UserAgentMiddleware 用于覆盖spider的默认user agent的中间件。 要使得spider能覆盖默认的user agent,其 user_agent 属性必须被设置。
- AjaxCrawlMiddleware 根据meta-fragment html标签查找 ‘AJAX可爬取’ 页面的中间件。
开发代理中间件
在爬虫开发中,更换代理IP是非常常见的情况,有时候每一次访问都需要随机选择一个代理IP来进行。
中间件本身是一个Python的类,只要爬虫每次访问网站之前都先“经过”这个类,它就能给请求换新的代理IP,这样就能实现动态改变代理。
在创建一个Scrapy工程以后,工程文件夹下会有一个middlewares.py文件,打开以后其内容如下图所示。
Scrapy自动生成的这个文件名称为middlewares.py,名字后面的s表示复数,说明这个文件里面可以放很多个中间件。Scrapy自动创建的这个中间件是一个爬虫中间件,这种类型在第三篇文章会讲解。现在先来创建一个自动更换代理IP的中间件。
在middlewares.py中添加下面一段代码:
代码语言:javascript复制class ProxyMiddleware(object):
def process_request(self, request, spider):
proxy = random.choice(settings['PROXIES'])
request.meta['proxy'] = proxy
要修改请求的代理,就需要在请求的meta里面添加一个Key为proxy,Value为代理IP的项。
由于用到了random和settings,所以需要在middlewares.py开头导入它们:
代码语言:javascript复制import random
from scrapy.conf import settings
在下载器中间件里面有一个名为process_request()
的方法,这个方法中的代码会在每次爬虫访问网页之前执行。
打开settings.py,首先添加几个代理IP:
代码语言:javascript复制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中找到下面这一段被注释的语句:
代码语言:javascript复制# Enable or disable downloader middlewares
# See http://scrapy.readthedocs.org/en/latest/topics/downloader-middleware.html
#DOWNLOADER_MIDDLEWARES = {
# 'AdvanceSpider.middlewares.MyCustomDownloaderMiddleware': 543,
#}
解除注释并修改,从而引用ProxyMiddleware。修改为:
代码语言:javascript复制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:
代码语言:javascript复制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的部分内容如下图所示。
配置好以后运行爬虫,爬虫会在每次请求前都随机设置一个代理。要测试代理中间件的运行效果,可以使用下面这个练习页面:
代码语言:javascript复制http://exercise.kingname.info/exercise_middleware_ip
这个页面会返回爬虫的IP地址,直接在网页上打开,如下图所示。
这个练习页支持翻页功能,在网址后面加上“/页数”即可翻页。例如第100页的网址为:
代码语言:javascript复制http://exercise.kingname.info/exercise_middleware_ip/100
使用了代理中间件为每次请求更换代理的运行结果,如下图所示。
代理中间件的可用代理列表不一定非要写在settings.py里面,也可以将它们写到数据库或者Redis中。一个可行的自动更换代理的爬虫系统,应该有如下的3个功能。
- 有一个小爬虫ProxySpider去各大代理网站爬取免费代理并验证,将可以使用的代理IP保存到数据库中。
- 在ProxyMiddlerware的process_request中,每次从数据库里面随机选择一条代理IP地址使用。
- 周期性验证数据库中的无效代理,及时将其删除。 由于免费代理极其容易失效,因此如果有一定开发预算的话,建议购买专业代理机构的代理服务,高速而稳定。
开发UA中间件
开发UA中间件和开发代理中间件几乎一样,它也是从settings.py配置好的UA列表中随机选择一项,加入到请求头中。代码如下:
代码语言:javascript复制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如下:
代码语言:javascript复制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是否每一次都不一样。练习页的地址为:
代码语言:javascript复制http://exercise.kingname.info/exercise_middleware_ua。
UA练习页和代理练习页一样,也是可以无限制翻页的。
运行结果如下图所示。
开发Cookies中间件
对于需要登录的网站,可以使用Cookies来保持登录状态。那么如果单独写一个小程序,用Selenium持续不断地用不同的账号登录网站,就可以得到很多不同的Cookies。由于Cookies本质上就是一段文本,所以可以把这段文本放在Redis里面。这样一来,当Scrapy爬虫请求网页时,可以从Redis中读取Cookies并给爬虫换上。这样爬虫就可以一直保持登录状态。
以下面这个练习页面为例:
代码语言:javascript复制http://exercise.kingname.info/exercise_login_success
如果直接用Scrapy访问,得到的是登录界面的源代码,如下图所示。
现在,使用中间件,可以实现完全不改动这个loginSpider.py里面的代码,就打印出登录以后才显示的内容。
首先开发一个小程序,通过Selenium登录这个页面,并将网站返回的Headers保存到Redis中。这个小程序的代码如下图所示。
这段代码的作用是使用Selenium和ChromeDriver填写用户名和密码,实现登录练习页面,然后将登录以后的Cookies转换为JSON格式的字符串并保存到Redis中。
接下来,再写一个中间件,用来从Redis中读取Cookies,并把这个Cookies给Scrapy使用:
代码语言:javascript复制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
对于一些很麻烦的异步加载页面,手动寻找它的后台API代价可能太大。这种情况下可以使用Selenium和ChromeDriver或者Selenium和PhantomJS来实现渲染网页。
这是前面的章节已经讲到的内容。那么,如何把Scrapy与Selenium结合起来呢?这个时候又要用到中间件了。
创建一个SeleniumMiddleware,其代码如下:
代码语言:javascript复制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_source
return 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”才能访问。遇到这种情况,与其花费大量的时间和精力去追踪时间切换点的变化规律,不如简单粗暴,直接先用今天去试,再用昨天的日期去试,反正最多两次,总有一个是正确的。
以上的两种场景,使用重试中间件都能轻松搞定。
打开练习页面
代码语言:javascript复制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,暂时禁用重试中间件,再跑一次爬虫。其运行结果如下图所示。
此时,对于参数不正确的请求,网站会自动重定向到以下网址对应的页面:
代码语言:javascript复制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,可以使用如下的代码:
代码语言:javascript复制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这个异常:
代码语言:javascript复制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等操作。
对团队来说,这种写法能实现多人同时开发,提高开发效率;对个人来说,写爬虫的时候不用考虑反爬虫、登录、验证码和异步加载等操作。另外,写中间件的时候不用考虑数据怎样提取。一段时间只做一件事,思路更清晰。
爬虫中间件
爬虫中间件的用法与下载器中间件非常相似,只是它们的作用对象不同。下载器中间件的作用对象是请求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中编写一个类:
代码语言:javascript复制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循环来把它展开。
def process_spider_output(response, result, spider):
for item in result:
if isinstance(item, scrapy.Item):
# 这里可以对即将被提交给pipeline的item进行各种操作
print(f'item将会被提交给pipeline')
yield item
或者对请求进行监控和修改:
代码语言:javascript复制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