彻底搞懂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_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”才能访问。遇到这种情况,与其花费大量的时间和精力去追踪时间切换点的变化规律,不如简单粗暴,直接先用今天去试,再用昨天的日期去试,反正最多两次,总有一个是正确的。

以上的两种场景,使用重试中间件都能轻松搞定。

打开练习页面

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”,如下图所示。

如果测试的次数足够多,时间足够长,就会发现以下内容。

  1. 同一个时间点,不同页数提交的参数中,date对应的日期可能是今天的也可能是昨天的。
  2. 同一个页数,不同时间提交的参数中,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等操作。

对团队来说,这种写法能实现多人同时开发,提高开发效率;对个人来说,写爬虫的时候不用考虑反爬虫、登录、验证码和异步加载等操作。另外,写中间件的时候不用考虑数据怎样提取。一段时间只做一件事,思路更清晰。

本文节选自我的新书《Python爬虫开发 从入门到实战》完整目录可以在京东查询到 https://item.jd.com/12436581.html

原文地址:https://www.cnblogs.com/xieqiankun/p/know_middleware_of_scrapy_2.html

时间: 2024-11-03 09:25:05

彻底搞懂Scrapy的中间件(二)的相关文章

彻底搞懂Scrapy的中间件(一)

中间件是Scrapy里面的一个核心概念.使用中间件可以在爬虫的请求发起之前或者请求返回之后对数据进行定制化修改,从而开发出适应不同情况的爬虫. "中间件"这个中文名字和前面章节讲到的"中间人"只有一字之差.它们做的事情确实也非常相似.中间件和中间人都能在中途劫持数据,做一些修改再把数据传递出去.不同点在于,中间件是开发者主动加进去的组件,而中间人是被动的,一般是恶意地加进去的环节.中间件主要用来辅助开发,而中间人却多被用来进行数据的窃取.伪造甚至攻击. 在Scrap

【搞懂Java多线程之二】多线程调度及守护进程

在前一篇文章中说到,所有处在就绪状态中的线程,操作系统会选择优先级最高的优先进行调度,那么是不是优先级高的线程就一定比优先级低的线程先执行呢?线程的优先级又是怎么划分的呢?这篇文章,楼楼就要来说说这个问题啦!欢迎关注我的个人博客主页www.anycodex.com 1.线程的优先级 在Java中,线程优先级的范围为0-10,整数值越大,说明优先级更高. 几个相关的宏定义: MAX_PRIORITY 10,最高优先级 MIN_PRIORITY 1,最低优先级 NORM_PRIORITY 5,默认优

准零基础搞懂FFT快速傅里叶变换及其实现程序(二)

上一篇文章我们了解了DFT的原理,FFT是基于DFT的更适合计算机运算的算法,本文我们就正式开始学习FFT的原理. 首先我么先来宏观的看一下FFT.如果我们把整个FFT的算法看成一个黑盒子的话,那么它的输入就是时间波形信号,比如声音波形(横轴为时间,纵轴为振幅).外什么FFT要比DFT速度更快呢?下面(图1)解释了FFT和DFT的(对于计算机的)算法复杂度 图1 从上面的数学表达式可以看出,一个1024采样点的FFT比DFT块了102.4倍.如果傅里叶变换的数量级更大,FFT的速度优势会更明显.

搞懂分布式技术17,18:分布式事务总结

搞懂分布式技术17:浅析分布式事务 众所周知,数据库能实现本地事务,也就是在同一个数据库中,你可以允许一组操作要么全都正确执行,要么全都不执行.这里特别强调了本地事务,也就是目前的数据库只能支持同一个数据库中的事务.但现在的系统往往采用微服务架构,业务系统拥有独立的数据库,因此就出现了跨多个数据库的事务需求,这种事务即为"分布式事务".那么在目前数据库不支持跨库事务的情况下,我们应该如何实现分布式事务呢?本文首先会为大家梳理分布式事务的基本概念和理论基础,然后介绍几种目前常用的分布式事

搞懂分布式技术2:分布式一致性协议与Paxos,Raft算法

搞懂分布式技术2:分布式一致性协议与Paxos,Raft算法 2PC 由于BASE理论需要在一致性和可用性方面做出权衡,因此涌现了很多关于一致性的算法和协议.其中比较著名的有二阶提交协议(2 Phase Commitment Protocol),三阶提交协议(3 Phase Commitment Protocol)和Paxos算法. 本文要介绍的2PC协议,分为两个阶段提交一个事务.并通过协调者和各个参与者的配合,实现分布式一致性. 两个阶段事务提交协议,由协调者和参与者共同完成. 角色 XA概

分分搞懂c#中的委托

分分搞懂c#中的委托: 不说废话,不来虚的概念,不管代码是否有意义,看我的优化之路,你会理解委托了: 源代码1 public class test { //我们不管代码是否有意义,我们直接看代码重构和一步步优化的过程 int flage = 1; public void show(int a) { if (flage == 1) { do1(a); } else if (flage == 2) { do2(a); } else if (flage == 3) { do3(a); } else i

彻底搞懂字符编码(unicode,mbcs,utf-8,utf-16,utf-32,big endian,little endian...)[转]

最近有一些朋友常问我一些乱码的问题,和他们交流过程中,发现这个编码的相关知识还真是杂乱不堪,不少人对一些知识理解似乎也有些偏差,网上百度, google的内容,也有不少以讹传讹,根本就是错误的(例如说 unicode编码是两个字节),各种软件让你选择编码的时候,常常是很长的一个选单,让用户不知道该如何选.基于这样的问题,我就写下我的理解吧,一方面帮助一些需要帮助的人纠正认识,一方面作为自己以后备查的资料. 1. ASCII(American Standard Code for Informati

JavaEE 要懂的小事:二、图解 Cookie(小甜饼)

Writer      :BYSocket(泥沙砖瓦浆木匠) 微         博:BYSocket 豆         瓣:BYSocket FaceBook:BYSocket Twitter    :BYSocket 上一篇 图解Http协议 ,这次继续Http家族中的Cookie.泥瓦匠最近看到博客园中一篇好文,如图: 这就是因为浏览器Cookie太大,导致请求时,请求头域过大造成发送失败.下面咱们就了解了解Cookie.按着以前的思路图文并茂哈,没图说个XX. 一.概述 首先从HTTP

五个小例子教你搞懂 JavaScript 作用域问题

众所周知,JavaScript 的作用域和其他传统语言(类C)差别比较大,掌握并熟练运用JavaScript 的作用域知识,不仅有利于我们阅读理解别人的代码,也有助于我们编写自己的可靠代码. 下面笔者将使用五个小例子来给大家分析下 JavaScript 的作用域要注意的问题. 感谢 例子的来源 (这5个例子我做错了2个 [嘿嘿,尽情鄙视吧],笔者就是要 死磕自己,奉献大家!) 先给出五个例子: 每个例子旁边都会给出答案的链接,如果你全部都正确了,你可以忽略这篇短文,并深深的鄙视下笔者. 例一: