Scrapy爬虫框架4-中间件

108 阅读10分钟

meta的深浅拷贝

当我们获取数据的时候,会创建⼀个存储对象(例如item)。如果不⽤涉及到传参,那么这个存储对象放置的位置是没有讲究的。

普通情况

无论是放置在循环中或者循环外,使用以下两种方法都是可以的:

for i in datas:
     data = {'data': i}
     yield data
data = {}
for i in datas:
    data['data'] = i
     yield data

复杂情况

当我们的的存储对象需要从⼀个请求传递到另外⼀个请求时,这种请求就复杂得多,以上两种办法可能要进⾏取舍。

情况1

for i in datas:
    data = {'data': i}
    yield scrapy.Request(url=detail_url,
                         callback=self.detail_parse,
                         meta={'item': data})
for i in datas:
    data = TencentItem()
    data['data']= i
    yield scrapy.Request(url=detail_url,
                         callback=self.detail_parse,
                         meta={'item': data}) 

当使⽤这种⽅式的时候,传递到另外⼀个请求是没有问题的,因为每次存储的存储对象,都是创建出来的新对象,对象之间不会影响到,所以不会影响传输的数据。

情况2

data = {}
for i in datas:
    data['data'] = i
    yield scrapy.Request(url=detail_url,
                         callback=self.detail_parse,
                         meta={'item': data})
data = TencentItem()
for i in datas:
    data['data'] = i
    yield scrapy.Request(url=detail_url,
                         callback=self.detail_parse,
                         meta={'item': data})

当使⽤上⾯这种情况时,数据就会出现问题,因为每次使⽤的数据是同⼀个对象,那么在新的详情解析异步代码中,进⾏参数赋值会影响到其他的详情解析,归根到底是使⽤了同⼀个实例对象。

解决办法:

解决办法就是将实例化的对象拷⻉⼀份,拷⻉的⽅法有深浅两种拷⻉,分别是copy模块中的copy和deepcopy。

copy浅拷贝

copy是浅拷贝,它是复制一个对象指向原本的数据,同时会拷贝一份数据的表层数据。内层数据是放在同一个内存地址的,所以当修改内存数据的时候,原数据和拷贝的数据都会发生改变。

from copy import copy
data = [1, [2, 3, 4]]
copy_data = copy(data)
copy_data[0] = 2
copy_data[1][0] = 1
print(data)
print(copy_data)

结果:

image.png

从以上结果可以看出,当外层数据被修改的时候,不会影响到原本的数据,当修改里面内部数据的时候就会影响到原本数据,这是因为copy浅拷贝,只会拷贝最外层的数据。

总结: 对于列表,字典这种可变的数据类型,拷⻉出来的对象修改数据依然会影响到原本的数据,因为他们 指定的数据是同⼀个数据,所以修改和获取也是同⼀个数据。

deepcopy深拷贝

deepcopy拷⻉会将深层的数据也进⾏拷⻉,这样拷⻉会将数据完全的复制⼀份出来,这样就不会影响到原本的数据了。

from copy import deepcopy
data = [1, [2, 3, 4]]
copy_data = deepcopy(data)
copy_data[0] = 2
copy_data[1][0] = 1
print(data)
print(copy_data)

结果:

image.png

总结:通过以上结果看出,深拷贝出来的数据和原数据是两份一样的数据,并且存储的地址也不一样,所以对拷贝数据进行修改的时候,对原数据是没有任何影响的。

综上所述,得出解决问题的办法:

from copy import deepcopy
data = TencentItem()
for i in datas:
    data['data']= i
    yield scrapy.Request(url=detail_url,
                        callback=self.detail_parse,
                        meta={'item': deepcopy(data)})

Scrapy中间件

下载中间件,可以处理请求之前和请求之后的数据。 下载中间件存在在middlewares.py⽂件中DownloaderMiddleware类如果想要使⽤,需要在setting.py中启动下载中间件。运⾏

DOWNLOADER_MIDDLEWARES = {
    "xxx.middlewares.TencentDownloaderMiddleware": 543,
}

middlewares.py的中间件类:

