irpas技术客

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

大大的周 5536

下载中间件(Downloader Middleware)

ps:下简称DM?

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

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

Engine把(从Schedule获取的)Request发送给Downloader的过程中;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的路上

参数:

request,是Request对象,即此被处理的Requestspider,是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的路上

参数:

request,是Request对象,即此Response对应的Requestresponse,是Response对象,即此被处理的Responsespider,是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()方法抛出异常时。

参数:

request,是Request对象,即产生异常的Requestexception,是Exception对象,即抛出的异常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去请求目标网站,很容易就被检测出来,所以要修改。而修改的方式有两种:

直接修改settings里面的USER_AGENT变量通过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起作用的三个位置:

Response从Downloader到Spider的路上Request从Spider到Engine的路上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对象,即生成该输出的Responseresult:包含Request或Item对象的可迭代对象,即Spider返回的结果spider:Spider对象,即结果对应的Spider对象

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


process_spider_exception(response, exception, spider)

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

参数:

response:Response对象,即异常被抛出时被处理的Responseexception: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?Requestspider: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就会被忽略)


<完>


1.本站遵循行业规范,任何转载的稿件都会明确标注作者和来源;2.本站的原创文章,会注明原创字样,如未注明都非原创,如有侵权请联系删除!;3.作者投稿可能会经我们编辑修改或补充;4.本站不提供任何储存功能只提供收集或者投稿人的网盘链接。

标签: #下载中间件Downloader