Spider Middleware 的使用

Spider Middleware,中文可以翻译为爬虫中间件,但我个人认为英文的叫法更为合适。它是处于 Spider 和 Engine 之间的处理模块。当 Downloader 生成 Response 之后,Response 会被发送给 Spider,在发送给 Spider 之前,Response 会首先经过 Spider Middleware 的处理,当 Spider 处理生成 Item 和 Request 之后,Item 和 Request 还会经过 Spider Middleware 的处理。

Spider Middleware 有如下 3 个作用。

  • Downloader 生成 Response 之后,Engine 会将其发送给 Spider 进行解析,在 Response 发送给 Spider 之前,可以借助 Spider Middleware 对 Response 进行处理。

  • Spider 生成 Request 之后会被发送至 Engine,然后 Request 会被转发到 Scheduler,在 Request 被发送给 Engine 之前,可以借助 Spider Middleware 对 Request 进行处理。

  • Spider 生成 Item 之后会被发送至 Engine,然后 Item 会被转发到 Item Pipeline,在 Item 被发送给 Engine 之前,可以借助 Spider Middleware 对 Item 进行处理。

总的来说,Spider Middleware 可以用来处理输入给 Spider 的 Response 和 Spider 输出的 Item 以及 Request。

使用说明

同样需要说明的是,Scrapy 其实已经提供了许多 Spider Middleware,与 Downloader Middleware 类似,它们被 SPIDER_MIDDLEWARES_BASE 变量所定义。

SPIDER_MIDDLEWARES_BASE 变量的内容如下:

{
    "scrapy.spidermiddlewares.httperror.HttpErrorMiddleware": 50,
    "scrapy.spidermiddlewares.offsite.OffsiteMiddleware": 500,
    "scrapy.spidermiddlewares.referer.RefererMiddleware": 700,
    "scrapy.spidermiddlewares.urllength.UrlLengthMiddleware": 800,
    "scrapy.spidermiddlewares.depth.DepthMiddleware": 900
}

SPIDER_MIDDLEWARES_BASE 里定义的 Spider Middleware 是默认生效的,如果我们要自定义 Spider Middleware,可以和 Downloader Middleware 一样,创建 Spider Middleware 并将其加入 SPIDER_MIDDLEWARES。直接修改这个变量就可以添加自已定义的 Spider MiddleWare,以及禁用 SPIDER_MIDDLEWARES_BASE 里面定义的 Spider Middleware。

这些 Spider Middleware 的调用优先级和 Downloader Middleware 也是类似的,数字越小的 Spider Middleware 是越靠近 Engine 的,数字越大的 Spider Middleware 是越靠近 Spider 的。

核心方法

Scrapy 内置的 Spider Middleware 为 Scrapy 提供了基础的功能。如果我们想要扩展其功能,只需要实现集几个方法。

每个 Spider Middleware 都定义了以下一个或多个方法的类,核心方法有如下 4 个。

  • process_spider_input(response, spider)

  • process_spider_output(response, result, spider)

  • process_spider_exception(response, exception, spider)

  • process_start_requests(start_requests, spider)

只需要实现其中一个方法就可以定义一个 Spider Middleware,下面我们来看看这 4 个方法的详细用法。

process_spider_input(response, spider)

当 Response 通过 Spider Middleware 时,process_spider_input 方法被调用,处理该 Response。它有两个参数。

  • response:Response 对象,即被处理的 Response。

  • spider:Spider 对象,即该 Response 对应的 Spider 对象。

process_spider_input 应该返回 None 或者抛出一个异常。

  • 如果它返回 None,Scrapy 会继续处理该 Response,调用所有其他的 Spider Middleware 直到 Spider 处理该 Response。

  • 如果它抛出一个异常,Scrapy 不会调用任何其他 Spider Middleware 的 process_spider_input 方法,并调用 Request 的 errback 方法。errback 的输出将会以另一个方向被重新输人中间件,使用 process_spider_output 方法来处理,当其抛出异常时则调用 process_spider_exception 来处理。

process_spider_output(response, result, spider)

当 Spider 处理 Response 返回结果时,process_spider_output 方法被调用。它有 3 个参数。

  • response:Response 对象,即生成该输出的 Response。

  • result:包含 Request 或 Item 对象的可送代对象,即 Spider 返回的结果。

  • spider:Spider 对象,即结果对应的 Spider 对象。

process_spider_output 必须返回包含 Request 或 Item 对象的可迭代对象。

process_spider_exception(response, exception, spider)