@classmethod
def from_crawler(cls, crawler):
    # This method is used by Scrapy to create your spiders.
    s = cls()
    crawler.signals.connect(s.spider_opened, signal=signals.spider_opened)
    return s

这个代码是⼀个类⽅法,⽤于整个爬⾍中间件的创建。

如果存在,则调⽤from_crawler类⽅法创建中间件实例。内部代码通过crawler.signals.connect ⽅法连接了 spider_opened 信号和 s.spider_opened ⽅法,这样在 spider_opened 信号触发时就会执⾏ s.spider_opened ⽅法,也就是爬⾍运⾏中间件的⽅法会依次执⾏中间件的 spider_opened ⽅法。

def spider_opened(self, spider):
    spider.logger.info("Spider opened: %s" % spider.name)

spider_opened 被scrapy的信号关联,当爬⾍启动的时候,⾃动调⽤中间件的spider_opened⽅法。此⽅法中使⽤了⽇志的输出,输出 "Spider opened: %s" % spider.name 爬⾍的名称,表⽰爬⾍已经启动。

既然他是初始化调⽤的地⽅,那么它通常⽤于在爬⾍被打开时执⾏⼀些初始化操作,必要的准备⼯作,或者记录⼀些相关的⽇志信息。

def process_request(self, request, spider):
    # Called for each request that goes through the downloader
    # middleware.

    # Must either:
    # - return None: continue processing this request
    # - or return a Response object
    # - or return a Request object
    # - or raise IgnoreRequest: process_exception() methods of
    #   installed downloader middleware will be called
    return None
    
#
def process_request(self, request, spider):
    # 对于通过下载的每个请求,都会调⽤此⽅法
    
    # 必须知道的:
    # - return None:继续处理此请求
    # - 或者返回⼀个 Response 对象
    # - 或者返回⼀个 Request 对象
    # - 或者 raise IgnoreRequest: process_exception() ⽅法
return None

对于通过下载的每个请求,都会调⽤此⽅法。

1. 返回 None :继续处理该请求,将它传递给后续的下载中间件或下载器进⾏处理。

2. 返回 Response 对象:表⽰该请求已被成功处理,并返回⼀个包含响应数据的 Response对象。这样,后续的下载中间件和爬⾍就会跳过该请求,直接获取响应数据进⾏处理。

3. 返回 Request 对象:表⽰该请求需要重新发送⼀个新的请求。这个新的请求将会被发送到下载 器,并经过后续的下载中间件的处理。这在需要重定向、重新发送请求、实现请求的动态⽣成等情况下使⽤⽤。

4. 抛出 IgnoreRequest 异常:表⽰该请求应该被忽略,不再进⾏后续的处理。下载中间件的process_exception ⽅法将会被调⽤来处理这个异常。

process_request ⽅法通常⽤于对将要发送的请求进⾏预处理或拦截,你可以对请求进⾏各种操作,例如修改请求的 headers、添加请求参数、对请求进⾏过滤或验证等。然后根据处理结果选择返回不同的对象或继续处理该请求。

def process_exception(self, request, exception, spider):
    # Called when a download handler or a process_request()
    # (from other downloader middleware) raises an exception.

    # Must either:
    # - return None: continue processing this exception
    # - return a Response object: stops process_exception() chain
    # - return a Request object: stops process_exception() chain
    pass

在请求过程中出现错误会⾃动调⽤该⽅法。

• 返回 None:表⽰继续处理该异常,可以继续执⾏后续的下载中间件的 process_exception⽅法。

• 返回 Response 对象:表⽰停⽌异常处理链,将会直接交给爬⾍进⾏处理,相当于正常情况下的返回 Response 对象。

• 返回 Request 对象:表⽰停⽌异常处理链,并重新发送⼀个新的请求。

process_exception 这个⽅法通常被⽤于处理下载过程中出现的异常,⽐如⽹络连接错误、超时、服务器错误等。通过在 process_exception ⽅法中进⾏处理,你可以根据异常的具体情况来决定如何处理异常,也可以动态地修改请求、重新发送请求等。

