课程笔记4:Scrapy框架——下载中间件&爬虫中间件的用法

下载中间件(Downloader Middleware)

ps:下简称DM 

这是处于Scrapy的Request和Response之间的处理模块。

DM在整个架构中起作用的两个位置:

  1. Engine把(从Schedule获取的)Request发送给Downloader的过程中;
  2. Downloader把Response发送给Engine的过程中(之后Engine会转发给Spider)。

常用功能:修改User-Agent、处理重定向、设置代理、失败重试、设置Cookies等。


使用说明

Scrapy内置的DM被DOWNLOADER_MIDDLEWARES_BASE变量所定义(默认为开启状态):

{
    'scrapy.downloadermiddlewares.robotstxt.RobotsTxtMiddleware': 100,
    'scrapy.downloadermiddlewares.httpauth.HttpAuthMiddleware': 300,
    'scrapy.downloadermiddlewares.downloadtimeout.DownloadTimeoutMiddleware': 350,
    'scrapy.downloadermiddlewares.defaultheaders.DefaultHeadersMiddleware': 400,
    'scrapy.downloadermiddlewares.useragent.UserAgentMiddleware': 500,
    'scrapy.downloadermiddlewares.retry.RetryMiddleware': 550,
    'scrapy.downloadermiddlewares.ajaxcrawl.AjaxCrawlMiddleware': 560,
    'scrapy.downloadermiddlewares.redirect.MetaRefreshMiddleware': 580,
    'scrapy.downloadermiddlewares.httpcompression.HttpCompressionMiddleware': 590,
    'scrapy.downloadermiddlewares.redirect.RedirectMiddleware': 600,
    'scrapy.downloadermiddlewares.cookies.CookiesMiddleware': 700,
    'scrapy.downloadermiddlewares.httpproxy.HttpProxyMiddleware': 750,
    'scrapy.downloadermiddlewares.stats.DownloaderStats': 850,
    'scrapy.downloadermiddlewares.httpcache.HttpCacheMiddleware': 900,
}

这是一个字典格式,字典的键名就是每个内置DM的名字,键值代表的是调用优先级——

数字越小代表越靠近Engine,数字越大代表越靠近Downloader。

每个DM都可以通过定义process_request()和process_response()方法来分别处理Request和Response。

  • 对于process_request()来说,优先级数字越小越先被调用;
  • 对于process_response()来说,优先级数字越大越先被调用。

DOWNLOADER_MIDDLEWARES_BASE变量是不能直接修改的!Scrapy还提供了DOWNLOADER_MIDDLEWARES变量,无论是想要添加自定义DM还是要禁用DOWNLOADER_MIDDLEWARES _BASE里的内置DM,都可以通过直接修改DOWNLOADER_MIDDLEWARES实现(在settings.py里面)。 

添加并启用自定义DM:

禁用内置DM(键值改为None):

DOWNLOADER_MIDDLEWARES = {
    'myproject.middlewares.CustomDownloaderMiddleware': 543,
    'scrapy.downloadermiddlewares.useragent.UserAgentMiddleware': None,
}

核心方法

DM的核心方法有3种(至少实现1个,就可以定义一个DM):

  • process_request(request, spider)
  • process_response(request, response, spider)
  • process_exception(request, exception, spider)

process_request(request, spider)

生效时机:Request从Scheduler到Downloader的路上

参数:

  1. request,是Request对象,即此被处理的Request
  2. spider,是Spider对象,即此Request对应的Spider

返回值:None/Response对象/Request对象/抛出IgnoreRequest异常

不同返回值的效果:

None——接着执行其他DM的process_request()方法,直接Downloader把Request执行后得到Response才结束。

Response对象——更低优先级的DM的process_request()和process_exception()方法不会被继续调用,每个DM的process_response()方法转而被依次调用。调用完毕之后,直接将Response对象发送给Spider处理。

Request对象——更低优先级的DM的process_request()方法不会被继续调用,返回的Request会被重新放到调度队列中(成为一个新的Request,等待被调度)。

IgnoreRequest异常抛出——所有DM的process_exception()方法会依次执行。如果没有一个方法处理这个异常,那么Request的errorback()方法就会回调。如果该异常还是没有被处理,那么它便会被忽略。


process_response(request, response, spider)

生效时机:Response从Downloader到Spider的路上

参数:

  1. request,是Request对象,即此Response对应的Request
  2. response,是Response对象,即此被处理的Response
  3. spider,是Spider对象,即次Response对应的Spider

返回值:Request对象/Response对象/抛出IgnoreRequest异常

不同返回值的效果:

Request对象——更低优先级的DM的process_response()方法不会被继续调用,返回的Request会被重新放到调度队列中(成为一个新的Request,等待被调度)。

Response对象——更低优先级的DM的process_response()方法会继续调用,继续对该Response对象进行处理。

IgnoreRequest异常抛出——Request的errorback()方法会回调。如果该异常还没有被处理,那么它便会被忽略。


process_exception(request, exception, spider)

