Scrapy中process_request返回request和None的区别

266 阅读4分钟

本文首发于个人博客,文章链接为:blog.d77.xyz/archives/67…

前言

上篇文章中遇到了一个问题,就是在 process_request 函数中返回 request 对象导致爬虫退出的问题,这篇文章来解释下。

环境搭建

为了弄清楚这个问题,首先搭建一个可以运行 scrapy 的环境,新建一个 scrapy 项目,scrapy startproject testscrapy,之后添加一个名为 testscrapy 的爬虫,代码为:

import scrapy as scrapy
from scrapy import Request


class TestScrapy(scrapy.Spider):
    name = 'testscrapy'

    def start_requests(self):
        # 使用http方便抓包
        yield Request(url='http://httpbin.org', callback=self.parse)

    def parse(self, response, **kwargs):
        print(response.status)
        print(response.xpath('//title/text()').get())

并且在设置中开启下载中间件,运行下爬虫没有问题环境就搭建好了。

问题原因

首先以正常模式运行爬虫,没有问题,可以正常打印状态码和 title。

接着修改下载中间件,在请求头中随意添加一个键值对。

运行爬虫,抓包可以看到请求头被正常添加进去了。

此时如果将返回值修改为 request 对象的话,爬虫会直接退出。

并不会打印状态码和 title,这里有两条日志供我们分析。

  • 首先是递归错误,超过了最大递归深度(RecursionError: maximum recursion depth exceeded while calling a Python object)。

  • 其次是不再显示重复项(no more duplicates will be shown (see DUPEFILTER_DEBUG to show all duplicates))

看到第一条日志我猜测可能是出现了死循环,process_request 函数在不停的调用自己。

看到第二条日志我猜测可能是 request 被重复调度了,scrapy 发现返回的 request 对象已经被请求过了,所以不再调度它。由于 scrapy 默认开启了 URL 去重,所以 request 对象会被丢掉。

再来看日志顺序,重复调度日志在递归错误之后,这里可以猜测在递归错误报错后(因为死循环时是无法调度的)调度器才调度到了 request 对象,然后发现请求过了,之后打印日志,丢掉 request 请求对象。

为了证明 process_request 进入了死循环,加一个延迟,运行爬虫。

可以看到,一直在打印数字 1,没有打印重复调度的日志,直到递归错误,爬虫才会往下继续运行提示重复调度。

到这里原因就很明显了,如果 process_request 方法返回一个 request 请求的话,scrapy 会马上将这个 request 对象添加到下载队列(不会进入到下载器),然后继续处理它,由于 process_request 方法又会返回 request 对象,又会重复将 request 对象添加到下载队列,进入到了死循环。

当发生递归错误之后,死循环被打破,scrapy 提示重复调度,丢掉请求,爬虫退出。

解决方法

所以解决方案就是:不要默认返回 request 对象!

如果只是添加请求头,修改代理 IP 等需求,直接返回 None 就可以了,scrapy 会将修改后的 request 对象调度到下载器去下载。

如果需要将一个请求转到另一个请求的话,需要做好判断后返回 request 对象

例如:如果想要将 httpbin.org 的请求转发到 baidu,可以按照以上的代码来请求,可以看到,返回了正确的状态码和百度的 title。

顺便再来说下返回值为 response 对象的适用范围。

如果 process_request 方法返回一个 response 对象的话,说明不需要 scrapy 的下载器来下载响应,直接就可以将 response 对象当作下载好的响应返回。

例如:

可以看到状态码和 body 都是返回的 response 响应里面的内容,而不是 httpbin 网站的 title。

最常见的用途就是 selenium,它使用 selenium 来渲染网页,然后将获取到的源代码封装成 response 对象返回,巧妙地跳过了 scrapy 自带的下载器,因为它不能执行 JS 代码。

总结

通过以上的实验,可以得知 scrapy 下载中间件死循环的原因和解决方法,scrapy 的官方文档只有简单的几句话介绍,并没有更详细的解释,所以在遇到这种解释很简单的方法时,最好自己动手实验一下,可以加深自己对 scrapy 的理解。