def process_response(self, request, response, spider):
    # Called with the response returned from the downloader.

    # Must either;
    # - return a Response object
    # - return a Request object
    # - or raise IgnoreRequest
    return response

• 返回⼀个 Response 对象:表⽰对该响应进⾏处理后返回⼀个新的响应。这样,后续的下载中间件和爬⾍就会使⽤这个新的响应进⾏后续处理。

• 返回⼀个 Request 对象:表⽰对该响应进⾏处理后返回⼀个新的请求对象。这个新的请求对象将会被发送到下载器,并经过后续的下载中间件的处理。

• 抛出 IgnoreRequest 异常:表⽰该响应应该被忽略,不再进⾏后续的处理。

这个⽅法的作⽤是对下载得到的响应进⾏处理。你可以选择返回⼀个新的 Response 对象,返回⼀个新的 Request 对象,或者抛出 IgnoreRequest 异常来终⽌该响应的处理。

总结:

  1. process_request(self, request, spider) : 这个⽅法会在每个请求发送之前被调⽤,⽤于对请求进⾏预处理或拦截。你可以在这个⽅法中修改请求的 headers、添加请求参数、对请求进⾏过滤或验证等。根据处理结果,可以返回 None 继续处理该请求,返回⼀个 Response 对象表⽰请求已被处理完毕,返回⼀个新的 Request 对象表⽰需要重新发送请求,或者抛出

IgnoreRequest 异常表⽰忽略该请求。

  1. process_response(self, request, response, spider) : 这个⽅法会在下载器返回响应后被调⽤,⽤于对响应进⾏处理。你可以在这个⽅法中对响应进⾏改动或过滤,并返回⼀个新的 Response 对象⽤于后续处理,或返回⼀个新的 Request 对象表⽰需要重新发送请求,或抛出IgnoreRequest 异常来忽略该响应。

  2. process_exception(self, request, exception, spider) : 这个⽅法会在下载过程中出现异常时被调⽤,⽤于处理下载过程中的异常情况。你可以在这个⽅法中根据异常的具体情况进⾏处理,例如重新发送请求、修改请求参数等。可以选择返回 None 继续处理该异常,返回⼀Response 对象⽤于后续处理,返回⼀个新的 Request 对象表⽰需要重新发送请求,或抛出IgnoreRequest 异常来忽略该异常。

演示

class CustomMiddleware:
    def process_request(self, request, spider):
        # 在请求头中添加⾃定义信息
        request.headers['User-Agent'] = 'MyCustomUserAgent'
        request.meta['custom_info'] = 'SomeCustomInfo'
        # # 可以修改请求的URL、Headers、添加代理等
        # 继续处理该请求
        return None
    def process_response(self, request, response, spider):
        # 对响应进⾏加⼯处理
        modified_body = response.body.upper() # 将响应数据转换为⼤写
        return response.replace(body=modified_body)
        def process_exception(self, request, exception, spider):
        # 处理异常的⽅法
        pass

常用使用示例

User-Agent中间件:用于切换随机的User-Agent

import random
from fake_useragent import UserAgent

class UADownloaderMiddleware:
    def process_request(self, request, spider):
        ua = UserAgent()
        user_agent = ua.random
        request.headers['User-Agent'] = user_agent
        
from scrapy import signals
import random

class RandomUserAgentMiddleware:
    def __init__(self, user_agents):
        self.user_agents = user_agents
        
    @classmethod
    def from_crawler(cls, crawler):
        user_agents = crawler.settings.get('USER_AGENTS')
        return cls(user_agents)
        
    def process_request(self, request, spider):
        user_agent = random.choice(self.user_agents)
        request.headers['User-Agent'] = user_agent

RandomUserAgentMiddleware 类会在每个请求发出前,从配置的 User-Agent 列表中随机选择⼀个 User-Agent,并将其设置到请求头中。当然也可以写简单点

import random
class WzryDownloaderMiddleware:
    def process_request(self, request, spider):