生效时机:当Downloader或process_request()方法抛出异常时。

参数:

  1. request,是Request对象,即产生异常的Request
  2. exception,是Exception对象,即抛出的异常
  3. spider,是Spider对象,即Request对应的Request

返回值:None/Response对象/Request对象

不同返回值的效果:

None——更低优先级的DM的process_exception()方法会被继续依次调用,直到所有方法都被调用完。

Response对象——更低优先级的DM的process_exception()不会再被继续调用,每个DM的process_response()方法转而被依次调用。

Request对象——更低优先级的DM的process_exception()不再被继续调用,返回的Request会被重新放到调度队列中(成为一个新的Request,等待被调度)。



归纳示意图


实战

修改Scrapy发送的(Request使用的headers里面的)User-Agent。

在默认情况下,User-Agent由Scrapy内置的UserAgentMiddleware设置。UserAgentMiddleware在DOWNLOADER_MIDDLEWARES_BASE中的配置如下:

{
    'scrapy.downloadermiddlewares.useragent.UserAgentMiddleware': 500,
}

如果按照这个默认的User-Agent去请求目标网站,很容易就被检测出来,所以要修改。而修改的方式有两种:

  1. 直接修改settings里面的USER_AGENT变量
  2. 通过Downloader Middleware的process_request方法来修改

第一种方式非常简单,只需要在settings.py里面加上一行对USER_AGENT的定义即可:

USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/97.0.4692.71 Safari/537.36'

第二种方式更加灵活,需要在middlewares.py里面添加一个RandomUserAgentMiddleware类:

import random

class RandomUserAgentMiddleware:
    def __init__(self):
        self.user_agents = [
            'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/97.0.4692.71 Safari/537.36 Edg/97.0.1072.55',
            'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/97.0.4692.71 Safari/537.36',
            'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.163 Safari/537.36'
        ]
    def process_request(self,request, spider):
        request.headers['User-Agent'] = random.choice(self.user_agents)

当然了,还得去setting.py中,将DOWNLOADER_MIDDLEWARES取消注释(启用)。

再次运行Spider(Scrapy crawl httpbin),User-Agent已经可以从列表中随机选出了。 

上述技巧稍加修改,可以设置代理甚至是设置随机代理。

class ProxyMiddleware:
    def process_request(self, request, spider):
        request.meta['proxy'] = 'http://203.184.132.103:7890'

当然了,依然还是得去setting.py中,添加进DOWNLOADER_MIDDLEWARES以启用。


补充1:process_request()不同返回值的效果

前面利用两个Downloader Middleware中的process_request()对Request进行了修改,它们两个都没有返回值,即返回值为None,这样一个个Downloader Middleware的process_request()才能被顺利依次执行了。

如果返回值为Request,修改代码如下:

class ProxyMiddleware:
    def process_request(self, request, spider):
        request.meta['proxy'] = 'http://203.184.132.103:7890'
        return request

结果就是后续DM中的process_request()不会被执行,同时,这个返回的Request会直接发送给Engine并加回到Schedule,等待下一次被调度。

由于现在只发起了一个Request,所以这个Request会被不断从Scheduler取出来放回去,形成无限循环。系统会得到一个递归错误的报错信息:

 如果返回值为Response,修改代码如下:

from scrapy.http import HtmlResponse

class ProxyMiddleware:
    def process_request(self,request, spider):
        return HtmlResponse(
            url = request.url,
            status= 200,
            encoding= 'utf-8',
            body= 'Test Downloader Middleware'.center(40,'*')
        )

在parse()方法中添加Response内容的输出结果:

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

结果就是更低优先级的DM中的process_request()和process_exception()方法不会被继续调用,每个DM中的process_response方法转而被依次调用。调用完毕后,直接将Response对象发送给Spider来处理(不会再经过Downloader执行下载了)。

Spider中定义的parse里面的print方法最终输出:

原本Request应该去请求http://www.httpbin.org/get得到结果的,但我们刚才定义HtmlResponse成为了最终传递给Spider解析的Response(一般情况下,Response是由Downloader对Request执行下载后得到的)。


补充2:process_response()不同返回值的效果

如果返回值为Response,修改代码如下:

在Middlewares中

class ChangeResponseMiddleware:
    def process_response(self,request,response,spider):
        response.status = 201
        return response

在Spider中

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

在Settings中

DOWNLOADER_MIDDLEWARES = {
   'scrapydownloadermiddlewaredemo.middlewares.RandomUserAgentMiddleware': 543,
   'scrapydownloadermiddlewaredemo.middlewares.ChangeResponseMiddleware': 544,
}

输出结果就是:

 Response的状态码被成功修改了。




爬虫中间件(Spider Downloader)

这是处于Spider和Engine之间的处理模块。

ps:下简称SM。

SM起作用的三个位置:

  1. Response从Downloader到Spider的路上
  2. Request从Spider到Engine的路上
  3. Item从Spider到Engine的路上

使用说明:

Scrapy内置的SM被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 Middleware同样需要添加进SPIDER_MIDDLEWARES变量,在这个变量中还可以禁用SPIDER_MIDDLEWARES_BASE变量中的内置SM。

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

核心方法

SM的核心方法有三种(至少实现一种就能定义一个SM):

  • process_spider_input(response, spider)
  • process_spider_output(response, result, spider)
  • process_spider_exception(response, exception, spider)
  • process_start_requests(start_requests, spider)

process_spider_input(response, spider)

生效时机:当Response通过SM时调用,处理该Response。

参数:

  • response:Response对象,即被处理的Response。
  • spider:Spider对象,即该Response对应的Spider对象。

返回值:None/抛出异常

不同返回值的效果:

  • None——继续处理该Response,调用所有其他的SM直到该Response到达Spider
  • 抛出异常——不再调用其他SM的process_spider_input()方法,转而调用Request的errback()方法(errback()的输出会以另一个方向被重新输入中间件,并使用process_spider_output()方法处理,当再次抛出异常时调用process_spider_exception()来处理。)

process_spider_output(response, result, spider)

生效时机:当Spider返回Request或者Item时调用。

参数:

  • response:Response对象,即生成该输出的Response
  • result:包含Request或Item对象的可迭代对象,即Spider返回的结果
  • spider:Spider对象,即结果对应的Spider对象

返回值:必须返回包含Request或Item对象的可迭代对象


process_spider_exception(response, exception, spider)

生效时机:当Spider或者SM的process_spider_input()方法抛出异常时被调用

参数:

  • response:Response对象,即异常被抛出时被处理的Response
  • exception:Exception对象,即抛出的异常
  • spider:Spider对象,即抛出该异常的Spider对象

返回值:必须返回None或者一个(包含Response或Item对象的)可迭代对象

不同返回值的效果:

  • None——继续处理该异常,调用其他SM中的process_spider_exception()方法,直到所有SM都被调用
  • 可迭代对象——不再调用其他SM的process_spider_exception(),转而调用其他SM的process_spider_output()方法

process_start_requests(start_requests, spider)

生效时机:以Spider启动的Request为参数被调用(执行过程类似于process_spider_output(),不过没有先关联的Response并且必须返回Request)

参数:

  • start_requests:包含Request的可迭代对象,即Start Request
  • spider:Spider对象,即Start Request所属的Spider

返回值:必须返回另一个包含Request对象的可迭代对象



归纳示意图



实战:

新建项目:scrapy startproject scrapyspidermiddlewaredemo

新建Spider:scrapy genspider httpbin www.httpbin.org

修改代码如下:

import scrapy
from ..items import DemoItem

class HttpbinSpider(scrapy.Spider):
    name = 'httpbin'
    allowed_domains = ['www.httpbin.org']
    start_urls = 'http://www.httpbin.org/get'

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

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

通过自定义start_requests方法,构造了五个Request,回调方法还是定义为parse方法。在parse方法下,将response变量的text属性打印输出,以便查看Scrapy发送的Request信息。

运行后可以看到:每个返回结果都带有args参数,query为0~4。

在Items中定义一个Item,其中定义4个字段(对应目标站点返回的4个字段):

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

在Spider中修改parse方法,将返回的Response内容转化为DemoItem:

def parse(self, response):
    item = DemoItem(**response.json())
    # **表示打包包含任意数量键值对的参数
    yield item

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


接下来通过定义一个SM(process_start_requests方法),实现对Request的处理

在middlewares.py添加代码:

class CustomizeMiddleware:

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

这里实现了process_start_requests()方法:从start_requests获取每一个request的URL,然后在每一个URL后面拼一个Query参数(name=germey),然后利用request的replace方法将url属性替换,最终实现给Request赋值一个新的URL。

接着需要在settings.py中开启这个SM:

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

重新运行可以看到:url属性成功添加上了name=germey的内容,在args参数里面也有相应的内容。


接下来通过定义一个SM(process_spider_input和process_spider_output方法),分别来处理Spider的输入(response)和输出(Item)

在CustomizeMiddleware增加代码如下:

from .items import DemoItem

class 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

在parse()方法中添加Response状态码的输出结果:

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

对于process_spider_input()方法,输入的自然是Response,这里设定了状态码的修改。

对于process_spider_output()方法,输出的自然是Request或者Item(但这里二者是混合在一起的,作为result参数传递过来)。通过遍历result(可迭代对象),再判断每个元素的类型(这里用的是isinstance()方法):如果i是DemoItem类型,就将它的origin设置为空(这里也可以对Request类型做类似处理)。

重新运行,结果就是状态码变成了201,Item的origin字段变成了None。


补充:三个内置的SM
HttpErrorMiddleware:过滤我们需要忽略的Response(比如状态码为200~299的会处理,500以上的不会处理)

OffsiteMiddleware:过滤不符合allowed_domains的Request(Spider里面定义的allowed_domains其实就是在这个SM里生效的)

UrlLengthMiddleware:根据Request的URL长度对Request进行过滤(如果URL长度过长,此Request就会被忽略)


<完>

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值