当 Spider 或 Spider Middleware 的 process_spider_input 方法抛出异常时,process_spider_exception 方法被调用。它有 3 个参数。

  • response:Response 对象,即异常被抛出时被处理的 Response。

  • exception:Exception 对象,被抛出的异常。

  • spider:Spider 对象,即抛出该异常的 Spider 对象。

process_spider_exception 必须返回 None 或者一个(包含 Response 或 Item 对象的)可选代对象。

  • 如果它返回 None,那么 Scrapy 将继续处理该异常,调用其他 Spider Middleware 中的 process_spider_exception 方法,直到所有 Spider Middleware 都被调用。

  • 如果它返回一个可选代对象,则其他 Spider Middleware 的 process_spider_output 方法被调用,其他的 process_spider_exception 不会被调用。

process_start_requests(start_requests, spider)

process_start_requests 方法以 Spider 启动的 Request 为参数被调用,执行的过程类似于 process_spider_output,只不过它没有相关联的 Response 并且必须返回 Request。它有两个参数。

  • start_requests:包含 Request 的可迭代对象,即 Start Requests。

  • spider:Spider 对象,即 Start Requests 所属的 Spider。

process_start_requests 方法必须返回另一个包含 Request 对象的可迭代对象。

实战

上面的内容理解起来还是有点抽象,下面我们结合一个实战项目来加深一下对 Spider Middleware 的认识。

首先我们新建一个 Scrapy 项目叫作 scrapyspidermiddlewaredemo,命令如下所示:

scrapy startproject scrapyspidermiddlewaredemo

然后进人项目,新建一个 Spider。我们还是以 https://www.httpbin.org/ 为例来进行演示,命令如下所示:

scrapy genspider httpbin www.httpbin.org

命令执行完毕后,新建了一个名为 httpbin 的 Spider。接下来我们修改 start_url 为 https://www.httpbin.org/get,然后自定义 start_requests 方法,构造几个 Request,回调方法还是定义为 parse 方法。随后将 parse 方法添加一行打印输出,将 response 变量的 text 属性输出,这样我们便可以看到 Scrapy 发送的 Request 信息了。

修改 Spider 内容如下所示:

from scrapy import Spider, Request


class HttpbinSpider(Spider):
    name = 'httpbin'
    allowed_domains = ['httpbin.org']
    start_url = 'https://httpbin.org/get'

    def start_requests(self):
        for i in range(5):
            url = f'{self.start_url}?query={i}'
            yield Request(url, callback=self.parse)

    def parse(self, response):
        print('Status', response.text)

接下来运行此 Spider,执行如下命令:

scrapy crawl httpbin

Scrapy 运行结果包含 Scrapy 发送的 Request 信息,内容如下所示:

{
  "args": {
    "query": "0"
  },
  "headers": {
    "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
    "Accept-Encoding": "gzip, deflate",
    "Accept-Language": "en",
    "Host": "httpbin.org",
    "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_5_1) AppleWebKit/533.1 (KHTML, like Gecko) Chrome/15.0.818.0 Safari/533.1",
    "X-Amzn-Trace-Id": "Root=1-65b9196e-6e206d1e711257466bb7bf89"
  },
  "origin": "27.38.238.130",
  "url": "https://httpbin.org/get?query=0"
}

这里我们可以看到几个 Request 对应的 Response 的内容就被输出了,每个返回结果带有 args 参数,query 为 0~4。

另外我们可以定义一个 Item,4 个字段就是目标站点返回的字段,相关代码如下:

import scrapy


class DemoItem(scrapy.Item):
    origin = scrapy.Field()
    headers = scrapy.Field()
    args = scrapy.Field()
    url = scrapy.Field()

可以在 parse 方法中将返回的 Response 的内容转化为 DemoItem,将 parse 方法做如下修改:

def parse(self, response):
    item = DemoItem(**response.json())
    yield item

这样重新运行,最终 Spider 就会产生对应的 DemoItem 了,运行效果如下:

{
  "args": {
    "query": "4"
  },
  "headers": {
    "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
    "Accept-Encoding": "gzip, deflate",
    "Accept-Language": "en",
    "Host": "httpbin.org",
    "User-Agent": "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_1) AppleWebKit/534.1 (KHTML, like Gecko) Chrome/60.0.840.0 Safari/534.1",
    "X-Amzn-Trace-Id": "Root=1-65b91b61-46f54a7e73ddc0de6cae8b97"
  },
  "origin": "27.38.238.130",
  "url": "https://httpbin.org/get?query=4"
}

可以看到原本 Response 的 JSON 数据就被转化为了 DemoItem 并返回。