# UA池
        user_agent_list = [
            'Mozilla/5.0 (X11; U; Linux i686; fr; rv:1.8.1.8) Gecko/20071030Fedora/2.0.0.8-2.fc8 Firefox/2.0.0.8',
            'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36(KHTML, like Gecko) Chrome/42.0.2311.135 Safari/537.36 Edge/12.246',
            'Mozilla/5.0 (Windows; U; MSIE 9.0; WIndows NT 9.0; en-US))',
            'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36(KHTML like Gecko) Chrome/46.0.2486.0 Safari/537.36 Edge/13.10586',
            'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/534.24 (KHTML, likeGecko) Chrome/11.0.696.3 Safari/534.24',
            'Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en-us) AppleWebKit/312.1(KHTML, like Gecko)',
            'Opera/9.21 (X11; Linux i686; U; en)',
            'Mozilla/5.0 (X11; U; Linux i686; de-DE; rv:1.7.5) Gecko/20041108Firefox/1.0',
            'Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9pre)Gecko/2008040318 Firefox/3.0pre (Swiftfox)',
            'Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; WOW64;Trident/5.0; chromeframe/12.0.742.112)',
            'Opera/9.80 (X11; Linux x86_64; U; en) Presto/2.2.15Version/10.00',
            'Mozilla/5.0 (Windows NT 10.0; WOW64; rv:45.66.18) Gecko/20177177 Firefox/45.66.18',
            'Mozilla/2.0 (compatible; MSIE 3.01; Windows 95)',
            'Opera/9.80 (Windows NT 5.1; U; cs) Presto/2.2.15 Version/10.10',
            'Mozilla/5.0 (Windows NT 6.0) AppleWebKit/535.2 (KHTML, likeGecko) Chrome/15.0.874.120 Safari/535.2',
            'Mozilla/4.0 (compatible; MSIE 5.0; Windows 98; DigExt; YComp5.0.2.6; yplus 1.0)',
            'Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; es-la) Opera9.27',
            'Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_3; de-de)AppleWebKit/531.22.7 (KHTML, like Gecko) Version/4.0.5 Safari/531.22.7',
            'Mozilla/5.0 (Windows NT 10.0) AppleWebKit/537.36 (KHTML, likeGecko) Chrome/86.0.8810.3391 Safari/537.36 Edge/18.14383',
            'Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.0.8)Gecko/20061107 Fedora/1.5.0.8-1.fc6 Firefox/1.5.0.8']
            
        request.headers['User-Agent'] = random.choice(user_agent_list)
        
        print('现在使⽤的UA是:', request.headers['User-Agent'])
        return None

IP代理中间件:用于切换随机的IP代理

import random
class RandomProxyMiddleware:
    def __init__(self, proxies):
        self.proxies = proxies

    @classmethod
    def from_crawler(cls, crawler):
        proxies = crawler.settings.get('PROXIES')
        return cls(proxies)
    
    def process_request(self, request, spider):
    # request.meta['proxy'] = 'https://61.64.144.123:8080' 代理ip
        proxy = random.choice(self.proxies)
        request.meta['proxy'] = proxy

Selenium代替下载器:用于使用selenium进行请求和处理javascript渲染的页面。

from scrapy.http import HtmlResponse
from selenium import webdriver

class SeleniumMiddleware:
    def __init__(self):
        self.driver = webdriver.Chrome()
        
    def process_request(self, request, spider):
        self.driver.get(request.url)
        body = self.driver.page_source.encode('utf-8')
        return HtmlResponse(url=request.url, body=body, encoding='utf-8',
                    request=request, status=200)

请求失败切换使用的代理ip

import random
def process_exception(self, request, exception, spider):
    # 检查请求的 URL 协议,如果是 HTTP,则设置代理为随机选择的 http 代理
    if request.url.split(':')[0] == 'http':
        request.meta['proxy'] = 'http://' + random.choice(self.PROXY_http)
    # 如果是 HTTPS,则设置代理为随机选择的 https 代理
    if request.url.split(':')[0] == 'https':
        request.meta['proxy'] = 'https://' + random.choice(self.PROXY_https)
    # 返回新的请求对象,以便重新发送请求
    return request