Scrapy中间件

写在前面:该篇文章不会作特别详细的解释,只是讲述一下大致的使用方法和应用场景

先了解scrapy的工作流程,如下图:

scrapy框架流程图.png

中间件的分类

  • 下载中间件(Downloader Middleware)
  • 爬虫中间件(Spider Middleware)
  • 自定义中间件

一、下载中间件

应用场景

  • 更换代理ip
  • 更换Cookies
  • 更换User-Agent
  • 自动重试

1. 更换代理ip

a. middlewares.py
import random
from Test.settings import PROXY

class ProxyMiddleware(object):

    def process_request(self, request, spider):
        proxy = random.choice(PROXY)
        # 这里可以根据具体情况选择使用http还是https的代理
        request.meta['proxy'] = proxy
b. settings.py
# 启用中间件
DOWNLOADER_MIDDLEWARES = {
     'Test.middlewares.Proxy': 542,
}

#代理列表举例
PROXY = ['http://182.158.6.123:8888', 'https://182.158.6.123:8888']

优化:使用代理池(以下代码的导包就不再展示)

a. middlewares.py
class RandomProxy(object):

    def process_request(self, request, spider):
        # 使用代理池,防止ip被封
        pool = redis.ConnectionPool(host=POOL_HOST, port=POOL_PORT, db=POOL_DB, decode_responses=True)
        r = redis.Redis(connection_pool=pool)
        # 获取代理的方法,以及代理存放的方式都可根据不同场景更换;以下是redis取代理举例
        while True:
            if r.llen('xb') > 0:
                ip = r.lpop('xb')
                if str(ip) != 'None':
                    ip = json.loads(ip)
                    temp_cha = int(time.time()) - int(ip["time"])
                    if temp_cha <= 60:
                        del ip['time']
                        break
        r.close()
        # 使用代理,这里可以选择只使用http的代理
        request.meta['proxy'] = ip['http']
b. settings.py
# 启用中间件
DOWNLOADER_MIDDLEWARES = {
     'Test.middlewares.RandomProxy': 542,
}

# 代理池配置
POOL_HOST = '192.168.0.123'
POOL_PORT = 6379
POOL_DB = 2

注意:

  1. 启用中间件时,后边的数字(例如:542)表示中间件执行的优先级,这个数字越小,优先级越高,越早被执行;范围:100 - 900
  2. 如果要禁用中间件,可以注释掉或者将优先级值设为 None
  3. Scrapy其实自带了UA中间件(UserAgentMiddleware)、代理中间件(HttpProxyMiddleware)和重试中间件(RetryMiddleware),所以原则上要开发者三种中间件,需要先禁用自带的中间件,如:
DOWNLOADER_MIDDLEWARES = {
  'Test.middlewares.RandomProxy': 542,
  'scrapy.contrib.downloadermiddleware.useragent.UserAgentMiddleware': None,
  'scrapy.contrib.downloadermiddleware.httpproxy.HttpProxyMiddleware': None
}

但是大可不必这样做,因为scrapy自带的中间件源码已经给我们做了处理,关键源码:

def process_request(self, request, spider)
    # ignore if proxy is already set
    if 'proxy' in request.meta:
        return

所以当我们设置了自定义的代理、UA等的时候,不需要注释掉自带的中间件

2. 更换UA

a. middlewares.py
class UAMiddleware(object):

    def process_request(self, request, spider):
        ua = random.choice(settings['USER_AGENT_LIST'])
        request.headers['User-Agent'] = ua
b. settings
# 启用中间件
DOWNLOADER_MIDDLEWARES = {
     'Test.middlewares.UAMiddleware': 542,
}

USER_AGENT_LIST = [
  "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.101 Safari/537.36",
  "Dalvik/1.6.0 (Linux; U; Android 4.2.1; 2013022 MIUI/JHACNBL30.0)",
  "Mozilla/5.0 (Linux; U; Android 4.4.2; zh-cn; HUAWEI MT7-TL00 Build/HuaweiMT7-TL00) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1",
  "AndroidDownloadManager",
  "Apache-HttpClient/UNAVAILABLE (java 1.4)",
  "Dalvik/1.6.0 (Linux; U; Android 4.3; SM-N7508V Build/JLS36C)",
  "Android50-AndroidPhone-8000-76-0-Statistics-wifi",
  "Dalvik/1.6.0 (Linux; U; Android 4.4.4; MI 3 MIUI/V7.2.1.0.KXCCNDA)",
  "Dalvik/1.6.0 (Linux; U; Android 4.4.2; Lenovo A3800-d Build/LenovoA3800-d)",
  "Lite 1.0 ( http://litesuits.com )",
  "Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 5.1; Trident/4.0; .NET4.0C; .NET4.0E; .NET CLR 2.0.50727)",
  "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/38.0.2125.122 Safari/537.36 SE 2.X MetaSr 1.0",
  "Mozilla/5.0 (Linux; U; Android 4.1.1; zh-cn; HTC T528t Build/JRO03H) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30; 360browser(securitypay,securityinstalled); 360(android,uppayplugin); 360 Aphone Browser (2.0.4)",
]

3. 使用Cookies

可以修改下载中间件里的process_request方法,也可自定义
a. middlewares.py
class TestDownloaderMiddleware(object):

        def process_request(self, request, spider):
        # 使用cookie访问
        request.cookies = COOKIES
        return None
b. settings.py(这里就不再重复启用中间件的代码)
COOKIES = {
    'a': 'aaaa',
    'b': 'bbbbb',
    'c': 'ccccc,
}
优化:将获取cookies的功能封装成一个程序,启动程序,重复获取cookie并存入redis或者数据库中,做一个cookie池,然后中间件中直接从cookie池中取cookie进行访问即可。降低被发现或者封号的可能性。