接下来我们实现一个 Spider Middleware,看看如何实现 Response、Item、Request 的处理吧!

在 middlewares.py 中重新声明一个 CustomizeMiddleware 类,内容如下:

class CustomizeMiddleware(object):

    def process_start_requests(self, start_requests, spider):
        for request in start_requests:
            url = request.url
            url += '&name=germey'
            request = request.replace(url=url)
            yield request

这里实现了 process_start_requests 方法,它可以对 start_requests 表示的每个 Request 进行处理,我们首先获取了每个 Request 的 URL,然后在 URL 的后面又拼接上了另外一个 Query 参数,name 等于 germey,然后我们利用 request 的 replace 方法将 url 属性替换,这样就成功为 Request 赋值了新的 URL。

接着我们需要将此 CustomizeMiddleware 开启,在 settings.py 中进行如下的定义:

SPIDER_MIDDLEWARES = {
    'scrapyspidermiddlewaredemo.middlewares.CustomizeMiddleware': 543,
}

这样我们就开启了 CustomizeMiddleware 这个 Spider Middleware 。

重新运行 Spider,这时候我们可以看到输出结果就变成了类似下面这样的结果:

https://www.httpbin.org/get?query=2&name=germey
{
  "args": {'name': 'germey', 'query': '2'},
  "headers": {
    "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
    "Accept-Encoding": "gzip, deflate",
    "Accept-Language": "en",
    "Host": "httpbin.org",
    "User-Agent": "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_1) AppleWebKit/534.1 (KHTML, like Gecko) Chrome/60.0.840.0 Safari/534.1",
    "X-Amzn-Trace-Id": "Root=1-65b91b61-46f54a7e73ddc0de6cae8b97"
  },
  "origin": "27.38.238.130",
  "url": "https://httpbin.org/get?query=2&name=germey"
}

可以观察到 url 属性成功添加了 name=germey 的内容,这说明我们利用 Spider Middleware 成功改写了 Request。

除了改写 start_requests,我们还可以对 Response 和 Item 进行改写,比如对 Response 进行改写,我们可以尝试更改其状态码,在 CustomizeMiddleware 里面增加如下定义:

def process_spider_input(self, response, spider):
    response.status = 201

def process_spider_output(self, response, result, spider):
    for i in result:
        if isinstance(i, DemoItem):
            i['origin'] = None
            yield i

这里我们定义了 process_spider_input 和 process_spider_output 方法,分别来处理 Spider 的输入和输出。对于 process_spider_input 方法来说,输入自然就是 Response 对象,所以第一个参数就是 response,我们在这里直接修改了状态码。对于 process_spider_output 方法来说,输出就是 Request 或 Item 了,但是这里二者是混合在一起的,作为 result 参数传递过来。result 是一个可迭代的对象,我们遍历了 result,然后判断了每个元素的类型,在这里使用 isinstance 方法进行判定:如果 i 是 DemoItem 类型,就把它的 origin 属性设置为空。当然这里还可以针对 Request 类型做类似的处理,此处略去。

另外在 parse 方法里面添加 Response 状态码的输出结果:

print('Status', response.status)

重新运行一下 Spider 可以看到输出结果类下面这样:

Status 201
2024-01-31 00:19:43 [scrapy.core.scraper] DEBUG: Scraped from <201 https://httpbin.org/get?query=1&name=germey>
{'args': {'name': 'germey', 'query': '1'},
 'headers': {'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
             'Accept-Encoding': 'gzip, deflate',
             'Accept-Language': 'en',
             'Host': 'httpbin.org',
             'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 4_2_1 like Mac '
                           'OS X) AppleWebKit/535.2 (KHTML, like Gecko) '
                           'CriOS/53.0.800.0 Mobile/59O019 Safari/535.2',
             'X-Amzn-Trace-Id': 'Root=1-65b9219e-4bde88046a170b8876c15b02'},
 'origin': None,
 'url': 'https://httpbin.org/get?query=1&name=germey'}

状态码变成了 201,Item 的 origin 字段变成了 None,证明 CustomizeMiddleware 对 Spider 输人的 Response 和输出的 Item 都实现了处理。

到这里,我们通过自定义 Spider Middleware 的方式,实现了对 Spider 输人的 Response 以及输出的 Request 和 Item 的处理。

另外在 Scrapy 中,还有几个内置的 Spider Middleware,我们简单介绍一下。

HttpErrorMiddleware

HttpErrorMiddleware 的主要作用是过滤我们需要忽略的 Response,比如状态码为 200~299 的会处理,500 以上的不会处理。其核心实现代码如下:

def __init__(self, settings):
    self.handle_httpstatus_all = settings.getbool("HTTPERROR_ALLOW_ALL")
    self.handle_httpstatus_list = settings.getlist("HTTPERROR_ALLOWED_CODES")

def process_spider_input(self, response, spider):
    if 200 <= response.status < 300:  # common case
        return
    meta = response.meta
    if meta.get("handle_httpstatus_all", False):
        return
    if "handle_httpstatus_list" in meta:
        allowed_statuses = meta["handle_httpstatus_list"]
    elif self.handle_httpstatus_all:
        return
    else:
        allowed_statuses = getattr(
            spider, "handle_httpstatus_list", self.handle_httpstatus_list
        )
    if response.status in allowed_statuses:
        return
    raise HttpError(response, "Ignoring non-200 response")

可以看到它实现了 process_spider_input 方法,然后判断了状态码为 200~299 就直接返回,否则会根据 handle_httpstatus_all 和 handle_httpstatus_list 来进行处理。例如状态码在 handle_httpstatus_list 定义的范围内,就会直接处理,否则抛出 HttpError 异常。这也解释了为什么刚才我们把 Response 的状态码修改为 201 却依然能被正常处理的原因,如果我们修改为非 200~299 的状态码,就会抛出异常了。

另外,如果想要针对一些错误类型的状态码进行处理,可以修改 Spider 的 handle_httpstatus_list 属性,也可以修改 Request meta 的 handle_httpstatus_list 属性,还可以修改全局 setttings HTTPERROR_ALLOWED_CODES。

比如我们想要处理 404 状态码,可以进行如下设置:

HTTPERROR_ALLOWED_CODES = [404]

OffsiteMiddleware

OffsiteMiddleware 的主要作用是过滤不符合 allowed_domains 的 Request,Spider 里面定义的 allowed_domains 其实就是在这个 Spider Middleware 里生效的。其核心代码实现如下:

def process_spider_output(self, response, result, spider):
    for x in result:
        if isinstance(x, Request):
            if x.dont_filter or self.should_follow(x, spider):
                yield x
            else:
                domain = urlparse_cached(x).hostname
                if domain and domain not in self.domains_seen:
                    self.domains_seen.add(domain)
                    logger.debug(
                        "Filtered offsite request to %(domain)r: %(request)s",
                        {"domain": domain, "request": request},
                        extra={"spider": spider},
                    )
                    self.stats.inc_value("offsite/domains", spider=spider)
                self.stats.inc_value("offsite/filtered", spider=spider)
        else:
            yield x

可以看到,这里首先遍历了 result,然后判断了 Request 类型的元素并赋值为 x。然后根据 x 的 dontfilter、url 和 Spider 的 allowed_domains 进行了过滤,如果不符合 allowed_domains,就直接输出日志并不再返回 Request,只有符合要求的 Request 才会被返回并继续调用

UrlLengthMiddleware

UrlLengthMiddleware 的主要作用是根据 Request 的 URL 长度对 Request 进行过滤:如果 URL 的长度过长,此 Request 就会被忽略。其核心代码实现如下:

@classmethod
def from_settings(cls,settings):
    maxlength = settings.getint('URLLENGTH_LIMIT')

def process_spider_output(self, response, result, spider):
    def _filter(request):
        if isinstance(request, Request) and len(request.url) > self.maxlength:
            logger:debug("Ignoring link (url length >%(maxlength)d): %(url)s",
            {'maxlength': self.maxlength,'url':request.url},
            extra={'spider':spider})
            return False
        else:
            return True

    return(r for r in result or () if _filter(r))

可以看到,这里利用了 process_spider_output 对 result 里面的 Request 进行过滤,如果是 Request 类型并且 URL 长度超过最大限制,就会被过滤。我们可以从中了解到,如果想要根据 URL 的长度进行过滤,可以设置 URLLENGTH_LIMIT。

比如我们只想爬取 URL 长度小于 50 的页面,那么就可以进行如下设置:

URLLENGTH_LIMIT = 50

可见 Spider Middleware 能够非常灵活地对 Spider 的输人和输出进行处理,内置的一些 Spider Middleware 在某些场景下也发挥了重要作用。另外,还有一些其他的内置 Spider Middleware,就不在此一一赞述了,更多内容可以参考官方文档: https://docs.scrapy.org/en/latest/topics/spider-middleware.html#built-in-spider-middleware-reference

总结

本节介绍了 Spider Middleware 的基本原理和自定义 Spider Middleware 的方法,在必要的情况下,我们可以利用它来对 Spider 的输入和输出进行处理,在某些场景下还是很有用的。