4. 中间件使用Selenium

selenium能够帮助我们解决异步加载的网站数据的抓取;不过弊端也很多,如效率低下,维护困难,爬虫程序启动所依赖的工具增加(浏览器和驱动)
a. middlewares.py
from scrapy.http import HtmlResponse
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.wait import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC

class SeleniumMiddleware(object):
    def __init__(self):
        # 这里以Chrome为例
        # self.driver = self.set_driver()
        self.ua = settings.get('USER_AGENT')
        self.driver = None

    @classmethod
    def from_crawler(cls, crawler):
        return cls(crawler.settings)
   
    def set_driver(self):
        """
        设置webdriver
        """
        options = webdriver.ChromeOptions()
        options.add_argument("--disable-blink-features=AutomationControlled")
        options.add_argument("--headless=new")
        options.add_argument("--no-sandbox")
        options.add_argument("--disable-dev-shm-usage")
        # 将 Chrome正受到自动化软件的控制 这几个关键字去掉
        options.add_experimental_option("excludeSwitches", ["enable-automation"])
        options.add_experimental_option("useAutomationExtension", False)
        options.add_argument("disable-infobars")
        # 将js window.navigator.webdriver 的执行结果置为 undefined
        options.add_argument("disable-blink-features=AutomationControlled")
        # 设置代理(这里也可改为自动获取随机代理),然后填入
        options.add_argument('--proxy-server=195.169.5.123:8888')

        driver = webdriver.Chrome(options=options)
        driver.execute_script(
            "Object.defineProperty(navigator, 'webdriver', {get: () => undefined})"
        )

        driver.execute_cdp_cmd(
            "Network.setUserAgentOverride", {"userAgent": self.ua}
        )
        return driver

    def process_request(self, request, spider):
        if spider.name == 'seleniumSpider':
            # 这里是每次重新设置driver还是在init里初始化,视情况而定
            self.driver = self.set_driver()
            try:
                self.driver.get(request.url)
            except selenium.common.exceptions.WebDriverException:
                self.driver.close()
                self.driver.get(request.url)
            time.sleep(2)
            # 等待元素加载完成
            # WebDriverWait(self.driver, 10).until(
            #     EC.presence_of_element_located((By.XPATH, '//*[@class="text"]'))
            # )
            body = self.driver.page_source
            request.meta['driver'] = self.driver
            return HtmlResponse(self.driver.current_url,
                                body=body,
                                encoding='utf-8',
                                request=request)

        def process_exception(self, request, exception, spider):
            self.driver.quit()
            return

5. 中间件中重试

对于请求失败失败的url或者被重定向未取到数据的url,我们可以在中间件的process_response中进行重试
class RetryMiddleware(RetryMiddleware):
    def __init__(self, settings):
        RetryMiddleware.__init__(self, settings)

    def process_response(self, request, response, spider):
        # 如果重定向,我们可以使用request.meta['redirect_urls']这个方法查看到被重定向的url列表,列表里边保存了每次重定向的url
        url = request.meta['redirect_urls'][0]  # 取出有真实数据的那个url
        yield scrapy.Request(url=url, method='POST', body=json_body, headers=headers)

6. 中间件中处理异常

一般情况下,请求失败之后,scrapy会原地重试三次,如果三次都失败了,则放弃这个请求;但如果url正确,由于各种原因导致三次请求都失败,则我们需要重新将其加入请求队列中;修改 process_exception 即可。例如现在有一个TCP超时错误:twisted.internet.error.TCPTimeOutError
# 先导入这个异常
from twisted.internet.error import TCPTimedOutError

# 还是在重试中间件中
class RetryMiddleware(RetryMiddleware):
    def __init__(self, settings):
        RetryMiddleware.__init__(self, settings)

    def process_exception(self, request, exception, spider):
        if isinstance(exception, TCPTimedOutError):
            # 这里可以根据具体场景进行一系列操作(比如:如果是代理异常,则删除失效代理),我这里就不再举例
            return request.copy()

二、爬虫中间件

应用场景

  • 处理爬虫本身的异常;常用来处理爬虫异常(比如将错误记录到数据库中)

被调用的情况

  • 当运行到yield scrapy.Request()或者yield item的时候,爬虫中间件的process_spider_output()方法被调用。
  • 当爬虫本身的代码出现了Exception的时候,爬虫中间件的process_spider_exception()方法被调用。
  • 当爬虫里面的某一个回调函数parse_xxx()被调用之前,爬虫中间件的process_spider_input()方法被调用。
  • 当运行到start_requests()的时候,爬虫中间件的process_start_requests()方法被调用。

1. process_spider_exception(self, response, exception, spider)

a. middlewares.py
class ExceptionCheckMiddleware(object):
    def process_spider_exception(self, response, exception, spider):
        # 创建一个erroritem
        error_item = ErrorItem()
        # 记录错误发生的页数和时间
        error_item['error_time'] = datetime.datetime,now().striftime('%Y-%m-%d %H:%M:%S')
        error_item['page'] = response.meta['page']
        yield error_item
b. settings.py
# 激活爬虫中间件
SPIDER_MIDDLEWARES = {
    'Test.middlewares.TestSpiderMiddleware': 543,
}

2. process_spider_output(response, result, output)

这里只写关键代码
def process_spider_output(response, result, spider):
    for item in result:
        if isinstance(item, scrapy.Item):
            # 这里可以对即将被提交给pipeline的item进行各种操作
            print(f'item将会被提交给pipeline')
        yield item
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,026评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,655评论 1 296
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,726评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,204评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,558评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,731评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,944评论 2 314
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,698评论 0 203
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,438评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,633评论 2 247
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,125评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,444评论 3 255
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,137评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,103评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,888评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,772评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,669评论 2 271

推荐阅读更多精彩内容