爬虫教程( 4 ) --- scrapy-redis、集群、curl-cffi、scrapy-stealth、aio-scrapy、omnicrawl-core、反检测(反爬)浏览器

本文深入解析 Scrapy-Redis 的工作原理,包括其如何利用 Redis 实现分布式爬虫,优化爬虫性能,以及如何配置和使用 Scrapy-Redis 的各项功能。涵盖调度器、Pipeline、配置细节、优化技巧等关键内容。

scrapy 文档:https://docs.scrapy.org/en/latest/
scrapy-redis 文档:https://github.com/rmax/scrapy-redis

spiderbox:https://spiderbox.cn/

scrapy-redis、scrapy_redis_cluster (集群版)

1、分布式 爬虫 

scrapy 怎么 分布式 ?

文档:http://doc.scrapy.org/en/master/topics/practices.html#distributed-crawls

Scrapy 并没有提供内置的机制支持分布式(多服务器)爬取。不过还是有办法进行分布式爬取, 取决于您要怎么分布了。

  • 如果有很多 spider,那分布负载最简单的办法就是启动多个Scrapyd,并分配到不同机器上。
  • 如果想要在多个机器上运行一个单独的spider,那您可以将要爬取的 url 进行分块,并发送给spider。
  • ......

scrapy-redis 分布式 爬虫框架

scrapy 任务调度是基于文件系统,这样只能在单机执行 crawl。但是 scrapy-redis 巧妙的利用 redis 队列实现 request queue items queue,利用 redis 的 set 实现 request 的去重,将 scrapy 从单台机器扩展多台机器,实现较大规模的爬虫集群

 scrapy-redis 主要功能如下:

  • 分布式爬虫。多个爬虫实例共享一个 redis request 队列,非常适合大范围多域名的爬虫集群
  • 分布式后处理。爬虫抓取到的 items push 到一个 redis items 队列,这就意味着可以开启多个 items processes 来处理抓取到的数据,比如存储到 Mongodb、Mysql
  • 基于 scrapy 即插即用组件。Scheduler + Duplication Filter、Item Pipeline、 Base Spiders

scrapy 架构图

Scrapy 中的数据流由执行引擎控制,流程如下:

  1. 引擎 首先从自己编写的 spider 中读取起始 url,然后封装成 Request对象
  2. 引擎 把 "封装后的Request对象" 传递给 调度器 ( 调度器主要作用就是管理、调度url,可以简单的看作是一个 "间接的队列",对 Requestd对象 管理、过滤 等操作)。
  3. 引擎 请求 调度器,调度器返回一个 Request对象 给引擎。
  4. 引擎 将 Request对象 发送到下载器。下载器会将请求通过下载器中间件。( process_request() )
  5. 下载器完成页面下载后,下载器会将生成的 响应Response 通过下载器中间件(process_response()),最后将其发送到引擎。
  6. 引擎接收来自下载器的 响应 并将其发送给 自己编写的 spider 进行处理,但是在发送之前会先 传递 通过spider中间件(参见process_spider_input())。
  7. 自己编写的 spider 处理响应并返回 "抓取的数据Item" 及(跟进的)新的Request给引擎,通过蜘蛛中间件(参见process_spider_output())。
  8. 引擎将 "抓取的数据Item" 发送到 pipeline,将 "新的请求" 发送到 调度程序。并继续从调度器中获取 下一个 "Request对象" 来抓取。
  9. 该过程重复(从步骤 3 开始),直到没有更多地 request对象 ,最后关闭引擎。

整个工作流程

  • 1.引擎 将爬虫中起始的url构造成request对象,并传递给调度器。
  • 2.引擎 从 调度器 中获取到request对象然后交给下载器。
  • 3.由 下载器 来获取到页面源代码,并封装成response对象,并返回给引擎。
  • 4.引擎 将获取到的response对象传递给 spider,由 spider 对数据进行解析(parse),并返回给引擎
  • 5.引擎将数据传递给 pipeline 进行数据持久化保存或进一步的数据处理
  • 6.在此期间如果spider中提取到的并不是数据。而是子页面ur.可以进一步提交给调度器,进而重复 步骤2 的过程

scrapy-redis 安装

文档:https://scrapy-redis.readthedocs.org.

安装 scrapy-redis:pip install scrapy-redis

scrapy-redis 源码 分析

Scrapy-Redis 四大核心

  • RedisScheduler:Redis 调度器,替代原生内存 Scheduler,管理待爬请求
  • RFPDupeFilter:Redis 指纹去重器,替代内存去重,全局 URL 去重
  • RedisSpider/RedisCrawlSpider:分布式爬虫基类。将请求队列、去重指纹队列、爬虫状态全部下沉到 Redis,实现分布式爬取、断点续爬、多机器协同采集。 核心本质:用 Redis 替代 Scrapy 内存调度器,实现分布式调度
  • RedisPipeline:可选,数据直接存入 Redis

分布式核心设计思想

  • 全局共享请求队列(Redis List) 所有爬虫节点共用同一个 Redis List 存储待爬 Request,多机器争抢消费,天然分布式。
  • 全局 URL 去重库(Redis Set) 所有爬虫共用一个 Set 存储已爬 URL 指纹,任意机器发起请求前先校验指纹,全网统一去重。
  • 持久化断点续爬 Redis 支持 RDB/AOF 持久化,爬虫异常宕机、重启后,队列与指纹数据不丢失,直接恢复爬取。
  • 规则爬取复用 CrawlSpider 规则提取 保留 rules 链接提取器规则,自动从响应中提取分页、详情页链接,存入 Redis 共享队列。

Redis 存储结构(关键 Key),假设爬虫名 demo_spider

  • demo_spider:requests(List):待爬请求队列,LPUSH 入队,BRPOP 阻塞消费
  • demo_spider:dupefilter(Set):URL 指纹集合,存储 sha1 加密的请求指纹,用于去重
  • start_urls / redis_key:起始 URL 池,爬虫启动自动读取初始化种子链接。
    非分布式是从 start_urls 读取链接,这个直接在爬虫文件中配置。
    分布式是从 redis_key 读取链接。这个需要手动往redis推送一次链接任务。

启用分布式调度,替换原生组件,必须配置:

# 启用Redis调度器
SCHEDULER = "scrapy_redis.scheduler.Scheduler"
# 全局Redis去重
DUPEFILTER_CLASS = "scrapy_redis.dupefilter.RFPDupeFilter"
# 开启断点续爬,Redis数据不清空
SCHEDULER_PERSIST = True
# Redis连接配置
REDIS_URL = "redis://127.0.0.1:6379/0"
# 可选:数据入库Redis
ITEM_PIPELINES = {"scrapy_redis.pipelines.RedisPipeline": 300}

断点续爬 & 多节点协同收尾逻辑

  • 多机并行:启动 N 台机器执行同一份爬虫代码,全部连接同一个 Redis,共同消费 requests 队列,任务自动负载均衡
  • 宕机恢复:爬虫强制关闭、服务器重启时,Redis 内 requests 队列、dupefilter 集合数据保留;再次启动爬虫直接继续爬剩余 URL
  • 爬虫清空:如需重新全量爬取,redis-cli 删除对应 key
  • 队列无任务:所有节点 BRPOP 阻塞等待,新种子 URL 推入后自动恢复爬取

关键底层源码运行逻辑简化

  • RedisCrawlSpider 重写父类 start_requests() 方法: 不再读取本地 start_urls,而是从 Redis redis_key 拉取种子链接生成请求。
  • RedisScheduler.enqueue_request(): 所有新请求(种子 + 页面提取链接)统一序列化后存入 Redis List。
  • RFPDupeFilter.request_seen(): 操作 Redis Set 判断指纹是否存在,实现全局去重。
  • 序列化机制:Request 对象通过 pickle 序列化存入 Redis,支持完整请求参数(headers、cookies、meta、代理等)跨机器传递。

可以看到 scrapy-redis 的 spiders.py 模块,导入了 scrapy.spiders 的 Spider、CrawlSpider,然后重新写了两个类 RedisSpiders、RedisCrawlSpider,分别继承 Spider、CrawlSpider,所以如果要想从 redis 读取任务,需要把自己写的 spider 继承 RedisSpiders、RedisCrawlSpider,而不是 scrapy 的 Spider、CrawlSpider。。。

spider.py 中 RedisMixin、RedisSpiders、RedisCrawlSpider

RedisMixin 类,读取 配置文件,决定使用 什么类型的 redis 队列

可以在 setting 中设置下面两项,来决定 redis 任务队列是 set 还是 zset
REDIS_START_URLS_AS_SET
REDIS_START_URLS_AS_ZSET

Scrapy-redis 之 RFPDupeFilter、Queue、Schedulerhttps://www.cnblogs.com/Alexephor/p/11446167.html

1.找到from scrapy_redis.scheduler import Scheduler
    -执行Scheduler.from_crawler
    -执行Scheduler.from_settings
        - 读取配置文件:
            SCHEDULER_PERSIST                # 是否在关闭时候保留原来的调度器和去重记录,True=保留,False=清空
            SCHEDULER_FLUSH_ON_START        # 是否在开始之前清空 调度器和去重记录,True=清空,False=不清空
            SCHEDULER_IDLE_BEFORE_CLOSE        # 去调度器中获取数据时,如果为空,最多等待时间(最后没数据,未获取到)
        - 读取配置文件:
            SCHEDULER_QUEUE_KEY                # 调度器中请求存放在redis中的key
            SCHEDULER_QUEUE_CLASS            # 这里可以选择三种先进先出、后进先出、优先级,默认使用优先级队列(默认),其他:PriorityQueue(有序集合),FifoQueue(列表)、LifoQueue(列表)
            SCHEDULER_DUPEFILTER_KEY        # 去重规则,在redis中保存时对应的key
            DUPEFILTER_CLASS                # 这里有两种选择使用默认或者自己定义的
                # 内置比如:DUPEFILTER_CLASS = 'scrapy_redis.dupefilter.RFPDupeFilter'
                # 自定义的比如:DUPEFILTER_CLASS = 'redisdepth.xxx.DupeFilter'    这个优先级别高 在源码里边是先判断然后再后续操作
            SCHEDULER_SERIALIZER            # 对保存到redis中的数据进行序列化,默认使用pickle
        - 读取配置文件:redis-server
            # 源码在connection.py中17行
            REDIS_HOST = '192.168.1.13'                           # 主机名
            REDIS_PORT = 3306                                     # 端口
            REDIS_PARAMS = {'password': 'woshinidaye'}            # Redis连接参数  默认:REDIS_PARAMS = {'socket_timeout': 30,'socket_connect_timeout': 30,'retry_on_timeout': True,'encoding': REDIS_ENCODING,})
            # REDIS_PARAMS['redis_cls'] = 'myproject.RedisClient' # 指定连接Redis的Python模块  默认:redis.StrictRedis
            REDIS_ENCODING = "utf-8"                              # redis编码类型  默认:'utf-8'
            # REDIS_URL = 'redis://user:pass@hostname:9001'       # 连接URL(优先于以上配置)源码可以看到
2.爬虫开始执行起始URL
        - 调用Scheduler.enqueue_request
        def enqueue_request(self, request):
            # 请求需要过滤?并且 去重规则是否已经有?(是否已经访问,如果未访问添加到去重记录)request_seen去重规则重要的一个方法
            if not request.dont_filter and self.df.request_seen(request):
                self.df.log(request, self.spider)
                # 已经访问过不再进行访问
                return False
            if self.stats:
                self.stats.inc_value('scheduler/enqueued/redis', spider=self.spider)
            # 未访问过,添加到调度器中把这个请求
            self.queue.push(request)
            return True
3.下载器去调度中获取任务,去执行任务下载
        - 调用Scheduler.next_request
        def next_request(self):
            block_pop_timeout = self.idle_before_close
            # 把任务取出来
            request = self.queue.pop(block_pop_timeout)
            if request and self.stats:
            # 此时下载
                self.stats.inc_value('scheduler/dequeued/redis', spider=self.spider)
            return request

settings 需要的配置

# redis去重配置
REDIS_HOST = '192.168.1.13'                           # 主机名
REDIS_PORT = 3306                                     # 端口
REDIS_PARAMS = {'password': 'woshinidaye'}            # Redis连接参数  默认:REDIS_PARAMS = {'socket_timeout': 30,'socket_connect_timeout': 30,'retry_on_timeout': True,'encoding': REDIS_ENCODING,})
# REDIS_PARAMS['redis_cls'] = 'myproject.RedisClient' # 指定连接Redis的Python模块  默认:redis.StrictRedis
REDIS_ENCODING = "utf-8"                              # redis编码类型  默认:'utf-8'

# REDIS_URL = 'redis://user:pass@hostname:9001'       # 连接URL(优先于以上配置)源码可以看到
DUPEFILTER_KEY = 'dupefilter:%(timestamp)s'
# 纯源生的它内部默认是用的以时间戳作为key
# DUPEFILTER_CLASS = 'scrapy_redis.dupefilter.RFPDupeFilter'
# 我自定义在源码之上改了保存在redis中的key配置
DUPEFILTER_CLASS = 'redisdepth.xxx.RedisDupeFilter'
# 自定义redis去重配置
# DUPEFILTER_CLASS = 'redisdepth.xxx.DupeFilter'


# #############调度器配置###########################
# from scrapy_redis.scheduler import Scheduler

SCHEDULER = "scrapy_redis.scheduler.Scheduler"
DEPTH_PRIORITY = 1  # 广度优先
# DEPTH_PRIORITY = -1 # 深度优先
SCHEDULER_QUEUE_CLASS = 'scrapy_redis.queue.PriorityQueue'  # 默认使用优先级队列(默认),其他:PriorityQueue(有序集合),FifoQueue(列表)、LifoQueue(列表)
# 广度优先
# SCHEDULER_QUEUE_CLASS = 'scrapy_redis.queue.FifoQueue'
# 深度优先
# SCHEDULER_QUEUE_CLASS = 'scrapy_redis.queue.LifoQueue'

SCHEDULER_QUEUE_KEY = '%(spider)s:requests'         # 调度器中请求存放在redis中的key
SCHEDULER_SERIALIZER = "scrapy_redis.picklecompat"  # 对保存到redis中的数据进行序列化,默认使用pickle
SCHEDULER_PERSIST = True                            # 是否在关闭时候保留原来的调度器和去重记录,True=保留,False=清空
SCHEDULER_FLUSH_ON_START = True                     # 是否在开始之前清空 调度器和去重记录,True=清空,False=不清空
SCHEDULER_IDLE_BEFORE_CLOSE = 10                    # 去调度器中获取数据时,如果为空,最多等待时间(最后没数据,未获取到)。
SCHEDULER_DUPEFILTER_KEY = '%(spider)s:dupefilter'  # 去重规则,在redis中保存时对应的key
SCHEDULER_DUPEFILTER_CLASS = 'scrapy_redis.dupefilter.RFPDupeFilter'    # 去重规则对应处理的类

配置文件大解读

# -*- coding: utf-8 -*-

# 爬虫名称
BOT_NAME = 'redisdepth'

# 爬虫应用路径
SPIDER_MODULES = ['redisdepth.spiders']
NEWSPIDER_MODULE = 'redisdepth.spiders'


# Crawl responsibly by identifying yourself (and your website) on the user-agent
# 客服端user-agent请求头
#USER_AGENT = 'redisdepth (+http://www.yourdomain.com)'
USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.169 Safari/537.36'

# 爬虫君子证书,禁止爬虫设置
# Obey robots.txt rules
# ROBOTSTXT_OBEY = True
ROBOTSTXT_OBEY = False

# Configure maximum concurrent requests performed by Scrapy (default: 16)
# 并发请求数 力度要粗点
#CONCURRENT_REQUESTS = 32

# Configure a delay for requests for the same website (default: 0)
# See https://docs.scrapy.org/en/latest/topics/settings.html#download-delay
# See also autothrottle settings and docs
# 延迟下载秒数
#DOWNLOAD_DELAY = 3
# The download delay setting will honor only one of:
# 单域名访问并发数 并且延迟下次秒数也用在每个域名
#CONCURRENT_REQUESTS_PER_DOMAIN = 16
# 单IP访问并发数,如果有值则忽略:CONCURRENT_REQUESTS_PER_DOMAIN,并且延迟下次秒数也应用在每个IP
#CONCURRENT_REQUESTS_PER_IP = 16

# Disable cookies (enabled by default)
# 是否支持cookie,cookiejar进行操作cookie
#COOKIES_ENABLED = False

# Disable Telnet Console (enabled by default)
# Telnet用于查看当前爬虫的信息,操作爬虫等...
#    使用telnet ip port ,然后通过命令操作
# TELNETCONSOLE_ENABLED = True
# TELNETCONSOLE_HOST = '127.0.0.1'
# TELNETCONSOLE_PORT = [6023,]
#TELNETCONSOLE_ENABLED = False

# 默认请求头
# Override the default request headers:
#DEFAULT_REQUEST_HEADERS = {
#   'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
#   'Accept-Language': 'en',
#}

# 爬虫中间件
# Enable or disable spider middlewares
# See https://docs.scrapy.org/en/latest/topics/spider-middleware.html
# SPIDER_MIDDLEWARES = {
#    # 'redisdepth.middlewares.RedisdepthSpiderMiddleware': 543,
#     'redisdepth.sd.Sd1': 666,
#     'redisdepth.sd.Sd2': 667,
#
# }

# 下载中间件
# Enable or disable downloader middlewares
# See https://docs.scrapy.org/en/latest/topics/downloader-middleware.html
# DOWNLOADER_MIDDLEWARES = {
#    # 'redisdepth.middlewares.RedisdepthDownloaderMiddleware': 543,
#    #  'redisdepth.md.Md1': 666,
#    #  'redisdepth.md.Md2': 667
# }

# 自定义扩展,基于信号进行调用
# Enable or disable extensions
# See https://docs.scrapy.org/en/latest/topics/extensions.html
EXTENSIONS = {
   # 'scrapy.extensions.telnet.TelnetConsole': None,
    'redisdepth.ext.MyExtension': 666,
}

# 定义pipeline处理请求
# Configure item pipelines
# See https://docs.scrapy.org/en/latest/topics/item-pipeline.html
#ITEM_PIPELINES = {
#    'redisdepth.pipelines.RedisdepthPipeline': 300,
#}

"""
 自动限速算法
    from scrapy.contrib.throttle import AutoThrottle
    自动限速设置
    1. 获取最小延迟 DOWNLOAD_DELAY
    2. 获取最大延迟 AUTOTHROTTLE_MAX_DELAY
    3. 设置初始下载延迟 AUTOTHROTTLE_START_DELAY
    4. 当请求下载完成后,获取其"连接"时间 latency,即:请求连接到接受到响应头之间的时间
    5. 用于计算的... AUTOTHROTTLE_TARGET_CONCURRENCY
    target_delay = latency / self.target_concurrency
    new_delay = (slot.delay + target_delay) / 2.0 # 表示上一次的延迟时间
    new_delay = max(target_delay, new_delay)
    new_delay = min(max(self.mindelay, new_delay), self.maxdelay)
    slot.delay = new_delay
"""
# Enable and configure the AutoThrottle extension (disabled by default)
# See https://docs.scrapy.org/en/latest/topics/autothrottle.html
# 开始自动限速
#AUTOTHROTTLE_ENABLED = True
# The initial download delay
# 初始下载延迟
#AUTOTHROTTLE_START_DELAY = 5
# The maximum download delay to be set in case of high latencies
# 最大下载延迟
#AUTOTHROTTLE_MAX_DELAY = 60
# The average number of requests Scrapy should be sending in parallel to
# each remote server
# 平均每秒并发数
#AUTOTHROTTLE_TARGET_CONCURRENCY = 1.0
# Enable showing throttling stats for every response received:
# 是否显示
#AUTOTHROTTLE_DEBUG = False

# Enable and configure HTTP caching (disabled by default)
# See https://docs.scrapy.org/en/latest/topics/downloader-middleware.html#httpcache-middleware-settings
"""
启用缓存
    目的用于将已经发送的请求或相应缓存下来,以便以后使用
    
    from scrapy.downloadermiddlewares.httpcache import HttpCacheMiddleware
    from scrapy.extensions.httpcache import DummyPolicy
    from scrapy.extensions.httpcache import FilesystemCacheStorage
"""
# 是否启用缓存策略
#HTTPCACHE_ENABLED = True
# 缓存策略:所有请求均缓存,下次在请求直接访问原来的缓存即可
# HTTPCACHE_POLICY = "scrapy.extensions.httpcache.DummyPolicy"
# 缓存策略:根据Http响应头:Cache-Control、Last-Modified 等进行缓存的策略
# HTTPCACHE_POLICY = "scrapy.extensions.httpcache.RFC2616Policy"
# 缓存超时时间
#HTTPCACHE_EXPIRATION_SECS = 0
# 缓存保存路径
#HTTPCACHE_DIR = 'httpcache'
# 缓存忽略的http状态码
#HTTPCACHE_IGNORE_HTTP_CODES = []
# 缓存存储的插件
#HTTPCACHE_STORAGE = 'scrapy.extensions.httpcache.FilesystemCacheStorage'

scrapy-redis 调度器(Scheduler) 

  • scrapy-redis 调度器 通过 redis 的 set 不重复的特性,巧妙的实现了 Duplication Filter 去重(DupeFilter set 存放爬取过的 request)。
  • Spider 新生成的 request,将 request 的指纹到 redis 的 DupeFilter set 检查是否重复,并将不重复的 request push 写入 redis 的 request 队列。
  • 调度器每次从 redis 的 request 队列里根据优先级 pop 出一个 request, 将此 request 发给 spider 处理。

scrapy-redis Pipeline

将 Spider 爬取到的 Item 给 scrapy-redis 的 Item Pipeline,将爬取到的 Item 存入 redis 的 items 队列。可以很方便的从 items 队列中提取 item,从而实现 items processes 集群

示例:scrapy-redis

https://www.51tietu.net/xiaohua/

scrapy_redis.spiders 下有两个类 RedisSpider 和 RedisCrawlSpider,能够使 spider 从 Redis 读取 start_urls,然后执行爬取,若爬取过程中返回更多的 request url,那么它会继续进行直至所有的 request 完成之后,再从 redis start_urls 中读取下一个 url,循环这个过程

创建 scrapy-redis 项目

  • 方法 1:命令行执行:scrapy startproject MyScrapyRedis,然后自己写的 spider 继承 RedisSpider 或者 RedisCrawlSpider ,设置对应的 redis_key ,即队列的在 redis 中的 key。注意:这个需要手动 在 setting.py 里面配置设置。( 参考配置:https://github.com/rmax/scrapy-redis
  •  方法 2:使用 scrapy-redis 的 example 来修改。先从 github ( https://github.com/rmax/scrapy-redis ) 上拿到 scrapy-redis 的 example,然后将里面的 example-project 目录移到指定的地址。

使用 PyCharm 打开 MyScrapyRedis 项目,继承 scrapy_redis 的 RedisSpider 类

scrapy_redis 的 RedisSpider 类 说明:

可以看到 RedisSpider 有三个属性,这三个属性的默认值都在 scrapy_redis 下的 default.py 中

  • redis_key  
  • redis_batch_size
  • redis_encoding

settings 需要设置的选项

  • REDIS_START_URLS_KEY  ( 如果设置了redis_key ,则覆盖这个配置 )
  • REDIS_START_URLS_BATCH_SIZE ( 已经废弃,使用 CONCURRENT_REQUESTS 代替 )
  • REDIS_START_URLS_AS_SET ( 如果是 True 则使用 set 集合作为 任务队列,默认 False 使用 list )
  • REDIS_ENCODING ( 设置 队列任务的编码 )

编写 example.py 爬虫文件

这里设置 redis_key = f'redis_key:{name}',同时设置 REDIS_START_URLS_AS_SET= True

import scrapy
from scrapy_redis.spiders import RedisSpider, RedisCrawlSpider


class ExampleSpider(RedisSpider):
    name = "example"
    redis_key = f'redis_key:{name}_zset'

    custom_settings = {
        'REDIS_START_URLS_AS_ZSET': True,
        # 'REDIS_START_URLS_AS_SET': True,
        # 'SCHEDULER_IDLE_BEFORE_CLOSE': 5
    }

    def parse(self, response):
        print(f'response.url ---> {response.url}')
        item = {
            'url': response.url,
        }
        yield item
        pass


if __name__ == '__main__':
    # add_task()
    from scrapy import cmdline
    cmdline.execute('scrapy crawl example'.split())
    pass

编写 Item

Scrapy 中可以直接返回一个 Python 的 字典 给 pipeline,但是这并不是最佳实践。scrapy提供了一个Item基类,可以通过继承这个类定义自己的结构化数据,比到处传递字典更好。

下面是官方文档的例子。

import scrapy

class Product(scrapy.Item):
    name = scrapy.Field()
    price = scrapy.Field()
    stock = scrapy.Field()
    last_updated = scrapy.Field(serializer=str)

一般都定义在 scrapy 项目的 items.py 文件中。定义好之后,在爬虫中我们就不应该在返回字典了,而是初始化并返回我们自定义的 Item 对象。

提示:为了演示,下面示例代码是直接返回 Python 字典给 pipeline

修改 settings.py

scrapy-redis 的默认配置

https://github.com/rmax/scrapy-redis/blob/master/src/scrapy_redis/defaults.py

custom_settings = {
        'REDIS_URL': 'redis://127.0.0.1:6379/0',
        'CONCURRENT_REQUESTS': 1,
        'DOWNLOAD_DELAY': 1,
        'CONCURRENT_REQUESTS_PER_DOMAIN': 100,
        'SCHEDULER': 'scrapy_redis.scheduler.Scheduler',
        'DUPEFILTER_CLASS': "scrapy_redis.dupefilter.RFPDupeFilter",
        'SCHEDULER_QUEUE_CLASS': "scrapy_redis.queue.PriorityQueue",
        'SCHEDULER_QUEUE_KEY': "%(spider)s:requests",
        'SCHEDULER_DUPEFILTER_KEY': "%(spider)s:dupefilter",
        'SCHEDULER_PERSIST': True,
        'REDIS_START_URLS_AS_SET': True
        # 'REDIS_START_URLS_AS_ZSET': True
    }

SCHEDULER_DUPEFILTER_KEY 与 DUPEFILTER_KEY 完整区别、使用场景

  • SCHEDULER_DUPEFILTER_KEY(调度器去重键,分布式爬虫主用)。对应类:scrapy_redis.dupefilter.RFPDupeFilter 作用: 分布式爬虫调度器内置的 URL 去重集合,和 Redis 请求队列配套使用。 每条爬虫独立一套去重集合,键默认:%(spider)s:dupefilter 存储结构:Redis Set 数据生命周期:由 SCHEDULER_PERSIST 控制,爬虫关闭可持久保留,支持断点续爬。标准分布式爬虫只会读取 SCHEDULER_DUPEFILTER_KEYDUPEFILTER_KEY 完全失效;
  • UPEFILTER_KEY(独立模式去重键,单机 standalone 模式)。对应场景:单独实例化 RFPDupeFilter、不依赖 scrapy-redis 调度器的独立去重器 默认值:dupefilter:%(timestamp)s 。%(timestamp)s 是爬虫启动时间戳,每次重启爬虫自动生成全新 key,爬虫重启后旧指纹直接废弃,无法断点续爬 几乎不用于常规分布式爬虫。DUPEFILTER_KEY 什么时候会生效?只有你不使用 redis 调度器,手动单独初始化去重器才会读取该配置:
    from scrapy_redis.dupefilter import RFPDupeFilter
    from scrapy.utils.project import get_project_settings
    settings = get_project_settings()
    # 手动创建独立去重器,此时读取 DUPEFILTER_KEY
    dupe_filter = RFPDupeFilter.from_settings(settings)
    这种写法脱离了分布式调度器,日常爬虫几乎不用。

优先级与覆盖规则

  1. 当开启 SCHEDULER 分布式调度时,只会读取 SCHEDULER_DUPEFILTER_KEYDUPEFILTER_KEY 完全失效,无论是否设置都不影响。
  2. 只有手动单独实例 RFPDupeFilter、不使用 redis 调度器时,才读取 DUPEFILTER_KEY
  3. 两者互不覆盖,两套独立逻辑,不存在互相冲突。

scrapy-redis 单独自定义 SCHEDULER_QUEUE_KEY / SCHEDULER_DUPEFILTER_KEY

  • 方法 1:直接在 settings.py 硬编码固定值(全局统一,所有爬虫共用一套队列 / 去重)。不需要占位符 %(spider)s,直接写死字符串:
    # 自定义请求队列key,不再自动拼接爬虫名
    SCHEDULER_QUEUE_KEY = "my_crawl:all_requests"
    # 自定义去重指纹key
    SCHEDULER_DUPEFILTER_KEY = "my_crawl:finger_filter"
    缺点:多个爬虫会共用同一个队列、同一个去重集合,互相干扰,适合单爬虫项目。
  • 方法 2:按爬虫名自定义前缀(推荐,保留隔离,自定义前缀)
    保留 %(spider)s 占位符,只修改前后固定字符串:
    # 队列:自定义前缀,自动拼接爬虫名
    SCHEDULER_QUEUE_KEY = "distributed_crawl:%(spider)s:task_queue"
    # 去重集合:自定义前缀
    SCHEDULER_DUPEFILTER_KEY = "distributed_crawl:%(spider)s:url_filter"
    Redis 实际生成 key 示例(爬虫名 test_spider):
    distributed_crawl:test_spider:task_queue
    distributed_crawl:test_spider:url_filter
  • 方法 3:单爬虫单独指定不同 key(动态覆盖,爬虫内重写配置)。在单个爬虫文件内,自定义 custom_settings,只对当前爬虫生效,不影响其他爬虫:
    import scrapy
    from scrapy.linkextractors import LinkExtractor
    from scrapy.spiders import CrawlSpider, Rule
    
    class DemoSpider(CrawlSpider):
        name = "demo_spider"
        allowed_domains = ["xxx.com"]
    
        # 当前爬虫单独覆盖redis队列、去重key
        custom_settings = {
            SCHEDULER_QUEUE_KEY: "demo_task:requests",
            SCHEDULER_DUPEFILTER_KEY: "demo_task:dupefilter",
        }
    
        rules = (
            Rule(LinkExtractor(), callback="parse_item", follow=True),
        )
    
        def parse_item(self, response):
            pass

SCHEDULER_PERSIST = True    # 开启持久化,关闭爬虫不清理 Redis 队列

  • scrapy-redis 的 Scheduler 会自动检查 spider_demo:requests 队列,如果队列中有任务,会直接从队列中读取并执行,无需重新添加起始 URL

SCHEDULER_QUEUE_CLASS = "scrapy_redis.queue.SpiderPriorityQueue"  是配置优先级队列。作用:爬虫请求队列使用 Redis 有序集合(zset)实现优先级队列

  • 原生 Scrapy:内存 FIFO 普通队列,无分布式、无持久化
  • 替换为 SpiderPriorityQueue
    1. 请求存入 Redis zset,支持 priority 优先级
    2. 分布式爬虫多机共享待爬队列
    3. 爬虫重启不会丢失请求

scrapy-redis 三种队列可选

  • ① 优先级队列(最常用) SCHEDULER_QUEUE_CLASS = "scrapy_redis.queue.SpiderPriorityQueue"   底层 Redis:zset,支持 Request(priority=xxx) 自定义权重,数字越大优先级越高。
  • ② FIFO 先进先出队列。SCHEDULER_QUEUE_CLASS = "scrapy_redis.queue.SpiderQueue"   底层 Redis:list,纯顺序队列,不支持优先级。
  • ③ LIFO 后进先出(栈模式)。SCHEDULER_QUEUE_CLASS = "scrapy_redis.queue.SpiderStack"  底层 Redis:list,深度优先抓取。

配套完整必要配置(settings.py)

# 启用Redis调度器
SCHEDULER = "scrapy_redis.scheduler.Scheduler"
# 去重依赖Redis
DUPEFILTER_CLASS = "scrapy_redis.dupefilter.RFPDupeFilter"
# 你的优先级队列
SCHEDULER_QUEUE_CLASS = "scrapy_redis.queue.SpiderPriorityQueue"

# Redis连接配置
REDIS_HOST = "127.0.0.1"
REDIS_PORT = 6379
# 有密码则加 REDIS_PASSWORD = "xxx"

# 爬虫关闭不清空Redis队列(断点续爬关键)
SCHEDULER_PERSIST = True

使用优先级示例(spider 代码)

yield scrapy.Request(
    url="https://xxx.com",
    callback=self.parse,
    priority=10  # 数值越大,越先被调度
)

示例:修改后的配置文件中与 scrapy-redis 有关的部分,middleware、proxy 等在此略过。

# 指定使用 scrapy-redis 的 Scheduler
SCHEDULER = "scrapy_redis.scheduler.Scheduler"

# 指定使用 scrapy-redis 的 RFPDupeFilter
DUPEFILTER_CLASS = "scrapy_redis.dupefilter.RFPDupeFilter"

# 在 redis 中保持 scrapy-redis 用到的各个队列,从而允许暂停和暂停后恢复
SCHEDULER_PERSIST = True

# 指定排序爬取地址时使用的队列,默认是按照优先级排序
SCHEDULER_QUEUE_CLASS = "scrapy_redis.queue.SpiderPriorityQueue"
# SCHEDULER_QUEUE_CLASS = "scrapy_redis.queue.SpiderQueue"
# SCHEDULER_QUEUE_CLASS = "scrapy_redis.queue.SpiderStack"

ITEM_PIPELINES = {
    'MyScrapyRedis.pipelines.ExamplePipeline': 300,
    'MyScrapyRedis.pipelines.MyRedisPipeline': 400,
    # 'scrapy_redis.pipelines.RedisPipeline': 400,
}

LOG_LEVEL = 'DEBUG'

# 指定redis的连接参数
REDIS_HOST = '127.0.0.1'
REDIS_PORT = 6379
REDIS_PARAMS = {}
# REDIS_URL = 'redis://user:pass@hostname:9001' # 连接URL (优先于以上配置) 源码可以看到

新建 pipeline

继承 scrapy_redis 的 pipeline

# Define your item pipelines here
#
# Don't forget to add your pipeline to the ITEM_PIPELINES setting
# See: http://doc.scrapy.org/topics/item-pipeline.html

import json
from datetime import datetime
from scrapy_redis.pipelines import RedisPipeline


class ExamplePipeline(object):

    def process_item(self, item, spider):
        item["crawled"] = str(datetime.now().replace(microsecond=0))
        item["spider"] = spider.name
        return item


class MyRedisPipeline(RedisPipeline):

    def _process_item(self, item, spider):
        key = self.item_key(item, spider)
        # data = self.serialize(item)
        self.server.rpush(key, json.dumps(item, ensure_ascii=False))
        return item

也可以不用重写,通过在 setting.py 里面配置 REDIS_ITEMS_SERIALIZER = 'json.dumps' 即可使用 json 序列化 通过查看 scrapy-redis 的 pipelines.py 可以知道:scrapy-redis 默认使用 ScrapyJSONEncoder 进行项目序列化  

参考:https://www.cnblogs.com/Alexephor/p/11446167.html

写入 MongoDB

Item Pipeline:https://docs.scrapy.org/en/latest/topics/item-pipeline.html

管道除了验证数据,还可以将数据保存到数据库中。这时候仅仅一个process_item(self, item, spider)函数就不够了。所以操作数据库的管道还应该包含几个函数用于建立和关闭数据库连接。
下面的例子也是 scrapy 官方文档的例子,演示了持久化数据管道的用法。这个管道是从类方法from_crawler(cls, crawler)中初始化出来的,该方法实际上读取了scrapy的配置文件。这和直接将数据库连接写在代码中相比,是更加通用的方式。初始化和关闭数据库连接的操作都在对应的方法中执行。

import pymongo

class MongoPipeline(object):

    collection_name = 'scrapy_items'

    def __init__(self, mongo_uri, mongo_db):
        self.mongo_uri = mongo_uri
        self.mongo_db = mongo_db

    @classmethod
    def from_crawler(cls, crawler):
        return cls(
            mongo_uri=crawler.settings.get('MONGO_URI'),
            mongo_db=crawler.settings.get('MONGO_DATABASE', 'items')
        )

    def open_spider(self, spider):
        self.client = pymongo.MongoClient(self.mongo_uri)
        self.db = self.client[self.mongo_db]

    def close_spider(self, spider):
        self.client.close()

    def process_item(self, item, spider):
        self.db[self.collection_name].insert_one(dict(item))
        return item

使用 "文件、图片" 管道

除了自己编写管道之外,scrapy 还预定义了几个管道,可以帮助我们方便的保存文件和图片。这些管道有以下特点:

  • 可以避免重复下载最近的文件。
  • 指定文件保存位置(文件系统或者亚马逊S3)

对于图片管道来说还有额外功能:

  • 将图片转换成常见格式(JPG)和模式(RGB)
  • 生成图片缩略图
  • 只下载大于某长宽的图片

使用文件管道的过程如下:

  1. 首先需要Item类中有file_urls和files两个属性,然后在爬虫中将想爬取的文件地址放到file_urls属性中,然后返回
  2. 在Item传递到文件管道的时候,调度程序会用下载器将地址对应的文件下载下来,将文件属性(包括保存路径等)放到files属性中,file_urls和files中是一一对应的

使用图片管道的过程是相似的,不过要操作的属性是image_urls和images。

如果你不想使用这几个属性,其实属性名也是可以修改的,需要修改下面四个属性。

FILES_URLS_FIELD = 'field_name_for_your_files_urls'
FILES_RESULT_FIELD = 'field_name_for_your_processed_files'
IMAGES_URLS_FIELD = 'field_name_for_your_images_urls'
IMAGES_RESULT_FIELD = 'field_name_for_your_processed_images'

要启用文件管道和图片管道,同样需要激活,当然如果同时激活这两个管道也是可行的。

ITEM_PIPELINES = {'scrapy.pipelines.images.ImagesPipeline': 1}
# 或者
ITEM_PIPELINES = {'scrapy.pipelines.files.FilesPipeline': 1}

文件和图片保存位置需要分别指定。

FILES_STORE = '/path/to/valid/dir'
IMAGES_STORE = '/path/to/valid/dir'

文件和图片管道可以避免下载最近的文件,对应的文件过期时间也可以配置,单位是天。

# 120 days of delay for files expiration
FILES_EXPIRES = 120

# 30 days of delay for images expiration
IMAGES_EXPIRES = 30

图片管道可以在保存图片的时候同时生成缩略图,缩略图配置是一个字典,键是缩略图的名字,值是缩略图长和宽。

IMAGES_THUMBS = {
    'small': (50, 50),
    'big': (270, 270),
}

最后图片会保存成下面这样,图片的文件名是图片路径的SHA1哈希值。

/图片保存路径/full/完整图片.jpg
/图片保存路径/thumbs/small/小图片.jpg
/图片保存路径/thumbs/big/中图片.jpg

如果不想使用SHA1哈希值作为文件名,可以继承ImagesPipeline基类并重写file_path函数,这里是另外一位简书作者的爬虫项目,他重写了这个函数。我们可以作为参考。

如果要过滤小图片,启用下面的配置。默认情况下对图片尺寸没有约束,所以所有图片都会下载。

IMAGES_MIN_HEIGHT = 110
IMAGES_MIN_WIDTH = 110

默认情况下文件和图片管道不支持重定向,遇到需要重定向的链接意味着下载失败,不过我们也可以启用重定向。

MEDIA_ALLOW_REDIRECTS = True

添加任务

scrapy-redis "初始任务Redis队列" 和 "Scheduler 内部 Redis 队列" 是两个不同概念

  • "初始任务Redis队列" 是系统入口,保存外部投递的 URL/JSON 任务。初始任务被取出后,并不会直接交给 Downloader,而是先转换成 Request,再进入 Scheduler 队列。
  • "Scheduler 内部 Redis 队列" 则是保存已经转换成 Scrapy Request、等待下载的请求。

添加任务到 redis 的 list 中:

通过命令添加 urls 到 redis:redis-cli lpush myspider:start_urls https://baidu.com

通过代码 添加任务到 redis 的 list 中

import json
from scrapy.utils.project import get_project_settings
from scrapy_redis.connection import get_redis_from_settings
from scrapy_redis import connection
from scrapy_redis.queue import PriorityQueue


# def _encode_request(self, request):
#     """Encode a request object"""
#     obj = request_to_dict(request, self.spider)
#     return self.serializer.dumps(obj)
#
#
# def _decode_request(self, encoded_request):
#     """Decode an request previously encoded"""
#     obj = self.serializer.loads(encoded_request)
#     return request_from_dict(obj, self.spider)


def add_task_to_redis():
    redis_key = 'start_urls:yy_spider_request'

    url_string = 'http://www.youyuan.com/find/beijing/mm18-25/advance-0-0-0-0-0-0-0/p1/'

    # 方法 1
    server = get_redis_from_settings(get_project_settings())
    server.lpush(redis_key, url_string)
    # server.zadd(redis_key, {url_string: 1000})

    # 方法 2
    # server = connection.from_settings(get_project_settings())
    # server.execute_command('ZADD', redis_key, 1000, url_string)


if __name__ == '__main__':
    # temp = 'test json string'
    # print(json.dumps(temp))
    add_task_to_redis()
    pass

添加 "json 格式的任务"  到 redis 的 set、zset 中

import json
import time
from datetime import datetime
from scrapy_redis.connection import get_redis

name = "example"
redis_key_set = f'redis_key:{name}'
redis_key_zset = f'redis_key:{name}_zset'


redis_config = {
    'host': '127.0.0.1',
    'port': 6379,
    'db': 0
}
redis_conn = get_redis(**redis_config)

url_list = [
    'https://www.51tietu.net/xiaohua/'
]


def add_task_to_set():
    for index in range(1, 10):
        url = f'https://www.51tietu.net/xiaohua/{index}'
        url_list.append(url)
    for url in url_list:
        redis_conn.sadd(redis_key_set, json.dumps({'url': url}, ensure_ascii=False))
        print(f'add url ---> {url}')


def add_task_to_zset():
    for index in range(1, 10):
        url = f'https://www.51tietu.net/xiaohua/{index}'
        url_list.append(url)
    for url in url_list:
        redis_conn.execute_command(
            'ZADD',
            redis_key_zset,
            # 用 15 位时间戳作为 score
            int(datetime.now().timestamp() * 100000),
            json.dumps({'url': url}, ensure_ascii=False)
        )
        print(f'add url ---> {url}')


if __name__ == '__main__':
    add_task_to_set()
    add_task_to_zset()
    pass

把失败任务再次添加到 队列中

import time
from scrapy import cmdline, spiders, Request
from scrapy.utils.project import get_project_settings
from redis.cluster import RedisCluster, ClusterNode

################################################################################
project_settings = get_project_settings()
redis_cluster_config = project_settings.get('REDIS_CLUSTER_CONFIG')
node_list = redis_cluster_config['node_list']
redis_startup_nodes = [
    ClusterNode(node['host'], node['port']) for node in node_list
]
################################################################################
redis_queue_mapping = {
    # '失败的队列': '要映射到的任队列',
    'spider_task:amazon_spu_fail': 'spider_task:amazon_spu',
    'spider_task:amazon_spu_anti': 'spider_task:amazon_spu',
    'spider_task:amazon_spu_404': 'spider_task:amazon_spu',
}
################################################################################


class SpiderMonitorQueue(spiders.Spider):
    name = 'monitor_redis_queue'

    custom_settings = {
        "CONCURRENT_REQUESTS": 1,
        "DOWNLOAD_DELAY": 1,
        'LOG_FILE': None,
        # 'LOG_LEVEL': 'INFO'
    }

    def __init__(self, *args, **kwargs):
        super(SpiderMonitorQueue, self).__init__(*args, **kwargs)
        self.redis_pipeline = None
        self.server = None

    def __del__(self):
        self.server.close()

    def parse(self, response, **kwargs):
        pass

    def get_redis_server(self):
        self.server = RedisCluster(
            startup_nodes=redis_startup_nodes,
            decode_responses=True
        )
        self.redis_pipeline = self.server.pipeline()

    def rename_key_in_cluster(self, old_key=None, new_key=None):
        is_exists = self.server.exists(new_key)
        if not is_exists:
            data_string_list = list(self.server.smembers(old_key))
            list_len = len(data_string_list)
            for index in range(list_len):
                data_string = data_string_list[index]
                if 0 == index % 5000:
                    self.redis_pipeline.execute()
                self.redis_pipeline.sadd(new_key, data_string)
            self.redis_pipeline.execute()
            self.server.delete(old_key)
            self.logger.info(f'{old_key} ---> {new_key}')
        pass

    def start_requests(self):
        try:
            self.get_redis_server()
            while True:
                try:
                    self.server.ping()
                except Exception as e:
                    self.get_redis_server()
                for old_key, new_key in redis_queue_mapping.items():
                    self.rename_key_in_cluster(old_key=old_key, new_key=new_key)
                self.logger.info('10分钟后再次扫描 redis 队列')
                time.sleep(60 * 10)
                pass
        except Exception as e:
            yield Request('https://httpbin.org/')
            print(e)


if __name__ == '__main__':
    # cmdline.execute(f'scrapy crawl {SpiderMonitorQueue.name}'.split())
    cmdline.execute(['scrapy', 'crawl', SpiderMonitorQueue.name])
    pass

运行 爬虫

  • 方式 1:scrapy crawl list  查看所有 爬虫,scrapw crawl 爬虫名 运行爬虫
  • 方式 2:运行:scrapy runspider example/spiders/myspider_redis.py
  • 方式 3:直接执行上面的 example.py

这里直接执行上面的 example.py。运行结果:

redis 中保存的 结果如下:

代理 中间件

启用 DownLoader 中间件

DOWNLOADER_MIDDLEWARES = {
    'MyScrapyRedis.middlewares.ProxyMiddleware': 400,
}

scrapy内置了14个下载器中间件,

详情参考文档。如果希望禁用某些内置的中间件,可以将值设置为 None

编写自己的下载器中间件

自定义下载器中间件应该继承 scrapy.downloadermiddlewares.DownloaderMiddleware 类,该类有如下几个方法,用于操纵请求和响应,我们只要重写这几个方法即可。这几个方法的作用请参考 官方文档 ( https://doc.scrapy.org/en/latest/topics/downloader-middleware.html ),它们比较复杂,所以我就不说了。

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

创建 middlewares.py 并编辑 (settings.py 同级目录)

import base64
import redis
from queue import Queue


class ProxyMiddleware(object):
    def __init__(self, settings):
        self.queue = 'Proxy:queue'
        # 初始化代理列表
        self.redis_conn = redis.Redis(
            host=settings.get('REDIS_HOST'),
            port=settings.get('REDIS_PORT'),
            db=1,
            password=settings.get('REDIS_PARAMS')['password']
        )

    @classmethod
    def from_crawler(cls, crawler):
        return cls(crawler.settings)

    def process_request(self, request, spider):
        proxy_config = {}
        source, data = self.redis_conn.blpop(self.queue)
        proxy_config['ip_port'] = data
        proxy_config['user_password'] = None

        if proxy_config['user_password']:           
            request.meta['proxy'] = f"http://{proxy_config['ip_port']}"
            # user_password = "USERNAME:PASSWORD"
            encoded_user_pass = base64.encodestring(proxy_config['user_password'])
            request.headers['Proxy-Authorization'] = f'Basic {encoded_user_pass}'
            print(f"********ProxyMiddleware have pass {proxy_config['ip_port']}*****")
        else:
            # ProxyMiddleware no password
            print(request.url, proxy_config['ip_port'])
            request.meta['proxy'] = f"http://{proxy_config['ip_port']}"

    def process_response(self, request, response, spider):
        """
            检查 response.status, 根据 status 是否在允许的状态码中决定是否切换到下一个 proxy, 或者禁用 proxy
        """
        print("-------%s %s %s------" % (request.meta["proxy"], response.status, request.url))
        # status不是正常的200而且不在spider声明的正常爬取过程中可能出现的
        # status列表中, 则认为代理无效, 切换代理
        if response.status == 200:
            print('rpush', request.meta["proxy"])
            self.redis_conn.rpush(self.queue, request.meta["proxy"].replace('http://', ''))
        return response

    def process_exception(self, request, exception, spider):
        """
            处理由于使用代理导致的连接异常
        """
        proxy_config = {}
        source, data = self.r.blpop(self.queue)
        proxy_config['ip_port'] = data
        proxy_config['user_password'] = None

        request.meta['proxy'] = f"http://{proxy_config['ip_port']}"
        new_request = request.copy()
        new_request.dont_filter = True
        return new_request

有这么一个场景,有些请求是不需要代理IP,怎么才能让它请求超时的时候,再使用代理池的IP地址进行重新请求呢?

  • 1、scrapy的基本请求步骤是,首先执行父类里面(scrapy.Spider)里面的start_requests方法,
  • 2、然后start_requests方法也是取拿我们设置的start_urls变量里面的url地址
  • 3、最后才执行make_requests_from_url方法,并只传入一个url变量

那么,我们就可以重写make_requests_from_url方法,从而直接调用scrapy.Request()方法。参数说明

  • 1、url=url,其实就是最后start_requests()方法里面拿到的url地址
  • 2、meta这里我们只设置了一个参数,download_timeout:10,作用就是当第一次发起请求的时候,等待10秒钟,如果没有请求成功的话,就会直接执行download_middleware里面的方法,我们下面介绍。
  • 3、callback回调函数,其实就是本次的本次所有操作完成后执行的操作,注意,这里可不是说执行完上面所有操作后,再执行这个操作,比如说请求了一个url,并且成功了,下面就会执行这个方法。
  • 4、dont_filter=False,这个很重要,有人说过不加的话默认就是False,但是亲测必须得加,作用就是scrapy默认有去重的方法,等于False的话就意味着不参加scrapy的去重操作。亲测,请求一个页面,拿到第一个页面后,抓取想要的操作后,第二页就不行了,只有加上它才可以。
import scrapy
 
class HttpbinTestSpider(scrapy.Spider):
    name = "httpbin_test"
    allowed_domains = ["httpbin.ort/get"]
    start_urls = ['http://httpbin.org/get']
 
    def make_requests_from_url(self,url):
        self.logger.debug('Try first time')
        return scrapy.Request(
            url=url,
            meta={'download_timeout':10},
            callback=self.parse,
            dont_filter=False
        )
 
    def parse(self, response):
        print(response.text)


class HttpbinProxyMiddleware(object):
    logger = logging.getLogger(__name__)
 
    # def process_request(self, request, spider):
    #     # pro_addr = requests.get('http://127.0.0.1:5000/get').text
    #     # request.meta['proxy'] = 'http://' + pro_addr
    #     pass
    #
    # def process_response(self, request, response, spider):
    #     # 可以拿到下载完的response内容,然后对下载完的内容进行修改等操作。
    #     pass
 
    def process_exception(self, request, response, spider):
        self.logger.debug('Try Exception time')
        self.logger.debug('Try second time')
        proxy_addr = requests.get('http://127.0.0.1:5000/get').text
        self.logger.debug(proxy_addr)
        request.meta['proxy'] = 'http://{0}'.format(proxy_addr)


在scrapy中的中间件里面,对应的中间件后面的数字越小,执行优先级越高。
DOWNLOADER_MIDDLEWARES = {
   'httpbin.middlewares.HttpbinProxyMiddleware': 543,
   #设置不参与scrapy的自动重试的动作
   'scrapy.downloadermiddlewares.retry.RetryMiddleware':None
}

实际爬虫过程中如果请求过于频繁,通常会被临时重定向到登录页面即302,甚至是提示禁止访问即403,因此可以对这些响应执行一次代理请求:
(1) 参考原生redirect.py 模块,满足dont_redirect 或handle_httpstatus_list 等条件时,直接传递response
(2) 不满足条件(1),如果响应状态码为 302 或 403,使用代理重新发起请求
(3) 使用代理后,如果响应状态码仍为 302 或 403,直接丢弃

from w3lib.url import safe_url_string
from six.moves.urllib.parse import urljoin
from scrapy.exceptions import IgnoreRequest


class MyAutoProxyDownloaderMiddleware(object):

    def __init__(self, settings):
        self.proxy_status = settings.get('PROXY_STATUS', [302, 403])
        self.proxy_config = settings.get('PROXY_CONFIG', 'http://username:password@some_proxy_server:port')

    @classmethod
    def from_crawler(cls, crawler):
        return cls(settings=crawler.settings)

    def process_response(self, request, response, spider):
        req_meta = request.meta
        if (req_meta.get('dont_redirect', False) or
                response.status in getattr(spider, 'handle_httpstatus_list', []) or
                response.status in req_meta.get('handle_httpstatus_list', []) or
                req_meta.get('handle_httpstatus_all', False)):
            return response

        if response.status in self.proxy_status:
            if 'Location' in response.headers:
                location = safe_url_string(response.headers['location'])
                redirected_url = urljoin(request.url, location)
            else:
                redirected_url = ''

                # AutoProxy for first time
                if not request.meta.get('auto_proxy'):
                    request.meta.update({'auto_proxy': True, 'proxy': self.proxy_config})
                    new_request = request.replace(meta=request.meta, dont_filter=True)
                    new_request.priority = request.priority + 2

                    spider.log(f'Will AutoProxy for <{response.status} {request.url}> {redirected_url}')
                    return new_request

                # IgnoreRequest for second time
                else:
                    spider.logger.warn(
                        '忽略 response <{response.status} {request.url}>: 代理后status_code 仍在{self.proxy_status}')
                    raise IgnoreRequest

            return response

项目 settings.py 添加代码,注意必须在默认的 RedirectMiddleware 和 HttpProxyMiddleware 之间。

DOWNLOADER_MIDDLEWARES = {
    # 'scrapy.downloadermiddlewares.redirect.RedirectMiddleware': 600,
    'my_middlewares.MyAutoProxyDownloaderMiddleware': 601,
    # 'scrapy.downloadermiddlewares.httpproxy.HttpProxyMiddleware': 750,    
}
PROXY_STATUS = [302, 403]
PROXY_CONFIG = 'http://username:password@some_proxy_server:port'

示例:

class HttpProxymiddleware(object):

    # 一些异常情况汇总
    EXCEPTIONS_TO_CHANGE = (
        defer.TimeoutError, TimeoutError, ConnectionRefusedError, ConnectError, ConnectionLost,
        TCPTimedOutError, ConnectionDone)

    def __init__(self):
        self.redis = redis.from_url(
		    'redis://:你的密码@localhost:6379/0', decode_responses=True
        )
        pass

    def process_request(self, request, spider):
        #拿出全部 key,随机选取一个键值对
        keys = self.rds.hkeys("xila_hash")
        key = random.choice(keys)
        #用eval函数转换为dict
        proxy = eval(self.rds.hget("xila_hash",key))
        logger.warning(f"{str(proxy)}使用中")
        #将代理 ip 和 key 存入 mate
        request.meta["proxy"] = proxy["ip"]
        request.meta["accountText"] = key

    def process_response(self, request, response, spider):
        http_status = response.status
        #根据response的状态判断 ,200的话ip的times +1重新写入数据库,返回response到下一环节
        if http_status == 200:
            key = request.meta["accountText"]
            proxy = eval(self.rds.hget("xila_hash",key))
            proxy["times"] = proxy["times"] + 1
            self.rds.hset("xila_hash",key,proxy)
            return response
        #403有可能是因为user-agent不可用引起,和代理ip无关,返回请求即可
        elif http_status == 403:
            logging.warning("403重新请求中")
            return request.replace(dont_filter=True)
        #其他情况姑且被判定ip不可用,times小于10的,删掉,大于等于10的暂时保留
        else:
            ip = request.meta["proxy"]
            key = request.meta["accountText"]
            proxy = eval(self.rds.hget("xila_hash", key))
            if proxy["times"] < 10:
                self.rds.hdel("xila_hash",key)
            logging.warning(f"{ip}不可用")
            return request.replace(dont_filter=True)

    def process_exception(self, request, exception, spider):
        #其他一些timeout之类异常判断后的处理,ip不可用删除即可
        if isinstance(exception, self.EXCEPTIONS_TO_CHANGE) \
                and request.meta.get('proxy', False):
            key = request.meta["accountText"]
            print("{key}不可用, 将被删除")
            proxy = eval(self.rds.hget("xila_hash", key))
            if proxy["times"] < 10:
                self.rds.hdel("xila_hash", key)
            logger.debug(f"Proxy {request.meta['proxy']}链接出错{exception}")
            return request.replace(dont_filter=True)

伪造 User-Agent

fake-useragent 下载 和 使用方法:https://pypi.org/project/fake-useragent

方法 1:

在setting.py文件中加入以下内容,这是一些浏览器的头信息

USER_AGENT_LIST = [
    "Avant Browser/1.2.789rel1 (http://www.avantbrowser.com)",
    "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/534.27 (KHTML, like Gecko) Chrome/12.0.712.0 Safari/534.27",
    "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/13.0.782.24 Safari/535.1",
    "Mozilla/5.0 (Windows NT 6.0) AppleWebKit/535.2 (KHTML, like Gecko) Chrome/15.0.874.120 Safari/535.2",
    "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/535.7 (KHTML, like Gecko) Chrome/16.0.912.36 Safari/535.7",
    "Mozilla/5.0 (Windows; U; Windows NT 6.0 x64; en-US; rv:1.9pre) Gecko/2008072421 Minefield/3.0.2pre",
    "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.0.10) Gecko/2009042316 Firefox/3.0.10",
    "Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US; rv:1.9.1.6) Gecko/20091201 Firefox/3.5.6 GTB5",
    "Mozilla/5.0 (Windows NT 6.1; rv:2.0.1) Gecko/20100101 Firefox/4.0.1",
    "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:2.0.1) Gecko/20100101 Firefox/4.0.1",
    "Mozilla/5.0 (Windows NT 5.1; rv:5.0) Gecko/20100101 Firefox/5.0",
    "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:6.0a2) Gecko/20110622 Firefox/6.0a2",
    "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:7.0.1) Gecko/20100101 Firefox/7.0.1",
    "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:2.0b4pre) Gecko/20100815 Minefield/4.0b4pre",
    "Mozilla/4.0 (compatible; MSIE 5.5; Windows NT 5.0 )",
    "Mozilla/4.0 (compatible; MSIE 5.5; Windows 98; Win 9x 4.90)",
    "Mozilla/5.0 (Windows; U; Windows XP) Gecko MultiZilla/1.6.1.0a",
]

在 spider 同级目录下建立一个 MidWare 文件价里面写一个 HeaderMidWare.py 文件 内容为

# encoding: utf-8
from scrapy.utils.project import get_project_settings
import random
 
settings = get_project_settings()
 
class ProcessHeaderMidware():
    """process request add request info"""
 
    def process_request(self, request, spider):
        """
        随机从列表中获得header, 并传给user_agent进行使用
        """
        ua = random.choice(settings.get('USER_AGENT_LIST'))  
        spider.logger.info(msg='now entring download midware')
        if ua:
            request.headers['User-Agent'] = ua
            # Add desired logging message here.
            spider.logger.info(u'User-Agent is : {} {}'.format(request.headers.get('User-Agent'), request))
        pass

在 setting.py 文件中添加

DOWNLOADER_MIDDLEWARES = {
    'projectName.MidWare.HeaderMidWare.ProcessHeaderMidware': 543,
}

方法 2:使用 fake_userAgent

fake_userAgent  github:https://github.com/sea1234/fake-useragent

安装 fake_userAgent:pip install fake-useragent

from fake_useragent import UserAgent
import requests
 
 
ua = UserAgent()
print(ua.ie)       #ie浏览器的user agent
print(ua.opera)    #opera浏览器
print(ua.chrome)   #chrome浏览器
print(ua.firefox)  #firefox浏览器
print(ua.safari)   #safri浏览器
 
#最常用的方式
#写爬虫最实用的是可以随意变换headers,一定要有随机性。支持随机生成请求头
print(ua.random)
print(ua.random)
print(ua.random)
 
#####################################################
 
#请求的网址
url="http://www.baidu.com"
 
#请求头
headers={"User-Agent":ua.random}
 
#请求网址
response=requests.get(url=url,headers=headers)
 
#响应体内容
print(response.text)
 
#响应状态信息
print(response.status_code)
 
#响应头信息
print(response.headers)

user_agent_middlewares.py

# -*- coding: utf-8 -*-
from fake_useragent import UserAgent
 
class RandomUserAgentMiddlware(object):
    #随机跟换user-agent
    def __init__(self,crawler):
        super(RandomUserAgentMiddlware,self).__init__()
        self.ua = UserAgent()
        self.ua_type = crawler.settings.get('RANDOM_UA_TYPE','random')#从setting文件中读取RANDOM_UA_TYPE值
 
    @classmethod
    def from_crawler(cls,crawler):
        return cls(crawler)
 
    def process_request(self,request,spider):  ###系统电泳函数
        def get_ua():
            return getattr(self.ua,self.ua_type)
        # user_agent_random=get_ua()
        request.headers.setdefault('User_Agent',get_ua())
        pass

在 setting.py 中添加

RANDOM_UA_TYPE = 'random'  ##random    chrome
DOWNLOADER_MIDDLEWARES = { 
    'projectName.MidWare.user_agent_middlewares.RandomUserAgentMiddlware': 543,  
    'scrapy.downloadermiddlewares.useragent.UserAgentMiddleware':None,
}

cookie 池

基于 Scrapy-Redis 的分布式以及 cookies 池:https://cuiqingcai.com/4048.html

爬虫 中间件

官网文档:https://docs.scrapy.org/en/latest/topics/spider-middleware.html

编写自己的爬虫中间件需要继承 scrapy.spidermiddlewares.SpiderMiddleware 基类,并重写以下几个方法。

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

内置的爬虫中间件

scrapy 内置了5个爬虫中间件。

这里仅介绍一两个。

  • DepthMiddleware:该中间件记录了爬虫爬取请求地址的深度。我们可以使用DEPTH_LIMIT来指定爬虫爬取的深度。
  • UrlLengthMiddleware:该中间件会过滤掉超出最大允许长度的URL,爬虫不会访问这些超长URL。最大长度通过URLLENGTH_LIMIT配置来指定,默认值是2083。URLLENGTH_LIMIT = 2083

内建服务 ( 日志、邮件、web服务 )

scrapy 学习笔记(1、2、3)

scrapy内置了几个服务,可以让我们使用scrapy更加方便。

日志:爬虫类定义了 log 函数,我们可以方便的在爬虫类中记录日志。

import scrapy

class MySpider(scrapy.Spider):

    name = 'myspider'
    start_urls = ['https://scrapinghub.com']

    def parse(self, response):
        self.logger.info('Parse function called on %s', response.url)

日志相关的配置,点击可以跳转到官方文档查看详细信息。

发送电子邮件:有时候我们可能希望爬到一定数量的数据就发送电子邮件进行提醒。scrapy也内置了这个功能。我们可以通过构造函数参数来创建邮件发送器。

from scrapy.mail import MailSender
mailer = MailSender(这里是构造函数参数)

也可以从配置文件实例化。

mailer = MailSender.from_settings(settings)

然后调用send方法就可以发送邮件了。

mailer.send(
    to=["someone@example.com"], 
    subject="Some subject", 
    body="Some body", 
    cc=["another@example.com"]
)

电子邮件相关配置参考官方文档

web 服务:这个功能本来是写在官方文档内建服务条目下的,但是实际上这个功能已经变成了一个单独的项目,需要额外安装。pip install scrapy-jsonrpc

然后在扩展中包含这个功能。

EXTENSIONS = {
    'scrapy_jsonrpc.webservice.WebService': 500,
}

还需要在配置中启用该功能。

JSONRPC_ENABLED = True

然后在爬虫运行的时候访问 http://localhost:6080/crawler 即可查看爬虫运行情况了。

该项目的其他配置查看其官方文档

优化爬虫 ( 增大并发数、增大线程池、降低日志级别 等)

爬虫项目可以通过修改一些配置进行优化。

设置禁止跳转(code=301、302)、请求超时

DOWNLOAD_TIMEOUT = 10
REDIRECT_ENABLED = False

增大并发数:并发数可以通过下面的配置进行设置。具体的并发数需要根据服务器的CPU等设置来进行更改。一般来说服务器CPU使用在80%-90%之间利用率比较高。我们可以从并发数100开始反复进行测试。

CONCURRENT_REQUESTS = 100

增大线程池:scrapy 通过一个线程池来进行 DNS 查询,增大这个线程池一般也可以提高 scrapy 性能。

REACTOR_THREADPOOL_MAXSIZE = 20

降低日志级别:默认情况下scrapy使用debug级别来打印日志,通过降低日志级别,我们可以减少日志打印,从而提高程序运行速度。

LOG_LEVEL = 'INFO'

禁用 Cookie:如果不是必须的,我们可以通过禁用Cookie来提高性能。如果需要登录用户才能爬取数据,不要禁用Cookie。

COOKIES_ENABLED = False

关闭重试:频繁重试可能导致目标服务器响应缓慢,我们自己访问不了别人也访问不了。所以可以考虑关闭重试。

RETRY_ENABLED = False

减少下载超时:如果网络连接比较快的话,我们可以减少下载超时,让爬虫卡住的请求中跳出来,一般可以提高爬虫效率。

DOWNLOAD_TIMEOUT = 15

关闭重定向:如果不是必要的话,我们可以关闭重定向来提高爬虫性能。

REDIRECT_ENABLED = False         关掉重定向,不会重定向到新的地址
HTTPERROR_ALLOWED_CODES = [302,] 返回302时,按正常返回对待,可以正常写入cookie

或者启用下载中间件
#'scrapy.contrib.downloadermiddleware.redirect.RedirectMiddleware': 600,

自动调整爬虫负载:scrapy有一个扩展可以自动调节服务器负载,它通过一个算法来确定最佳的爬虫延时等设置。它的文档在这里

相关配置如下,点击链接可以跳转到对应文档。

编辑器中的调试

在页面的任意位置添加如下代码

from scrapy.shell import inspect_response
def paser(self,response):
    inspect_response(response,self)     #当程序运行到这里就会跳出终端,并且在终端出现调试命令,当然这个可以随便写在哪里

暂停、恢复 爬虫

初学者最头疼的事情就是没有处理好异常,当爬虫爬到一半的时候突然因为错误而中断了,但是这时又不能从中断的地方开始继续爬,顿时感觉心里日了狗,但是这里有一个方法可以暂时的存储你爬的状态,当爬虫中断的时候继续打开后依然可以从中断的地方爬,不过虽说持久化可以有效的处理,但是要注意的是当使用cookie临时的模拟登录状态的时候要注意cookie的有效期

只需要在setting.pyJOB_DIR=file_name 其中填的是你的文件目录,注意这里的目录不允许共享,只能存储单独的一个spdire的运行状态,如果你不想在从中断的地方开始运行,只需要将这个文件夹删除即可

当然还有其他的放法:scrapy crawl somespider -s JOBDIR=crawls/somespider-1,这个是在终端启动爬虫的时候调用的,可以通过ctr+c中断,恢复还是输入上面的命令

部署爬虫

官方文档介绍了两种部署爬虫的方式,可以将爬虫部署到服务器上远程执行。第一种是通过Scrapyd开源项目来部署,也是这里要介绍的方式。第二种是通过scrapy公司提供的商业收费版服务Scrapy Cloud部署,推荐有财力的公司考虑。

服务器端:首先服务器需要安装scrapyd包,如果是Linux系统还可以考虑使用对应的包管理器来安装。

pip install scrapyd
apt-get install scrapyd

然后运行scrapyd服务,如果使用系统包管理器安装,那么可能已经配置好了systemd文件。

scrapyd
# 或者
systemctl enable scrapyd

scrapyd 附带了一个简单的 web 界面可以帮助我们查看爬虫运行情况,默认情况下访问http://localhost:6800/ 来查看这个界面。

scrapyd 的配置文件可以是~/.scrapyd.conf或者/etc/scrapyd/scrapyd.conf。下面是一个简单配置,绑定所有端口,这样一来从任意位置都可以访问web界面。

[scrapyd]
bind_address = 0.0.0.0

scrapyd的功能可以查看其 API文档

客户端:客户端如果要上传爬虫,可以通过服务器API的端点addversion.json来实现,或者安装一个简便工具scrapyd-client

首先安装客户端工具:pip install scrapyd-client

这个客户端目前好像有bug,在windows下运行scrapy-deploy命令不会直接执行,而是弹出一个文件关联对话框。如果你遇到这种情况,可以找到Python安装路径下的脚本路径(例如C:\Program Files\Python36\Scripts),然后编写一个scrapyd-deploy.bat批处理文件,内容如下。这样就可以正常运行了。

@"c:\program files\python36\python.exe" "c:\program files\python36\Scripts\scrapyd-deploy" %*

然后切换到项目路径,编辑项目全局配置文件scrapy.cfg,添加部署路径。

[deploy]
url = http://192.168.64.136:6800/
project = quotesbot

然后直接运行scrapy-deploy命令,就可以看到项目已经成功部署到服务器上了。

运行爬虫需要使用scrapyd的API,例如使用curl,可以用下面的命令。

 curl http://192.168.64.136:6800/schedule.json -d project=quotesbot -d spider=toscrape-css

或者使用Jetbrains 系列IDE 2017.3的基于编辑器的HTTP客户端。

然后点击Jobs就可以看到爬虫已经开始运行了。如果要查看状态,点击右边的log即可。

以上就是scrapy的进阶介绍了,利用这些功能,我们可以编写更加实用的爬虫,并将它们部署到服务器上。

URL 过滤 ( Bloom Filter、Hyperloglog )

正常业务逻辑下,爬虫不会对重复请求爬取两次。所以爬虫默认都会对重复请求进行过滤,但当爬虫体量达到千万级时,默认的过滤器占用的内存将会远远超乎你的想象。
为了解决这个问题,可以通过一些算法来牺牲一点点过滤的准确性来换取更小的空间复杂度

  • Bloom Filter:可以用于检索一个元素是否在一个集合中。它的优点是空间效率和查询时间都远远超过一般的算法,缺点是有一定的误识别率和删除困难。
  • Hyperloglog:是一个基数估计算法。其空间效率非常高,1.5K内存可以在误差不超过2%的前提下,用于超过10亿的数据集合基数估计。

这两种算法都是合适的选择,以 Hyperloglog 为例:由于 redis 已经提供了支持 hyperloglog 的数据结构,所以只需对此数据结构进行操作即可

基于 Scrapy-redis 的分布式爬虫设计:https://www.jianshu.com/p/cd4054bbc757/

# coding: utf-8
from scrapy import Item, Field
from scrapy.spiders import Rule
from scrapy_redis.spiders import RedisCrawlSpider
from scrapy.linkextractors import LinkExtractor
from redis import Redis
from time import time
from urllib.parse import urlparse, parse_qs, urlencode


class MasterSpider(RedisCrawlSpider):
    name = 'ebay_master'
    redis_key = 'ebay:start_urls'

    ebay_main_lx = LinkExtractor(allow=(r'http://www.ebay.com/sch/allcategories/all-categories', ))
    ebay_category2_lx = LinkExtractor(allow=(r'http://www.ebay.com/sch/[^\s]*/\d+/i.html',
                                             r'http://www.ebay.com/sch/[^\s]*/\d+/i.html?_ipg=\d+&_pgn=\d+',
                                             r'http://www.ebay.com/sch/[^\s]*/\d+/i.html?_pgn=\d+&_ipg=\d+',))

    rules = (
        Rule(ebay_category2_lx, callback='parse_category2', follow=False),
        Rule(ebay_main_lx, callback='parse_main', follow=False),
    )

    def __init__(self, *args, **kwargs):
        domain = kwargs.pop('domain', '')
        # self.allowed_domains = filter(None, domain.split(','))
        super(MasterSpider, self).__init__(*args, **kwargs)

    def parse_main(self, response):
        pass
        data = response.xpath("//div[@class='gcma']/ul/li/a[@class='ch']")
        for d in data:
            try:
                item = LinkItem()
                item['name'] = d.xpath("text()").extract_first()
                item['link'] = d.xpath("@href").extract_first()
                yield self.make_requests_from_url(item['link'] + r"?_fsrp=1&_pppn=r1&scp=ce2")
            except:
                pass

    def parse_category2(self, response):
        data = response.xpath("//ul[@id='ListViewInner']/li/h3[@class='lvtitle']/a[@class='vip']")
        redis = Redis()
        for d in data:
            # item = LinkItem()
            try:
                self._filter_url(redis, d.xpath("@href").extract_first())

            except:
                pass
        try:
            next_page = response.xpath("//a[@class='gspr next']/@href").extract_first()
        except:
            pass
        else:
            # yield self.make_requests_from_url(next_page)
            new_url = self._build_url(response.url)
            redis.lpush("test:new_url", new_url)
            # yield self.make_requests_from_url(new_url)
            # yield Request(url, headers=self.headers, callback=self.parse2)

    def _filter_url(self, redis, url, key="ebay_slave:start_urls"):
        is_new_url = bool(redis.pfadd(key + "_filter", url))
        if is_new_url:
            redis.lpush(key, url)


    def _build_url(self, url):
        parse = urlparse(url)
        query = parse_qs(parse.query)
        base = parse.scheme + '://' + parse.netloc + parse.path

        if '_ipg' not in query.keys() or '_pgn' not in query.keys() or '_skc' in query.keys():
            new_url = base + "?" + urlencode({"_ipg": "200", "_pgn": "1"})
        else:
            new_url = base + "?" + urlencode({"_ipg": query['_ipg'][0], "_pgn": int(query['_pgn'][0]) + 1})
        return new_url


class LinkItem(Item):
    name = Field()
    link = Field()

当 redis.pfadd() 执行时,一个 url 尝试插入 hyperloglog 结构中,如果 url 存在返回 0,反之返回 1。由此来判断是否要将该 url 存放至待爬队列

集成 bloomfilter 到 scrapy-redis 中

scrapy_redis去重优化(已有7亿条数据):https://blog.csdn.net/bone_ace/article/details/53099042
将bloomfilter(布隆过滤器)集成到scrapy-redis中:https://www.cnblogs.com/adc8868/p/7442306.html

模拟用户登录

有时候需要模拟用户登录,这时候可以使用 FormRequest.from_response 方法。这时候爬虫功能稍有变化,parse 函数用来发送用户名和密码,抽取数据的操作放在回调函数中进行。

import scrapy

class LoginSpider(scrapy.Spider):
    name = 'example.com'
    start_urls = ['http://www.example.com/users/login.php']

    def parse(self, response):
        return scrapy.FormRequest.from_response(
            response,
            formdata={'username': 'john', 'password': 'secret'},
            callback=self.after_login
        )

    def after_login(self, response):
        # 检查是否登录成功
        if "authentication failed" in response.body:
            self.logger.error("Login failed")
            return

        # 在这里继续爬取数据

模拟登陆过程中遇到的那些坑:https://blog.csdn.net/amaomao123/article/details/52511882

动态 JavaScript 渲染、无头浏览器 playwright-python

https://docs.scrapy.org/en/latest/topics/dynamic-content.html

scrapy-redis 无任务时自动退出 / 打印提示实现方案

2、集群版 scrapy-redis

scrapy-redis-cluster

scrapy_redis_cluster ( 已经不在维护 ):https://github.com/thsheep/scrapy_redis_cluster
scrapy-redis-cluster :https://pypi.org/project/scrapy-redis-cluster

scrapy-redis-cluster 已经不在维护 !!!!!

scrapyd-redis 的集群版

  • 此包Python名称:scrapy-redis-cluster
  • 目前版本: scrapy-redis-cluster 0.4
  • 最后维护时间:Jul 5, 2018
  • 摘要:scrapyd-redis的集群版
  • 安装命令:pip install scrapy-redis-cluster
  • 其它:scrapy-redis-cluster 这个Python第三方库的作者没有提供更多的项目描述信息了,2019-11-10 23:44:14。

正常情况单机的redis可以满足scrapy-redis进行分布式爬取,可是如果单机的redis的内存过小,很容易导致系统内存不够,读取数据缓慢,如果使用docker运行redis,更加可能导致redis的容器的进程被杀掉。使用 redis 集群可以增加 redis 集体内存,防止出现上面的情况。scrapy redis-cluster 很简单,只需要按照以下步骤:

1. 安装库:pip install scrapy-redis-cluster

2. 修改 settings 文件

# Redis集群地址
REDIS_MASTER_NODES = [
    {"host": "192.168.10.233", "port": "30001"},
    {"host": "192.168.10.234", "port": "30002"},
    {"host": "192.168.10.235", "port": "30003"},
]

# 使用的哈希函数数,默认为6  
BLOOMFILTER_HASH_NUMBER = 6

# Bloomfilter使用的Redis内存位,30表示2 ^ 30 = 128MB,默认为22 (1MB 可去重130W URL)  
BLOOMFILTER_BIT = 22

# 不清空redis队列  
SCHEDULER_PERSIST = True  
# 调度队列  
SCHEDULER = "scrapy_redis_cluster.scheduler.Scheduler"  
# 去重 
DUPEFILTER_CLASS = "scrapy_redis_cluster.dupefilter.RFPDupeFilter"  
# queue  
SCHEDULER_QUEUE_CLASS = 'scrapy_redis_cluster.queue.PriorityQueue'

scrapy-redis-sentinel

scrapy-redis-sentinel :https://github.com/crawlaio/scrapy-redis-sentinel
pypi 地址:https://pypi.org/project/scrapy-redis-sentinel/
基于原项目 scrpy-redis:https://github.com/rmax/scrapy-redis

进行修改,修改内容如下:

  1. 添加了 Redis 哨兵连接支持
  2. 添加了 Redis 集群连接支持
  3. 添加了 Bloomfilter 去重

安装第三方库:pip install scrapy-redis-sentinel

原版本 scrpy-redis 的所有配置都支持。优先级:哨兵模式 > 集群模式 > 单机模式

配置示例

# ----------------------------------------Bloomfilter 配置-------------------------------------
# 使用的哈希函数数,默认为 6
BLOOMFILTER_HASH_NUMBER = 6

# Bloomfilter 使用的 Redis 内存位,30 表示 2 ^ 30 = 128MB,默认为 30   (2 ^ 22 = 1MB 可去重 130W URL)
BLOOMFILTER_BIT = 30

# 是否开启去重调试模式 默认为 False 关闭
DUPEFILTER_DEBUG = False

# ----------------------------------------Redis 单机模式-------------------------------------
# Redis 单机地址
REDIS_HOST = "172.25.2.25"
REDIS_PORT = 6379

# REDIS 单机模式配置参数
REDIS_PARAMS = {
    "password": "password",
    "db": 0
}

# ----------------------------------------Redis 哨兵模式-------------------------------------

# Redis 哨兵地址
REDIS_SENTINELS = [
    ('172.25.2.25', 26379),
    ('172.25.2.26', 26379),
    ('172.25.2.27', 26379)
]

# REDIS_SENTINEL_PARAMS 哨兵模式配置参数。
REDIS_SENTINEL_PARAMS= {
    "service_name":"mymaster",
    "password": "password",
    "db": 0
}

# ----------------------------------------Redis 集群模式-------------------------------------

# Redis 集群地址
REDIS_STARTUP_NODES = [
    {"host": "172.25.2.25", "port": "6379"},
    {"host": "172.25.2.26", "port": "6379"},
    {"host": "172.25.2.27", "port": "6379"},
]

# REDIS_CLUSTER_PARAMS 集群模式配置参数
REDIS_CLUSTER_PARAMS= {
    "password": "password"
}

# ----------------------------------------Scrapy 其他参数-------------------------------------

# 在 redis 中保持 scrapy-redis 用到的各个队列,从而允许暂停和暂停后恢复,也就是不清理 redis queues
SCHEDULER_PERSIST = True  
# 调度队列  
SCHEDULER = "scrapy_redis_sentinel.scheduler.Scheduler"  
# 去重 
DUPEFILTER_CLASS = "scrapy_redis_sentinel.dupefilter.RFPDupeFilter"  

# 指定排序爬取地址时使用的队列
# 默认的 按优先级排序( Scrapy 默认),由 sorted set 实现的一种非 FIFO、LIFO 方式。
# SCHEDULER_QUEUE_CLASS = 'scrapy_redis_sentinel.queue.SpiderPriorityQueue'
# 可选的 按先进先出排序(FIFO)
# SCHEDULER_QUEUE_CLASS = 'scrapy_redis_sentinel.queue.SpiderStack'
# 可选的 按后进先出排序(LIFO)
# SCHEDULER_QUEUE_CLASS = 'scrapy_redis_sentinel.queue.SpiderStack'

注:当使用集群时单机不生效

spiders 使用

原版本 scrpy-redis 使用方式

from scrapy_redis.spiders import RedisSpider

class Spider(RedisSpider):
    ...

修改 RedisSpider 引入方式后,scrapy-redis-sentinel 的使用方式

from scrapy_redis_sentinel.spiders import RedisSpider

class Spider(RedisSpider):
    ...

使用示例:

修改 setting.py文件

ITEM_PIPELINES = { 
    'scrapy_redis_sentinel.pipelines.RedisPipeline': 543,
}
 
# Bloomfilter 配置
# 使用的哈希函数数,默认为 6
BLOOMFILTER_HASH_NUMBER = 6
 
# Bloomfilter 使用的 Redis 内存位,30 表示 2 ^ 30 = 128MB,默认为 30   (2 ^ 22 = 1MB 可去重 130W URL)
BLOOMFILTER_BIT = 30
 
# 是否开启去重调试模式 默认为 False 关闭
DUPEFILTER_DEBUG = False
 
# Redis 集群地址
REDIS_MASTER_NODES = [
    {"host": "192.168.56.30", "port": "9000"},
    {"host": "192.168.56.31", "port": "9000"},
    {"host": "192.168.56.32", "port": "9000"},
]
 
# REDIS_CLUSTER_PARAMS 集群模式配置参数
REDIS_CLUSTER_PARAMS= {
    # "password": "password"
}
 
# scrapy其他参数
# 在 redis 中保持 scrapy-redis 用到的各个队列,从而允许暂停和暂停后恢复,也就是不清理 redis queues
SCHEDULER_PERSIST = True
# 调度队列
SCHEDULER = "scrapy_redis_sentinel.scheduler.Scheduler"
# 去重
DUPEFILTER_CLASS = "scrapy_redis_sentinel.dupefilter.RFPDupeFilter"
 
# 指定排序爬取地址时使用的队列
# 默认的 按优先级排序( Scrapy 默认),由 sorted set 实现的一种非 FIFO、LIFO 方式。
SCHEDULER_QUEUE_CLASS = 'scrapy_redis_sentinel.queue.SpiderPriorityQueue'
# 可选的 按先进先出排序(FIFO)
# SCHEDULER_QUEUE_CLASS = 'scrapy_redis_sentinel.queue.SpiderStack'
# 可选的 按后进先出排序(LIFO)
# SCHEDULER_QUEUE_CLASS = 'scrapy_redis_sentinel.queue.SpiderStack'

修改 spider

from scrapy_redis_sentinel.spiders import RedisSpider
 
class scrapy_spider(RedisSpider):
    ......

Redis 集群( Redis5.0.7集群搭建:https://blog.csdn.net/pcengineercn/article/details/104502061

经过调试,修复了一个bug使用默认爬取队列时会报错,需要将源码中的 PriorityQueue(位于 Python 安装目录 /lib/python3.6/site-packages/scrapy_redis_sentinel/queue.py)替换为如下

class PriorityQueue(Base):
    """Per-spider priority queue abstraction using redis' sorted set"""
 
    def __len__(self):
        """Return the length of the queue"""
        return self.server.zcard(self.key)
 
    def push(self, request):
        """Push a request"""
        data = self._encode_request(request)
        score = -request.priority
        # We don't use zadd method as the order of arguments change depending on
        # whether the class is Redis or StrictRedis, and the option of using
        # kwargs only accepts strings, not bytes.
        self.server.execute_command("ZADD", self.key, score, data)
 
    def pop(self, timeout=0):
        """
        Pop a request
        timeout not support in this queue class
        """
        if not isinstance(self.server, RedisCluster):
            # use atomic range/remove using multi/exec
            pipe = self.server.pipeline()
            pipe.multi()
            pipe.zrange(self.key, 0, 0).zremrangebyrank(self.key, 0, 0)
            results, count = pipe.execute()
            if results:
                return self._decode_request(results[0])
 
        # 使用集群的时候不能使用 multi/exec 来完成一个事务操作;使用lua脚本来实现类似功能
        pop_lua_script = """
        local result = redis.call('zrange', KEYS[1], 0, 0)
        local element = result[1]
        if element then
            redis.call('zremrangebyrank', KEYS[1], 0, 0)
            return element
        else
            return nil
        end
        """
        script = self.server.register_script(pop_lua_script)
        results = script(keys=[self.key])
        if results:
            return self._decode_request(results)

改造 scrapy-redis

改造 scrapy-redis 支持 redis 集群。把 xxx 换成自己对应文件名

REDIS_START_URLS_AS_ZSET = True
SCHEDULER = "xxx.spiders.xxx.MyScheduler"
DUPEFILTER_CLASS = "xxx.spiders.xxx.MyRFPDupeFilter"
# SCHEDULER_QUEUE_CLASS = "xxx.spiders.xxx.MyPriorityQueue"
SCHEDULER_QUEUE_CLASS = "scrapy_redis.queue.FifoQueue"

改造代码

import importlib
from scrapy.http import Response
from scrapy import FormRequest, signals
from scrapy_redis.spiders import RedisSpider
from redis.cluster import RedisCluster, ClusterNode, PRIMARY
from scrapy.utils.misc import load_object
from scrapy_redis import defaults
from scrapy_redis.scheduler import Scheduler
from scrapy_redis.queue import PriorityQueue
from scrapy_redis.dupefilter import RFPDupeFilter

redis_cluster_config = {
    "skip_full_coverage_check": True,
    "node_list": [
        {"host": "192.168.9.5", "port": 7000},
        {"host": "192.168.9.6", "port": 7000},
        {"host": "192.168.9.7", "port": 7000},
        # 添加更多节点
    ],
    "decode_responses": True,
}

node_list = redis_cluster_config['node_list']
redis_startup_nodes = [
    ClusterNode(node['host'], node['port']) for node in node_list
]


class MyRFPDupeFilter(RFPDupeFilter):

    @classmethod
    def from_spider(cls, spider):
        settings = spider.settings
        server = RedisCluster(startup_nodes=redis_startup_nodes)
        dupefilter_key = settings.get(
            "SCHEDULER_DUPEFILTER_KEY",
            defaults.SCHEDULER_DUPEFILTER_KEY
        )
        key = dupefilter_key % {"spider": spider.name}
        debug = settings.getbool("DUPEFILTER_DEBUG")
        return cls(server, key=key, debug=debug)


class MyScheduler(Scheduler):

    @classmethod
    def from_settings(cls, settings):
        kwargs = {
            "persist": settings.getbool("SCHEDULER_PERSIST"),
            "flush_on_start": settings.getbool("SCHEDULER_FLUSH_ON_START"),
            "idle_before_close": settings.getint("SCHEDULER_IDLE_BEFORE_CLOSE"),
        }
        optional = {
            "queue_key": "SCHEDULER_QUEUE_KEY",
            "queue_cls": "SCHEDULER_QUEUE_CLASS",
            "dupefilter_key": "SCHEDULER_DUPEFILTER_KEY",
            "dupefilter_cls": "DUPEFILTER_CLASS",
            "serializer": "SCHEDULER_SERIALIZER",
        }
        for name, setting_name in optional.items():
            val = settings.get(setting_name)
            if val:
                kwargs[name] = val
        dupefilter_cls = load_object(kwargs["dupefilter_cls"])
        if not hasattr(dupefilter_cls, "from_spider"):
            kwargs["dupefilter"] = dupefilter_cls.from_settings(settings)
        if isinstance(kwargs.get("serializer"), str):
            kwargs["serializer"] = importlib.import_module(kwargs["serializer"])
        server = RedisCluster(startup_nodes=redis_startup_nodes)
        server.ping()
        return cls(server=server, **kwargs)


class SpiderBase(RedisSpider):
    name = "spider_base"

    def __init__(self, *args, **kwargs):
        super(SpiderBase, self).__init__(*args, **kwargs)
        self.fetch_data = None
        self.count_size = None
        self.server = None

    def setup_redis(self, crawler=None):

        if self.server is not None:
            return

        settings = crawler.settings
        if self.redis_batch_size is None:
            self.redis_batch_size = settings.getint("CONCURRENT_REQUESTS", 16)
        self.redis_batch_size = int(self.redis_batch_size)
        if self.redis_encoding is None:
            self.redis_encoding = settings.get("REDIS_ENCODING", "utf-8")

        self.logger.info(
            f"redis key '{self.redis_key}', batch size: {self.redis_batch_size}, encoding: {self.redis_encoding}")

        self.server = RedisCluster(
            startup_nodes=redis_startup_nodes,
            decode_responses=True
        )

        if settings.getbool("REDIS_START_URLS_AS_SET", False):
            self.fetch_data = self.server.spop
            self.count_size = self.server.scard
        elif settings.getbool("REDIS_START_URLS_AS_ZSET", False):
            self.fetch_data = self.pop_priority_queue
            self.count_size = self.server.zcard
        else:
            self.fetch_data = self.pop_list_queue
            self.count_size = self.server.llen

        if self.max_idle_time is None:
            self.max_idle_time = settings.get("MAX_IDLE_TIME_BEFORE_CLOSE", 0)
        self.max_idle_time = int(self.max_idle_time)
        crawler.signals.connect(self.spider_idle, signal=signals.spider_idle)

    def parse(self, response: Response, **kwargs):
        resp_text = response.text
        resp_status = response.status
        print(f'[{resp_status}] ----> {response.url}')


if __name__ == '__main__':
    pass

3、反检测 浏览器

github 搜索:https://github.com/search?q=Anti-detect+browser&type=repositories

camoufox

github:https://github.com/daijro/camoufox

Camoufox 是一款开源的反检测浏览器,专为网络爬虫和 AI Agent 而设计。

Camoufox 是 Firefox 的一个分支,专为网页抓取和人工智能代理而设计。它采用无头模式,难以被检测到,并针对大规模运行进行了优化。每次运行都会根据真实世界的设备分布生成一个全新的身份,因此它能够融入正常的网络流量中,而不会显得格格不入。

  • 专为人工智能代理打造🤖
    • 精简版 Firefox——启动速度快,运行成本低
    • 通过 Python 接口实现 Playwright 的即插即用兼容性
    • 对反机器人系统不可见,因此您可以将代理集群运行在本地或云端而不会被标记。
  • 精心设计,难以察觉🎭
    • 页面自动化功能已隐藏,无法通过 JavaScript 检测发现。更多详情请参阅隐蔽页面。
  • 指纹注入和旋转(无需JS注入!)
    • 所有导航器属性(设备、操作系统、硬件、浏览器等)✅
    • 屏幕尺寸、分辨率、窗口和视口属性 ✅
    • 地理位置、时区、语言环境和国际欺骗 ✅
    • WebRTC 协议级 IP 地址欺骗 ✅
    • 语音、语音播放速度等。✅
    • 还有更多精彩内容!
  • 反图形指纹识别
    • WebGL 参数、支持的扩展、上下文属性和着色器精度格式 ✅
    • 字体欺骗和反指纹识别 ✅
  • 针对自动化进行了优化
    • 类似人类的鼠标移动🖱️
    • 屏蔽和绕过广告🛡️
    • 不使用 CSS 动画 💨
  • 精简并优化内存效率⚡
  • 用于更新和自动指纹注入的PyPi 包📦
  • 始终保持与最新 Firefox 版本同步 🕓

donutbrowser

github:https://github.com/zhom/donutbrowser 

简单而强大的反检测浏览器

  • 无限浏览器配置文件:每个配置文件都完全隔离,拥有自己的指纹、Cookie、扩展程序和数据。
  • 反检测 Chromium 引擎:由Wayfern提供支持,Wayfern 是一个注重隐私的 Chromium 分支,其指纹欺骗技术不会被 Cloudflare、reCaptcha v3 或其他浏览器指纹识别和反机器人服务检测到。
  • DNS广告拦截器:通过基于配置文件的DNS拦截功能,屏蔽广告、追踪器和其他不需要的内容。
  • 代理支持:每个配置文件支持 HTTP、HTTPS、SOCKS4、SOCKS5,并支持动态代理 URL
  • VPN 支持:每个配置文件均可配置 WireGuard
  • 本地 API 和 MCP:用于与 Claude、自动化工具和自定义工作流集成的REST API 和模型上下文协议服务器
  • 配置文件组:组织配置文件并应用批量设置
  • 导入配置文件:从 Chrome、Edge、Brave 或其他 Chromium 内核浏览器迁移
  • Cookie 和扩展程序管理:导入/导出 Cookie,按用户配置文件管理扩展程序
  • 默认浏览器:将 Donut 设置为默认浏览器,并选择哪个配置文件打开每个链接
  • 云同步:跨设备同步配置文件、代理和组(可自托管)
  • 端到端加密:可选的端到端加密同步,密码只有您知道。
  • 零遥测:无追踪或设备指纹识别

CloakBrowser

github:https://github.com/CloakHQ/CloakBrowser 

CloakBrowser 是源码级打补丁的 Chromium 反检测浏览器,专门用于爬虫 / 自动化,用来绕过 Cloudflare Turnstile、reCAPTCHA v3、Datadome 等机器人检测。和 playwright‑stealthundetected‑chromedriver 的本质区别:不是 JS 注入 Hook,直接修改 Chromium C++ 源码编译二进制,指纹底层一致性更高,不容易被反爬探测出自动化痕迹。

项目Playwright + playwright‑stealthundetected‑chromedriverCamoufoxCloakBrowser
补丁层级JS 注入 Hook配置 / 启动参数C++(Firefox)C++(Chromium)
reCAPTCHA v3 分数0.10.3‑0.70.7‑0.90.9
Cloudflare Turnstile经常失败偶尔过可过稳定可过
APIPlaywrightSelenium自有 API原生 Playwright API
浏览器内核ChromiumChromiumFirefoxChromium

注意:免费版锁定旧 Chromium 版本;最新 Chromium 补丁为 Pro 付费版;上一个大版本永久免费开源。CloakBrowser 是源代码级指纹补丁,30/30 测试全部通过。

  • 71 个源代码级 C++ 补丁——画布、WebGL、音频、字体、GPU、屏幕、WebRTC、网络时序、自动化信号、CDP 输入行为
  • humanize=True— 类似人类的鼠标曲线、键盘操作时机和滚动模式。一个标志,行为检测通过
  • 优点:reCAPTCHA v3 得分 0.9 分——达到人类水平,服务器验证
  • 通过 Cloudflare Turnstile、FingerprintJS 和 BrowserScan 的检测——已在 30 多个检测网站上进行测试。
  • 自动下载正确的二进制文件——根据您的许可证,免费版或专业版均可。
  • pip install cloakbrowser或者npm install cloakbrowser——二进制自动下载,零配置
  • 最新二进制文件,免费试用——使用 GitHub 登录,将最新版本应用于最棘手的目标,Pro 版本可扩展至数千个会话。

核心原理

  • 普通 playwright‑stealth:运行时注入 JS,修改navigator等对象,JS 层可以被网站检测到补丁痕迹,Chrome 大版本更新容易失效。
  • CloakBrowser:修改 Chromium C++ 源码,58 + 处底层指纹补丁:Canvas、WebGL、Audio、WebRTC、GPU、屏幕、字体、自动化标记、CDP 行为信号全部底层修改。每次启动自动生成一套自洽完整指纹(UA、时区、分辨率、硬件信息全部匹配)。对外提供完全兼容 Playwright 的 API,几乎零改动迁移现有 Playwright 爬虫代码。提供humanize=True开关:模拟真人鼠标曲线、键盘输入延迟、滚动行为,对抗行为风控。

别再用 playwright-stealth 了!CloakBrowser 源码级反检测才是正解:https://juejin.cn/post/7639006640004186131

codex 接入 CloakBrowser 指纹浏览器操作爬虫自动化抓取

fingerprint-browser

GitHub:https://github.com/zhaotoday/fingerprint-browser

r0chrome

r0ysue 大佬的 r0chrome:https://github.com/CrackerCat/r0chrome

其他

lightpanda:https://lightpanda.io/

  • AI 专用浏览器。用于自动化、爬虫和AI Agent的快速轻量级浏览器引擎。支持JavaScript执行,无图形渲染。

bb-browser:https://github.com/epiral/bb-browser/blob/main/README.zh-CN.md

  • 坏孩子浏览器 BadBoy Browser。浏览器就是 API。互联网是为浏览器构建的。AI Agent 一直试图通过 API 访问它 — 但 99% 的网站不提供 API。bb-browser 翻转了这个逻辑:不是让网站适配机器,而是让机器使用人的界面。 adapter 在你的浏览器 tab 里跑 eval,用你的 Cookie 调 fetch(),或者直接调用页面的 webpack 模块。网站以为是你在操作。因为就是你
Playwright / Selenium爬虫库bb-browser
浏览器无头、隔离环境没有浏览器你的真实 Chrome
登录态没有,要重新登录偷 Cookie已经在了
反爬检测容易被识别猫鼠游戏无法检测 — 它就是用户
复杂鉴权无法复制需要逆向页面自己处理

4、pypi 中其他 scrapy 框架

pypi 搜索 scrapy:https://pypi.org/search/?q=scrapy

sd-spider-utils

爬虫工具包,写爬虫更快。

安装:pip install sd-spider-utils

安装:pip install "sd_spider_utils[all]"

AyugeSpiderTools

scrapy 扩展库:用于扩展 Scrapy 功能来解放双手。

安装:pip install ayugespidertools[all]

pypi:https://pypi.org/project/AyugeSpiderTools/

在使用 Scrapy 开发爬虫时,免不了会重复地编写 settings,items,middlewares,pipelines 和入库前的去重更新等一些通用方法,但各项目中的这些内容都大致相同,那为何不把它们统一整理在一起呢?我也想扩展一些功能,比如当 spider 中添加字段后,不用再修改对应的 item 和 pipeline。

项目的主旨是让开发者只需专注于 spider 脚本的编写,减少开发和维护流程。理想状态下,只需关注 spider 中字段的解析规则和 VIT 下的 .conf 配置即可,脱离无意义的重复操作

ayuge version    # 查看库版本
ayuge startproject <project_name>    # 创建项目
cd <project_name>    # 进入项目根目录

# 替换(覆盖)为真实的配置 .conf 文件:
# 这里是为了演示方便,正常情况是直接在 VIT 中的 .conf 文件填上你需要的配置即可
cp /root/mytemp/.conf DemoSpider/VIT/.conf

# 生成爬虫脚本
ayuge genspider <spider_name> <example.com>

# 运行脚本
scrapy crawl <spider_name>
# 注:也可以使用 ayuge crawl <spider_name>

具体的场景案例请在 DemoSpider 项目中查看,也可以在 readthedocs 文档中查看教程。

功能

  • [✓] scrapy 的扩展功能场景

    • [✓] scrapy 脚本运行信息统计和项目依赖表采集量统计,可用于日志记录和预警

    • [✓] 自定义模板,在 ayuge startproject <projname> 和 ayuge genspider <spidername> 时生成适合本库的模板文件

    • [✓] 从远程应用管理服务中获取项目配置

      • [✓] 从 consul 获取项目配置

      • [✓] 从 nacos 获取项目配置(注意:优先级小于 consul)

    • [✓] 代理中间件(独享代理、动态隧道代理)

    • [✓] 随机请求头 UA 中间件,根据 fake_useragent 中的权重来随机

    • [✓] 使用以下工具来替换 scrapy 的 Request 来发送请求

      • [✓] requests: 这个不推荐使用,requests 同步库会降低 scrapy 运行效率(已移除此功能,更推荐 aiohttp 的方式)

      • [✓] aiohttp: 集成将 scrapy Request 替换为 aiohttp 的协程方式

    • [✓] Mysql 存储的场景下适配

      • [✓] 自动创建 Mysql 用户场景下需要的数据库和数据表及字段格式,还有字段注释

    • [✓] MongoDB 存储场景适配

    • [✓] PostgreSQL 存储场景适配

    • [✓] ElasticSearch 存储场景适配

    • [✓] Oracle 存储场景适配

    • [✓] oss 上传场景适配

    • [✓] asyncio 语法支持与 async 第三方库支持示例

      • [✓] spider 中使用 asyncio 的 aiohttp 示例

      • [✓] pipeline 中使用 asyncio 的 aioMysql 示例

    • [✓] 集成 Kafka,RabbitMQ 等数据推送功能

  • [✓] 常用开发场景

    • [✓] sql 语句拼接,只用于简单场景。

    • [✓] 数据格式化处理,比如:去除网页标签,去除无效空格等

    • [✓] 字体反爬还原方法

      • [✓] 基于 ttf,woff 之类的字体文件映射,或结合 css 等实现

        • [✓] 可以直接在字体文件 xml 中找到映射关系的:使用 fontforge 工具导出映射即可。

        • [✓] 无法找到映射关系的,则一般使用 ocr 识别(准确率非百分百),通过 fontforge 导出每个映射的png,后再通过各种方式识别。

      • [✓] 字体反爬部分功能迁移到 FontMapster 项目中。

    • [✓] html 数据处理,去除标签,不可见字符,特殊字符改成正常显示等

    • [✓] 添加常用的图片验证码中的处理方法

      • [✓] 滑块缺口距离的识别方法(多种实现方式)

      • [✓] 根据滑块距离生成轨迹数组的方法

      • [✓] 识别点选验证码位置及点击顺序

      • [✓] 图片乱序混淆的还原方法示例

注意:功能演示我将放入 readthedocs 文档中,以防此部分内容过多。

modis-crawler-utils

Scrapy 实用工具。

  • MongoDBPipeline 用于将数据保存到 MongoDB 中的工具/流程。
  • KafkaPipeline 用于将数据推送到 Kafka 的管道工具。
  • OpenSearch : OpenSearchRequestsDownloaderMiddleware 将请求与响应组合成一个数据项,然后再将其发送给 OpenSearch。

scraping

scraping 是自适应网络爬虫框架:pip install "scrapling[all]"

github:https://github.com/D4Vinci/Scrapling/blob/main/docs/README_CN.md

scraped 是 scraping 的 工具包:pip install scraped

omnicrawl-core (重要)

无所不能的爬虫框架。整合 Scrapling + curl_cffi + Playwright,绕过 WAF,LLM 友好输出。

pypi:https://pypi.org/project/omnicrawl-core/

安装:pip install omnicrawl-core

从源码安装:
git clone https://github.com/Meteorkid/omnicrawl.git
cd omnicrawl

# 创建虚拟环境
python3.12 -m venv .venv
source .venv/bin/activate

# 安装(基础版,包含 curl_cffi + Scrapling)
pip install -e .

# 安装浏览器(Browser/Stealth 模式需要)
playwright install chromium
patchright install chromium

# 安装 Camoufox(51job 等强反爬站点需要)
pip install -e ".[camoufox]"
camoufox fetch

现有的爬虫库各有局限:

问题
requestsTLS 指纹暴露,被 WAF 秒封
httpx同上,HTTP/2 指纹也暴露
Selenium自动化痕迹明显,资源消耗大
Playwright无内置爬虫逻辑,无反检测
Scrapling功能强大但 API 复杂

OmniCrawl 的定位:把这些库的最佳能力整合到一个统一 API 中,自动选择最优方案。

核心特性

特性说明
🔒 TLS 指纹伪装37+ 浏览器指纹(Chrome/Safari/Firefox/Edge/Tor),绕过 JA3/JA4 检测
🛡️ WAF 绕过阿里云 WAF、Cloudflare、Akamai 等专用策略配置
🦊 CamoufoxFirefox 原生反检测浏览器,绕过 JS 环境检测(51job 等强反爬站点)
🔄 自动降级HTTP → Browser → Camoufox → Stealth,被封时自动切换
🌐 代理轮换支持轮询/随机/加权策略,内置代理健康检查
⏱️ 智能限速基于域名的自适应延时,被封自动退避,成功自动恢复
📝 LLM 输出自动去除导航/广告/脚本,输出干净 Markdown,Token 计数
🕷️ Spider 框架类 Scrapy 架构,支持并发控制、流式输出
🔄 失败重试指数退避重试,每次重试自动轮换 TLS 指纹

示例:

from omnicrawl.spider import Spider
from omnicrawl.spider.base import SpiderItem
from omnicrawl.fetchers.base import FetchResult

class MySpider(Spider):
    name = "my_spider"
    start_urls = ["https://news.ycombinator.com/"]
    max_concurrent = 4
    download_delay = 1.0

    async def parse(self, response: FetchResult):
        # 解析页面,yield 采集项
        from omnicrawl.parser import HTMLParser
        parser = HTMLParser(response.html)
        titles = parser.css_all(".titleline > a::text")
        for title in titles:
            yield SpiderItem(data={"title": title}, url=response.url)

# 运行
spider = MySpider()
items = await spider.run()
print(f"采集到 {len(items)} 条数据")

# 流式输出
async for item in spider.stream():
    print(item.data)

scrapy-calyprium

安装:pip install scrapy-calyprium

反检测 Scrapy 中间件,用于网络爬取的反检测 Scrapy 中间件。

借助 Calyprium 技术实现代理路由和隐蔽式浏览器渲染功能。

  • VeilProxyMiddleware——通过使用 TLS 指纹识别技术,让请求经过多个代理服务器进行路由处理。
  • MimicBrowserMiddleware——利用隐形的浏览器实例来渲染 JavaScript 页面
  • S3 存储功能——利用 Scrapy 内置的功能,将爬虫处理后的数据写入 Calyprium 存储系统中。

curl-cffi

curl_cffi 是 curl 最受欢迎的 Python 实现方式。与 httpx 、 requests 等其他纯 Python 实现的 HTTP 客户端不同, curl_cffi 能够模拟浏览器的 TLS/JA3 以及 HTTP/2 协议特征。如果你因为某些不明原因被某个网站阻止了访问,可以试试使用 curl_cffi 。

安装:pip install curl-cffi

scrapy-cffi

受 Scrapy 启发的异步式网络爬虫框架,后端基于 curl_cffi 实现。

安装:​pip install scrapy_cffi
安装:pip install "scrapy_cffi[kafka]"

主要特点

  • Scrapy 式架构:spiders, items, interceptors, pipelines, signals
  • 完全基于异步 I/O 的引擎,可实现最高程度的并发处理
  • 支持 HTTP 和 WebSocket 协议:内置异步客户端功能
  • 灵活的数据库集成:支持 Redis、MySQL、PostgreSQL、MongoDB 等数据库。具备单次连接失败后的异步重连功能,同时支持各种原生 IDE 所支持的客户端类型。
  • 消息队列调度:RabbitMQ 和 Kafka(Kafka 中的启动/工作主题需单独处理,并需手动确认)
  • 可配置的部署方式:该系统支持 .env 模式、单实例模式、哨兵模式和集群模式。
  • scrapy-cffi infra :为 Redis、MySQL、PostgreSQL、MongoDB、RabbitMQ 和 Kafka 等数据库/服务,构建并管理独立的本地 Docker 基础设施。
  • 专为高并发、高可用性的爬取任务而设计。

scrapy-curl-cffi

Scrapy 与 curl_cffi(curl-impersonate)的集成。

pypi:https://pypi.org/project/scrapy-curl-cffi/

安装:pip install scrapy-curl-cffi

scrapy-fake-tls

利用 curl_cffi 实现 TLS 指纹伪装功能的 Scrapy 下载处理程序

pypi:https://pypi.org/project/scrapy-fake-tls/ 

scrapy-playwright

Scrapy 集成 playwright

安装:pip install scrapy-playwright

安装浏览器:playwright install

import scrapy

class AwesomeSpider(scrapy.Spider):
    name = "awesome"

    async def start(self):  # start_requests in Scrapy < 2.13
        # GET request
        yield scrapy.Request("https://httpbin.org/get", meta={"playwright": True})
        # POST request
        yield scrapy.FormRequest(
            url="https://httpbin.org/post",
            formdata={"foo": "bar"},
            meta={"playwright": True},
        )

    def parse(self, response, **kwargs):
        # 'response' contains the page as seen by the browser
        return {"url": response.url}

astrocrawl

AstroCrawl 是基于 Playwright 无头 Chromium 的全功能异步网页爬虫,约 29,490 行 Python,113 源文件,内置 3 个 AI Provider,103 测试文件 0 测试用例。支持 JavaScript 渲染、robots.txt 遵从(RFC 9309)、Sitemap 自动发现、代理轮换、内容去重、崩溃恢复,以及声明式 CSS 选择器提取规则引擎、通用插件系统和多 Provider AI 基础设施。提供 PySide6 GUI 图形界面和功能完整的 CLI 命令行两种使用方式。

能力说明
JavaScript 渲染Playwright 无头 Chromium,完整执行页面 JS,支持 CDP 健康检测
robots.txt 遵从RFC 9309 完整实现 — data/policy 分离,始终获取 robots.txt,Disallow 拦截按需开关,Crawl-Delay 独立控制
站点地图发现自动从 robots.txt / 默认路径发现 Sitemap,递归解析 Sitemap Index,UrlGate 统一准入
结构化提取声明式 CSS 选择器规则引擎 — MatchScope 4 级精确度,字段级提取 + 5 变换流水线,3 层规则源(用户/远程/预置)
Schema.org 提取零成本自动解析 JSON-LD 和 Microdata 结构化数据,所有页面默认执行
AI 辅助规则生成双路径:外部 AI ChatML 粘贴导入 / GUI 一键 API 调用,零样本 Prompt,共享 3 级 HTML 预处理
AI 多 Provider 底座3 Provider(OpenAI/Anthropic/Google)、多 Profile 管理、C-mode 上下文选择、流式/工具调用/嵌入
代理轮换4 种代理模式 + 3 级断路器 + TCP 主动探测 + DomainPathMemory 双缓存 + ProxyProfile 配置档案
双层速率控制每域名随机延迟 + 同域名并发限制,非阻塞锁设计
资源阻断请求级拦截非必要资源类型(image/font/media/websocket/prefetch/manifest),CSS/JS 不禁用
崩溃恢复SQLite WAL 全量持久化 — in_flight 恢复、边界链接自动展开/暂存、链接图自愈
内容去重两层独立:URL 去重 + 稳健哈希内容去重(头/中/尾采样 MD5,24h TTL)
深度控制UrlGate 统一准入(对标 Heritrix CrawlScope),超限 URL 自动暂存边界链接
认证支持HTTP Basic Auth / Bearer Token / Cookie 文件导入 / 自定义 HTTP 头
双界面PySide6 GUI(3 主题模式 + 双语 en↔zh_CN) + 功能完整的 CLI
健康监控统一 HealthChecked 协议 + A/B/C 三级分类调度 + HTTP /health 端点
三重诊断SIGUSR1 asyncio 任务转储 + HTTP /health 端点 + 卡死/熔断自动转储
通知爬取完成 Webhook POST(JSON 报告)
插件系统通用插件底座 — entry_points 自动发现 + 静态 manifest 声明 + COLLECTOR / CHAIN 两种调度模式 + 子进程沙箱纵深防御 + sigstore / GPG 签名验证 + 9 个 CLI 管理子命令

scrapy-scraper

安装:pip install scrapy-scraper

基于Scrapy和Playwright的无头浏览器的网页爬虫和抓取工具。

scrapy-webarchive

Scrapy Webarchive 是 Scrapy 的一个插件,它允许用户在爬取网页时,将网页内容以 WARC 和 WACZ 格式保存下来。

scrapy-flaresolverr

scrapy-flaresolverr提供了 Scrapy 与 FlareSolverr v1 API,支持多后端,轮询选择, 请求层选项、有界并发、响应重建和 Scrapy 统计数据。

用于绕过 Cloudflare 防护的代理服务器:https://github.com/FlareSolverr/FlareSolverr

FlareSolverr 会启动一个代理服务器,以极低的资源消耗处于待机状态,等待用户的请求。当有请求到来时,它会利用 Selenium 和 undetected-chromedriver 来创建一个网页浏览器(Chrome)。该浏览器会使用用户的参数来打开相应的网址,然后等待 Cloudflare 的验证过程完成(或超时)。之后,HTML 代码和 cookies 会被发送回用户手中。这些 cookies 可以被用来通过其他 HTTP 客户端来绕过 Cloudflare 的过滤。

scrapy-h5

一个 Scrapy 下载器中间件,它用 HTML5 解析方式取代了默认的 HTML 解析方式 lxml

安装:pip install scrapy-h5

为什么选择 html5ever

  • 更符合 HTML5 标准:以与浏览器相同的方式解析 HTML 代码
  • 能妥善处理格式错误的 HTML 代码:对现实世界中的 HTML 格式具有更好的兼容性。
  • 快如 Parsel:基于 Rust 的解析器,同时提供 Python 接口支持( markupever )

为什么选择 Lexbor

  • 最快的 HTML5 解析器:基于 C 语言的解析器,同时提供了 Python 接口( selectolax )
  • 更符合 HTML5 标准:以与浏览器相同的方式解析 HTML 代码
  • 能妥善处理格式错误的 HTML 代码:对现实世界中的 HTML 格式具有更好的兼容性。

scrapy-hrequests

安装:pip install scrapy-hrequests

scrapy-hrequests 是 Scrapy 下载处理程序,它使用 hrequests 来替代 Scrapy 默认的 HTTP 下载功能。这样就可以在现有的 Scrapy 爬虫中直接使用 hrequests ,而无需修改任何爬取逻辑。

Hrequests 是一个简单、可配置且功能丰富的工具,可以替代 Python 的 requests 库来处理各种请求:https://github.com/daijro/hrequests

  • 在 HTTP 与无头浏览模式之间实现无缝切换
  • 集成的快速 HTML 解析器
  • 利用 goroutines 和 gevent 实现高性能的网络并发处理
  • 浏览器 TLS 指纹的复制技术
  • JavaScript 渲染功能已启用
  • 支持 HTTP/2 协议
  • 利用 BrowserForge 实现逼真的浏览器标题栏生成效果
  • JSON 序列化的速度比标准库快 10 倍以上

浏览器爬取

  • 简单易用的浏览器自动化工具
  • 利用 Camoufox 和 Patchright 实现反检测浏览功能
  • 类似人类的光标移动和打字方式
  • 无头模式与有头模式支持

scrapy-html

Scrapy-HTML 是一个简单高效的 Python 软件包,可以从任何网页抓取 HTML 内容。现在有了按标签过滤的高级参数,可以按标签过滤,支持多个标签、CSS 类和自定义属性,让抓取变得更加灵活和强大。它使用 BeautifulSoup4 和 Requests 库来进行抓取,并以结构化且易读的方式返回 HTML。安装:pip install scrapy-html

scrapy-impersonate

安装:pip install scrapy-impersonate

scrapy-impersonate 是一个能够模仿浏览器指纹特征的 Scrapy 下载处理程序。该项目结合了 curl_cffi 来发送 HTTP 请求,因此能够模仿浏览器的 TLS 签名或 JA3 指纹信息。一种

scrapy-lokilog

一个 Scrapy 扩展,用于通过 lokilog 将日志以非阻塞方式发送到 Grafana 和 Loki。

安装:pip install scrapy-lokilog

scrapy-mcp

安装:pip install scrapy-mcp

一个基于 Scrapy 构建的无头网络爬取 MCP 服务器。它将 Scrapy 的各种爬取功能——如数据的获取、CSS/XPath 元素的提取、链接和表格的提取、站点地图及 robots.txt 文件的读取、以及异步爬取等——以 MCP 工具的形式呈现出来,供代理程序通过标准输入输出进行调用。

  • 没有页面渲染功能。页面以 HTML 格式被获取和解析;无需浏览器,也无需执行 JavaScript 代码。这样一来,系统的占用资源极低,即使在性能较差的机器上也能顺利运行。
  • 具有反应式处理能力。所有操作都在短暂的 Scrapy 子进程中执行,因此 Twisted 的反应式处理机制不会存在于 asyncio MCP 服务器中(没有 ReactorNotRestartable )。此外,每次调用后,相关内存都会被及时回收。
  • 默认遵守 robots.txt 的指令,通过 AutoThrottle 功能来控制爬取速度,并严格限制页面数量和深度,从而确保爬取过程不会失控。

scrapy-patchwright

scrapy-patchwright 是 scrapy-patchwright 的一个变体版本,它利用 patchright 技术来帮助用户绕过那些防止机器人访问的网站。安装:pip install scrapy-patchwright

Patchright 其实是 Playwright Testing and Automation Framework 的一个经过修改但未被检测出来的版本。它可以直接替代 Playwright 来使用。

scrapy-stealth (重要)

安装:pip install scrapy-stealth

隐蔽式爬取方式。极致的爬取效果。这是一个专为 Scrapy 设计的、可插拔式的反机器人程序和隐蔽式爬取框架。scrapy-stealth 在 Scrapy 的基础上增加了浏览器模拟、代理服务器轮换、指纹信息随机化处理以及智能重试机制等功能——这些功能都是为了解决大规模、生产环境下的爬虫需求而设计的。pypi:https://pypi.org/project/scrapy-stealth/

Scrapy 虽然功能强大,但现代网站都采用了各种先进的反机器人程序保护措施,比如:

  • TLS 指纹识别
  • 浏览器行为检测
  • 速率限制与 IP 地址封锁机制

  scrapy-stealth 功能:

  • 可插拔的引擎系统( scrapy 、 stealth )
  • 可通过 request.meta 来为每个请求单独选择引擎。
  • 支持代理服务器的切换与轮换机制
  • 浏览器指纹随机化身份伪装处理。(基于 TLS 和 HTTP/2 的指纹识别机制)
  • 智能重试机制。
  • 反机器人检测功能(基于状态检测和内容分析,由 Cloudflare 和 Akamai 提供支持)
  • 智能浏览器选择功能——运行速度快,首先执行操作,然后会在可见的 Chrome 浏览器界面中重试该操作,即使遇到 JavaScript 相关的限制或禁令也不例外(该功能在 driver="auto" basic turbo STEALTH_ENABLED = True 启用时自动生效)
  • 线程安全的异步集成
  • 专为那些包含大量 JavaScript 代码的页面而设计的真实浏览器引擎(CDP)
  • 智能会话回收机制。在连续被禁止访问后,浏览器会重新启动 Chrome;同时还会清除基本的/快速的 HTTP 会话数据。
  • 静态资源拦截。跳过图片、字体、CSS 和媒体文件,从而加快浏览器加载速度,减轻浏览器负担。
  • 代理绕过列表。将选定的域名直接指向原始服务器,而不是通过代理服务器 ( --proxy-bypass-list )

对比 其他框架

Featurescrapy-stealthscrapy-impersonatescrapy-playwrightscrapy-splashScrapy (default)
TLS fingerprint spoofing
HTTP/2 support
Browser impersonation⚠️ partial
Proxy rotation (built-in)
Fingerprint rotation
Anti-bot detection
Smart retry logic
Per-request engine switching
Headless browser required
JavaScript rendering️✅
Screenshot / snapshot
Native Scrapy integration
Memory footprint🟢 Low🟢 Low🔴 High🔴 High🟢 Low

⚠️ scrapy-playwright 传递真实的浏览器,但不像 Scray-stealth 那样模拟指纹配置文件。 scrapy-impersonate 通过 curl_cffi 提供 TLS/HTTP2 模拟,但缺乏内置的旋转、检测或按请求引擎切换功能。

推荐的设置

DOWNLOADER_MIDDLEWARES = {
    "scrapy_stealth.middlewares.StealthDownloaderMiddleware": 950,
}
STEALTH_ENABLED = True  # injects driver="auto" on every request

该机制首先使用 HTTP 伪装技术进行快速识别,然后通过 turbo 进行更深入的 TLS 指纹分析。如果检测到有 JavaScript 相关的问题或会话被禁止,它会再次尝试使用可见的 Chrome 界面进行识别。没有额外的备用方案——这就是唯一的选项。 driver="auto"

两种设置方法

方式 1:全局设置 ( settings.py )

# 1. Enable the middleware
DOWNLOADER_MIDDLEWARES = {
    "scrapy_stealth.middlewares.StealthDownloaderMiddleware": 950,
}

# 2. Route ALL requests through stealth — injects driver="auto" automatically (turbo first)
STEALTH_ENABLED = True
# STEALTH_DRIVER = "basic"  # optional: lighter HTTP driver instead of default turbo

# 3. (Optional) Proxy list — seeded as engine default; rotated on ban-streak session recycle
#    Supported schemes: http, https, socks4, socks5
STEALTH_PROXIES = [
    "http://proxy1:8080",
    "http://proxy2:8080",
    "http://user:pass@proxy3:8080",  # with authentication
    "socks5://proxy4:1080",
]

# 4. (Optional) Pin hosts to fixed origin IPs (bypass public DNS)
#    Connects to the IP while keeping the hostname for TLS SNI / Host / certs
STEALTH_DNS_OVERRIDES = {
    "example.com": "203.0.113.10",
    "www.example.com": "203.0.113.10",
}

方式 2:通过 spider 进行设置 ( custom_settings )

class MySpider(scrapy.Spider):
    name = "example"

    custom_settings = {
        "DOWNLOADER_MIDDLEWARES": {
            "scrapy_stealth.middlewares.StealthDownloaderMiddleware": 950,
        },
        "STEALTH_ENABLED": True,
        "STEALTH_PROXIES": [
            "http://proxy1:8080",
            "http://user:pass@proxy2:8080",
            "socks5://proxy3:1080",
        ],
        "STEALTH_DNS_OVERRIDES": {
            "example.com": "203.0.113.10",
        },
    }

给指定的请求设置 stealth

# Smart path — HTTP first, browser on challenge/ban
yield scrapy.Request(
    url="https://example.com",
    meta={"stealth": {"driver": "auto"}},
)

# HTTP-only — no browser fallback
yield scrapy.Request(
    url="https://example.com",
    meta={"stealth": {"driver": "basic"}},
)

给所有请求都设置 stealth

# settings.py or custom_settings
STEALTH_ENABLED = True
# STEALTH_DRIVER = "basic"  # optional: lighter HTTP driver instead of default turbo
# 这里不需要设置 meta, 中间件会自动注入 driver="auto"
yield scrapy.Request(url="https://example.com")

# 给特定请求取消 stealth 
yield scrapy.Request(url="https://api.internal/health", meta={"stealth": False})

# 强制请求使用 HTTP(无浏览器回退)
yield scrapy.Request(url="https://example.com", meta={"stealth": {"driver": "basic"}})

全局配置

可以通过共享实例来定制整个包级别的默认设置。所有设置必须在模块级别进行配置,而不能在spider类级别进行更改。因为引擎客户端是在中间件初始化时创建的,所以在此之后的任何修改都将无效。 config start_requests parse

# myspider.py
import scrapy
from scrapy_stealth.config import config

config.DEFAULT_ENGINE = "stealth"  # "scrapy" (native) or "stealth" (browser impersonation)
config.DEFAULT_PROFILE = "chrome_147"  # browser profile when meta["stealth"]["profile"] is not set
config.DEFAULT_TIMEOUT = 30  # stealth request timeout in seconds
config.STEALTH_DRIVER = "turbo"  # "turbo" (default), "basic", "browser", or "auto"
config.HTTP2 = True  # False for servers that only support HTTP/1.1
config.BLOCK_CODES |= {407}  # extend blocked status codes (|= keeps defaults)
config.BLOCK_KEYWORDS.append("banned")  # extend blocked body-text patterns
config.BROWSER_HEADLESS = True  # browser driver: headless mode (False = visible window, more stealthy)
config.BROWSER_SETTLE_S = 4.0  # browser driver: seconds to wait after navigation for JS to finish
config.BROWSER_EXECUTABLE_PATH = "/usr/bin/brave-browser"  # custom browser binary (default: auto-detect Chrome)
config.STEALTH_RECYCLE_AFTER_BANS = 5  # recycle Chrome / HTTP sessions after 5 consecutive bans
config.BROWSER_STATIC_ASSETS_BLOCK = True  # block images/fonts/CSS/media (skipped when snapshot=True)
config.BROWSER_PROXY_BYPASS_LIST = ["example.com", "*.internal"]  # these bypass the proxy
config.STEALTH_DNS_OVERRIDES = {"example.com": "203.0.113.10"}  # pin host → origin IP


class MySpider(scrapy.Spider):
    name = "example"
    ...

下面是错误示例

class MySpider(scrapy.Spider):
    def start_requests(self):
        config.HTTP2 = False  # has no effect
        ...

以编程方式获取任何数值信息:

config.get("DEFAULT_ENGINE")  # "scrapy"
config.get("MISSING_KEY", "default")  # "default"
  • DEFAULT_ENGINE:str 类型,默认值 "scrapy",当键不存在时使用的引擎 request.meta["stealth"]
  • DEFAULT_PROFILE:str 类型,默认值 "chrome_147",未指定时使用的浏览器配置文件
  • DEFAULT_TIMEOUT:int 类型,默认值 30,请求超时(秒)
  • STEALTH_DRIVER:str 类型,默认值 "turbo",当未在每个请求的隐身字典中设置驱动程序时,即为默认的初级HTTP驱动程序。选项:,,,。可通过Scrapy设置读取为driver="auto""basic""turbo""browser""auto"STEALTH_DRIVER
  • STEALTH_ENABLED:bool 类型,默认值 False,当请求已设置驱动程序或通过 `Truedriver="auto"meta={"stealth": False}` 拒绝时,将所有请求通过隐身模式转发并注入。
  • HTTP2,bool 类型,默认 True,HTTP/2 模式;可通过 meta["stealth"]["http2"] 按请求覆盖
  • BLOCK_CODES:frozenset[int],默认值 {403, 429, 503},需要阻塞的 http 状态码
  • BLOCK_KEYWORDS:类型 list[str],默认值 ["captcha", "access denied", …],当 body-text 中出现关键字时,则阻塞。
  • BROWSER_HEADLESS:类型 bool,默认 True,无头模式(= 无可见窗口,更隐蔽)。
  • BROWSER_SETTLE_S:类型 float,默认 4.0,导航后等待JS渲染完成的秒数
  • BROWSER_NO_SANDBOX:类型 bool | None,默认值 None,禁用 Chrome 沙盒。= 自动检测(以 root 用户运行时启用,例如 Docker)无
  • BROWSER_EXECUTABLE_PATH:类型 str | None,默认值None,浏览器二进制文件的路径。= 自动检测 Chrome/Chromium。设置为使用 Brave 或自定义安装(例如 None"/usr/bin/brave-browser")
  • BROWSER_MAX_TABS:类型 int,默认值 10,请求的最大并发 Chrome 窗口数
  • STEALTH_RECYCLE_AFTER_BANS:类型 int,默认值 5,经过多次连续禁用后:重启 Chrome;清除 HTTP 会话/客户端缓存。任何干净的响应都会重置计数浏览器基础涡轮。
  • BROWSER_STATIC_ASSETS_BLOCK:类型 bool,默认值 False,通过CDP阻止图片、字体、CSS和媒体内容。可通过请求时覆盖;当meta["stealth"]["static_assets_block"]snapshot=True时始终关闭
  • BROWSER_PROXY_BYPASS_LIST:类型 list[str],默认值 [],通过 Chrome 的 . 选项绕过代理,直接连接到源服务器的域名/模式。支持通配符()、IP/CIDR、端口和 。仅在使用代理时生效;在浏览器启动时设置(配置/设置,而非每次请求),--proxy-bypass-list*.example.com<local>
  • STEALTH_DNS_OVERRIDES:类型 dict[str, str],默认值 {},用于 /(以及 Chrome)的主机→IP 映射。在保持 TLS SNI、主机名、证书验证的同时连接到该 IP 地址。也可从 Scrapy 配置中读取为 。通过 basicturbo--host-resolver-rulesbrowserSTEALTH_DNS_OVERRIDESmeta["stealth"]["dns"] 实现请求级覆盖。

对于单次请求的临时覆盖,请设置 meta["stealth"]["driver"] 或 meta["stealth"]["http2"](参见下方的按请求配置)。

单个请求的配置

所有选项都通过 request.meta["stealth"] 传递。当存在 meta["stealth"] (一个字典对象)时,会激活隐身模式。如果省略该键,则会使用默认的 Scrapy 引擎。当使用 STEALTH_ENABLED = True 时,所有请求都会处于隐身状态;而使用 driver="auto" 则可以实现完全隐身。要关闭隐身模式,可以使用 meta={"stealth": False} ;或者设置明确的 driver 来覆盖此行为。

yield scrapy.Request(
    url,
    meta={
        "stealth": {
            "driver": "turbo",
            # optional overrides — otherwise profile/proxy come from defaults and
            # rotate automatically when the session recycles after consecutive bans
            "profile": "chrome_147",
            "proxy": "http://user:pass@proxy:8080",
            "stealth_timeout": 60,
            "http2": True,
            "dns": "203.0.113.10",  # or {"example.com": "203.0.113.10"}
        }
    },
)
  • driver:字符串类型。"basic" 、 "turbo" 、 "browser" 或 "auto" 。使用 "auto" 进行智能选择:首先执行 HTTP 操作( STEALTH_DRIVER ),然后在挑战/禁令情况下进行一次浏览器重试。当 STEALTH_ENABLED = True 触发时自动注入。仅使用 "basic" 或 "turbo" 时不会回退到浏览器操作。
  • fallback:bool 类型,将 False 设置为禁用在 driver="auto" 激活时的浏览器重试功能
  • profile:字符串类型,浏览器配置文件(例如: "chrome_147" 、 "safari_ios_18_1_1" )。无需使用自定义设置;默认情况下,配置会基于连续禁言会话进行轮换。
  • proxy:字符串类型,无需使用 STEALTH_PROXIES 作为默认值;默认情况下,代理会在基于连击效果的会话循环中使用。
  • dns:类型 str or dict   str 或 dict,Pin DNS:此请求的主机名对应的直接 IP 地址,或者 {host: ip} 的映射。会合并 STEALTH_DNS_OVERRIDES 的值。每个请求都可以使用 basic 或 turbo 的值;而 browser 的值仅在 Chrome 启动时会使用全局覆盖值。
  • stealth_timeout:int 类型,按请求计时的超时时间(单位秒,可覆盖默认的 30 秒时间限制)
  • http2:bool 类型,True = HTTP/2, False = HTTP/1.1(此请求替换了 config.HTTP2 的值)
  • headless:bool 类型, True = 无界面模式, False = 可见窗口(更隐蔽)
  • settle:float 类型,导航后需要几秒钟即可加载 JavaScript 代码(默认值为 4.0 )
  • snapshot:bool 类型,生成 PNG 快照——结果可表示为 response.meta["snapshot_content"] ( bytes )
  • static_assets_block:bool 类型,仅浏览器驱动:该请求会屏蔽图片、字体、CSS 代码以及媒体内容(会覆盖 config.BROWSER_STATIC_ASSETS_BLOCK 中的设置)。当 snapshot 等于 True 时,该设置会被忽略,始终启用这些功能。

POST、headers、cookies

所有隐秘传输的驱动程序( basic 、 turbo 、 browser 和 driver="auto" )都遵循相同的 Scrapy Request 字段规范——包括 HTTP 方法、请求体数据、 Cookie 字段以及自定义头部。使用常规的 Scrapy 语法即可;对于 POST 请求或认证头部信息,不需要额外的隐秘传输相关元数据。

在内部,每个驱动程序都会调用 build_stealth_request() 函数来对请求进行标准化和验证(包括方法、URL、请求体、cookie 以及头部信息)。头部信息的指纹识别则由引擎的冒充层来处理——只需像往常一样在 Scrapy 请求中设置 Authorization 、 Content-Type 、 Cookie 以及其他特定于应用的头部信息即可。

JSON POST (API login, search 等)。适用于 basic 、 turbo 和 browser 。使用 postman-echo.com/post ——它可以将 JSON 数据原封不动地返回,并且能够稳定运行(避免使用 httpbin.org ,因为它常常会返回 503 错误)。

import json

yield scrapy.Request(
    "https://postman-echo.com/post",
    method="POST",
    body=json.dumps({"search": "laptop", "page": 1}).encode(),
    headers={"Content-Type": "application/json"},
    meta={"stealth": {"driver": "turbo"}},  # or basic / browser / auto
)

如果启用了全局隐身功能,则无需提供 meta 信息——同样的请求格式仍然适用:

STEALTH_ENABLED = True  # settings.py

yield scrapy.Request(
    "https://postman-echo.com/post",
    method="POST",
    body=json.dumps({"search": "laptop"}).encode(),
    headers={"Content-Type": "application/json"},
)

表单提交方式:POST(登录、过滤)。quotes.toscrape.com/login 是一个公开的 Scrapy 教程网站,拥有真实的登录表单:

from urllib.parse import urlencode

yield scrapy.Request(
    "https://quotes.toscrape.com/login",
    method="POST",
    body=urlencode({"username": "admin", "password": "admin"}).encode(),
    headers={"Content-Type": "application/x-www-form-urlencoded"},
    meta={"stealth": {"driver": "auto"}},
)

Cookies、Authorization

Cookies 与 授权。在 Scrapy 请求中传递会话 cookie 或持有令牌——所有驱动程序都会转发这些元素。postman-echo.com/get 会回传请求头信息:

yield scrapy.Request(
    "https://postman-echo.com/get",
    headers={
        "Cookie": "session_id=abc123; cart_token=xyz",
        "Authorization": "Bearer test-token-123",
    },
    meta={"stealth": {"driver": "turbo"}},
)

还可以使用 Scrapy 内置的 cookie 存储功能,在 basic 和 turbo 之间进行 GET/POST 操作。每个请求都会通过 CDP 来应用 Cookie 头部信息。 browser 驱动程序会负责处理这些请求。

PUT / PATCH / DELETE

相同的模式——设置 method 和可选的 body 。

yield scrapy.Request(
    "https://jsonplaceholder.typicode.com/posts/1",
    method="PATCH",
    body=b'{"title": "patched"}',
    headers={"Content-Type": "application/json"},
    meta={"stealth": {"driver": "basic"}},
)

Driver 行为 汇总

DriverGET / HEADPOST / PUT / PATCH / DELETE / …
basic原生 HTTP 客户端 + 配置文件 TLS 加密通信同样。方法+主体+头部信息
turbocurl-impersonate TLS fingerprint同样。方法+主体+头部信息
browserChrome tab navigationpage 内的 fetch() ,包含  method + body

对于 driver="auto" ,第一阶段会使用 basic / turbo (包括 POST 请求)。如果响应是 JavaScript 挑战或会话禁令,第二阶段会再次尝试,使用相同的方法、相同的数据体和相同的头部信息。

不需要手动设置的部分

在 Scrapy 请求中不要覆盖指纹头信息,这些信息会被删除,并由活动配置文件替代。

  • 不需要设置的部分:User-AgentAcceptAccept-LanguageAccept-Encoding,sec-ch-ua*sec-fetch-*Upgrade-Insecure-Requests, sec-ch-ua* 、 sec-fetch-* 、 Upgrade-Insecure-Requests 等。
  • 仅设置应用头部信息 ( Content-Type , Cookie , Authorization , X-* , …)。

 受 JS 保护的 POST 流程

当某个网站需要在 Cloudflare 之后进行登录或调用 API 时,请指向一个真实的端点——在这里,可以通过浏览器驱动来访问 postman-echo.com/post ,将你的生产环境目标 URL 替换进去即可。

yield scrapy.Request(
    "https://postman-echo.com/post",
    method="POST",
    body=b'{"items": [{"sku": "A1", "qty": 2}]}',
    headers={"Content-Type": "application/json"},
    meta={"stealth": {"driver": "browser", "headless": False, "settle": 6}},
)

对于采用 JavaScript 渲染的 GET ,可以尝试使用 quotes.toscrape.com 与 driver="browser" 进行组合操作。或者让 driver="auto" 先尝试使用快速 HTTP 协议,只有在确实需要时才会切换到仅支持浏览器的方式。

自定义 DNS 配置修改

智能的网页选择功能

使用 driver="auto" 可以自动选择合适的引擎:对于普通页面,继续使用快速的 HTTP 伪装方式( basic / turbo );只有当响应结果类似 JavaScript 挑战或会话禁令(403/429/503 状态码,Cloudflare 的“请稍等”提示,Akamai、DataDome 等类似信号)时,才切换到真实的 Chrome 浏览器进行处理。浏览器回退功能现在完全由 driver="auto" 来控制。当 STEALTH_ENABLED = True 触发时,中间件会自动执行相关操作。

步骤DriverWhen
1turbo (默认) 或者 basic第一次尝试——内存较低,但吞吐量较高( STEALTH_DRIVER )
2browser (headless=False)当第一阶段遇到障碍或受到挑战时,会尝试一次重试。

这个后备方案总是会打开一个可见的 Chrome 窗口( headless=False ),以便更好地进行规避操。无论 BROWSER_HEADLESS 的值如何,或者之前设置的 meta["stealth"]["headless"] 值如何。

全局模式,最简单的设置方式:

STEALTH_ENABLED = True
# STEALTH_DRIVER = "basic"  # optional — lighter HTTP driver instead of default turbo

根据请求(无需使用 STEALTH_ENABLED ):

yield scrapy.Request(
    url,
    meta={"stealth": {"driver": "auto"}},
)

始终使用浏览器进行访问(跳过第一阶段):

meta={"stealth": {"driver": "browser"}}

仅支持 HTTP 协议(无浏览器重试机制):

meta={"stealth": {"driver": "basic"}}  # or "turbo"

禁用浏览器重试机制,但保留自动 HTTP 驱动功能(较为罕见的情况)。

meta={"stealth": {"driver": "auto", "fallback": False}}

每个请求最多会尝试重试一次。如果浏览器获取响应失败,则会返回原始的 basic / turbo 响应。控制台输出和统计信息( stealth/fallbacks 、 stealth/requests/browser )会显示事件发生的时刻。

Browser Engine

对于受 CloudFlare JS 挑战保护的应用或需要大量 JavaScript 渲染的场景,可以使用 browser 驱动程序。该驱动程序通过开发工具协议运行真实的 Chrome 实例(无需 WebDriver),会保持一个持续的浏览器实例,并每个请求都打开一个新的标签页。

每个请求 (最常见的情况)

yield scrapy.Request(
    url,
    meta={
        "stealth": {
            "driver": "browser",
            "headless": False,  # visible window — harder to detect (default: True)
            "settle": 4.0,  # seconds to wait for JS after page load
        }
    },
)

那些使用 Cloudflare 服务的重型网站——其加载时间有所延长:

meta = {"stealth": {"driver": "browser", "headless": False, "settle": 12}}

全局默认设置(所有隐秘的请求都通过浏览器引擎来执行):

from scrapy_stealth.config import config

config.STEALTH_DRIVER = "browser"
config.BROWSER_HEADLESS = False  # more stealthy
config.BROWSER_SETTLE_S = 6.0  # longer wait for JS

自定义浏览器二进制文件(适用于 Brave、Chromium 浏览器,或安装路径不是默认路径的 Chrome 浏览器):

from scrapy_stealth.config import config

config.BROWSER_EXECUTABLE_PATH = "/usr/bin/brave-browser"  # Linux
# config.BROWSER_EXECUTABLE_PATH = r"C:\Program Files\BraveSoftware\Brave-Browser\Application\brave.exe"  # Windows

或者通过 settings.py / custom_settings :BROWSER_EXECUTABLE_PATH = "/usr/bin/brave-browser"

当 BROWSER_EXECUTABLE_PATH 等于 None (默认情况下), scrapy-stealth 会从标准系统路径中自动检测出 Google Chrome 或 Chromium。但如果在使用 Brave 浏览器或非标准的 Chrome 浏览器安装时需要手动设置该值,那么如果路径不存在,系统会提示明确的错误。

智能重启 / session 回收:

在连续 STEALTH_RECYCLE_AFTER_BANS 次被识别为恶意响应的情况下(根据防作弊检测结果),scrapy-stealth 会重新初始化驱动会话:

  • browser :重新启动 Chrome 浏览器(会加载新的指纹数据、缓存数据以及 CDP 会话状态)
  • basic / turbo:基础模式/加速模式——会清除缓存的 HTTP 客户端/会话信息,并更换默认的指纹配置文件以及代理设置,由 STEALTH_PROXIES 负责操作(无需针对每个请求单独进行设置)

一个干净的响应可以重置这一连续成功记录,因此,只要处理了大量的请求,就不应该让同样的响应重复出现。

from scrapy_stealth.config import config

config.STEALTH_RECYCLE_AFTER_BANS = 5  # 连续被禁5次后回收(默认)

静态资源 阻塞

Scrapy-Stealth 可以通过 CDP Fetch 机制阻止浏览器加载静态资源(如图片、字体、CSS 文件等),从而加快页面加载速度并减少带宽占用。这一功能默认是关闭的——可以通过在 settings.py 中设置 BROWSER_STATIC_ASSETS_BLOCK = True 来全局启用该功能,或者通过 meta["stealth"]["static_assets_block"] 在每个请求中启用该功能。不过,当使用 snapshot=True 时,静态资源加载功能仍然会被启用,因为快照生成需要完整的页面渲染结果。

# settings.py
BROWSER_STATIC_ASSETS_BLOCK = True
# per-request
meta = {"stealth": {"driver": "browser", "static_assets_block": True}}
# snapshot always wins — assets are never blocked here, even with the global default on
meta = {"stealth": {"driver": "browser", "snapshot": True}}

代理绕过名单

当配置了代理服务器时,你可以将特定的域名直接发送到原始服务器,而不是通过代理进行转发。这些域名会被传递给 Chrome 的 --proxy-bypass-list 启动参数,因此支持完整的 Chrome 绕过语法——包括简单的域名、通配符( *.example.com )、IP/IPCIDR 范围、端口,以及特殊的 <local> 标记。

# settings.py
STEALTH_DRIVER = "browser"
STEALTH_PROXIES = ["http://user:pass@proxy:8080"]
BROWSER_PROXY_BYPASS_LIST = [
    "example.com",  # exact host
    "*.internal.net",  # wildcard subdomains
    "127.0.0.1",  # IP
    "<local>",  # any plain hostname without dots
]
# or via config
from scrapy_stealth.config import config

config.BROWSER_PROXY_BYPASS_LIST = ["example.com", "*.internal.net"]

“绕过列表”是一个适用于 Chrome 浏览器的启动标志。当浏览器启动时,该标志会被读取一次,并且在整个浏览器运行期间都有效。该设置是全局性的,不是针对每个请求单独设置的。除非使用了代理服务器,否则该标志不会产生任何效果。

Docker(以 root 用户身份运行):

当进程以 root 身份运行时,Chrome 需要 --no-sandbox 。 scrapy-stealth 可以自动检测这一点,但你也可以直接在 settings.py 中显式设置它。

BROWSER_NO_SANDBOX = True  # force no-sandbox (Docker, any root environment)
BROWSER_EXECUTABLE_PATH = "/usr/bin/chromium"  # use Chromium instead of Chrome in Docker

或者通过 config :

config.BROWSER_NO_SANDBOX = True
config.BROWSER_EXECUTABLE_PATH = "/usr/bin/chromium"

性能报告:该浏览器引擎的速度比 basic / turbo 慢(每页需要 5 到 15 秒,而turbo 则不到 2 秒)。只有那些受到攻击的 URL 才会被发送到浏览器进行处理,其余内容都通过快速的 HTTP 传输方式传递。

Screenshots 截图

截取 browser 驱动程序所渲染的任何页面的 PNG 截图,并将其保存到磁盘上。

在 request 中启用

yield scrapy.Request(
    url,
    meta={
        "stealth": {
            "driver": "browser",
            "snapshot": True,
        }
    },
    callback=self.parse,
)

那些原始 PNG 字节数据可以在 response.meta["snapshot_content"] 处获取,该数据会在你的回调函数中提供。

使用 snapshot 装饰器进行自动保存

from scrapy_stealth.decorators import snapshot


class MySpider(scrapy.Spider):

    @snapshot
    def parse(self, response): ...

    @snapshot(path="stealth_shots/page.png")
    def parse(self, response): ...

    @snapshot(path=lambda r: r.url.split("/")[-1] + ".png")
    def parse(self, response): ...

注意:该功能需要用户在请求元数据中包含 driver="browser" 和 snapshot=True 参数。如果响应中找不到任何快照数据,将会记录错误。

自定义处理方式,不包括内置的辅助功能。这个截图只是来自 bytes ,在 response.meta["snapshot_content"] 中——你可以随意使用它吧。

def parse(self, response):
    shot: bytes | None = response.meta.get("snapshot_content")
    if shot is None:
        return  # screenshot was not requested or capture failed

    # Save manually
    with open("page.png", "wb") as f:
        f.write(shot)

    # Pass to a pipeline via item
    yield {"url": response.url, "screenshot": shot}

自动旋转功能

在连续被 STEALTH_RECYCLE_AFTER_BANS 次封禁后,会话会重新启用,此时会自动选择新的默认配置文件和代理设置(来自 STEALTH_PROXIES )。这样就能保持稳定的会话性能,同时实现会话的复用。

# settings.py
STEALTH_PROXIES = ["http://proxy1:8080", "http://proxy2:8080"]
STEALTH_RECYCLE_AFTER_BANS = 5  # default

# spider — per-request stealth with explicit driver (no auto injection)
yield scrapy.Request(url, meta={"stealth": {"driver": "turbo"}})

Scrapy 统计信息:在爬取完成后(或经过 crawler.stats 的中途处理),检查结果如下:

KeyMeaning
stealth/requests / stealth/requests/{driver}Stealth fetches
stealth/responses / stealth/responses/{driver}Completed responses
stealth/successes / stealth/successes/{driver}Non-banned responses below HTTP 400
stealth/failures / stealth/failures/{driver}Banned responses or HTTP 400+
stealth/status/{code}Response count by HTTP status
stealth/bans / stealth/bans/{driver}Session-ban responses
stealth/recycles / stealth/recycles/{driver}Session / Chrome recycles
stealth/ban_streakCurrent consecutive ban streak
stealth/driverLast stealth driver used
stealth/profileLast fingerprint profile used
stealth/proxyLast proxy as host:port (no credentials)
stealth/proxy/requests/{driver}Requests sent through a proxy
stealth/dns/requests/{driver}Requests using DNS overrides
stealth/dns/hostsTotal pinned hosts applied
stealth/dns/active_hostsPinned hosts on latest request
stealth/fallbacks / stealth/fallbacks/{driver}Browser escalations from driver="auto"
# e.g. in spider_closed
stats = spider.crawler.stats.get_stats()
print(stats.get("stealth/bans"), stats.get("stealth/recycles"))

代理轮换

from scrapy_stealth.strategies.proxy import ProxyRotator

proxy_rotator = ProxyRotator([
    "http://proxy1:8080",
    "http://proxy2:8080",
])

yield scrapy.Request(
    url,
    meta={
        "stealth": {
            "proxy": proxy_rotator.get(),
        }
    },
)

指纹 旋转

from scrapy_stealth.strategies.fingerprint import ProfileRotator

fp = ProfileRotator()

yield scrapy.Request(
    url,
    meta={
        "stealth": {
            "profile": fp.get(),
        }
    },
)

智能 重试

from scrapy_stealth.strategies.retry import RetryHandler

retry = RetryHandler()


def parse(self, response):
    if retry.should_retry(response):
        yield retry.build(response.request)
        return

反机器人检测功能

from scrapy_stealth.detectors.antibot import AntiBotDetector

detector = AntiBotDetector()

if detector.is_blocked(response):
    print("Blocked!")

完整 spider 示例

完整的运行程序代码保存在 examples/full_spider.py 中。

  • 中间件 + STEALTH_ENABLED 通过 custom_settings 实现(自动注入 driver="auto" ,优先使用 turbo(涡轮) 模式)
  • 每个请求 basic / browser 的设定进行覆盖
  • 带有 JSON 数据体和自定义头信息的 POST 请求(参见关于 POST 请求、头部和 cookie 的信息)
  • 带有 @snapshot 的可选快照
  • 在近距离情况下,可以检测到 bans 以及游戏的隐秘属性数据。
# from a Scrapy project
scrapy crawl stealth_demo

# or one-off
scrapy runspider examples/full_spider.py

最小版本:

import scrapy


class ExampleSpider(scrapy.Spider):
    name = "example"
    custom_settings = {
        "DOWNLOADER_MIDDLEWARES": {
            "scrapy_stealth.middlewares.StealthDownloaderMiddleware": 950,
        },
        "STEALTH_ENABLED": True,
    }

    def start_requests(self):
        yield scrapy.Request("https://example.com")

    def parse(self, response):
        yield {"title": response.css("title::text").get(), "url": response.url}

性能分析工具

有选择地使用隐身功能:

  • 更快的爬虫速度(对于简单的页面,可以使用 Scrapy 实现)
  • 降低代理成本
  • 在受保护的页面上,成功率更高

drissionpage

pypi 搜索:https://pypi.org/search/?q=drissionpage

scrapy-drissionpage

Scrapy DrissionPage 是一个将 DrissionPage网页自动化工具 与 Scrapy 框架无缝集成的扩展工具,让您可以在 Scrapy 爬虫中使用 DrissionPage 的全部功能。本扩展工具支持浏览器自动化和数据包收发两种模式,并可以自由切换,大幅提高爬虫开发效率与稳定性。

主要特性:

  • 模式自由切换:支持在浏览器模式(chromium)和会话模式(session)间动态切换
  • 性能优化:提供数据读取加速功能,支持静态解析,提高数据提取速度
  • 数据包监听:可监听网络请求,轻松获取AJAX加载的数据
  • 文件下载:集成下载功能,支持自定义保存路径和文件名
  • 简洁语法:兼容DrissionPage的简洁元素定位语法,大大减少代码量

aio-scrapy (重要)

AioScrapy是一个基于Python异步IO的强大网络爬虫框架。它的设计理念源自Scrapy,但完全基于异步IO实现,提供更高的性能和更灵活的配置选项。

  • 完全异步:基于Python的asyncio库,实现高效的并发爬取
  • 多种下载处理程序:支持多种HTTP客户端,包括aiohttp、httpx、requests、pyhttpx、curl_cffi、DrissionPage、playwright和sbcdp
  • 灵活的中间件系统:轻松添加自定义功能和处理逻辑
  • 强大的数据处理管道:支持多种数据库存储选项
  • 内置信号系统:方便的事件处理机制
  • 丰富的配置选项:高度可定制的爬虫行为
  • 分布式爬取:支持使用Redis和RabbitMQ进行分布式爬取
  • 数据库集成:内置支持Redis、MySQL、MongoDB、PostgreSQL和RabbitMQ

AioScrapy 是一个基于异步IO的网络爬虫框架,它的流程和Scrapy基本是一样的,它的架构由以下主要组件组成:

  1. 引擎(Engine):协调所有组件之间的数据流
  2. 调度器(Scheduler):接收引擎发送的请求,并在引擎请求时返回它们
  3. 下载器(Downloader):获取网页并将它们返回给引擎
  4. 爬虫(Spiders):处理下载的响应并提取数据和新的请求
  5. 项目管道(Item Pipelines):处理爬虫提取的数据
  6. 中间件(Middlewares):处理请求和响应的钩子

AioScrapy 中的数据流如下:

  1. 引擎从爬虫获取初始请求
  2. 引擎将请求发送给调度器,并请求下一个要爬取的请求
  3. 调度器返回下一个请求给引擎
  4. 引擎通过下载器中间件将请求发送到下载器
  5. 下载完成后,下载器生成一个响应,并通过下载器中间件将其发送到引擎
  6. 引擎接收到响应,并通过爬虫中间件将其发送到爬虫进行处理
  7. 爬虫处理响应,并将提取的数据项和新的请求返回给引擎
  8. 引擎将提取的数据项发送给项目管道,并将新的请求发送给调度器
  9. 重复步骤2-8,直到没有更多请求

异步架构。AioScrapy使用Python的asyncio库实现异步操作,这使得它能够高效地处理大量并发请求。主要的异步组件包括:

  • 异步下载器:使用异步HTTP客户端(如aiohttp、httpx)执行请求
  • 异步管道:使用异步数据库客户端(如motor、aiomysql)存储数据
  • 异步中间件:使用异步方法处理请求和响应
  • 异步爬虫:使用异步方法解析响应和生成请求

Spiders 是 AioScrapy 的核心组件。每个爬虫都必须继承自Spider基类,并实现以下方法:

  • parse:处理下载的响应并提取数据和新的请求
  • start_requests(可选 | optional):生成初始请求

Requests 由以下主要属性组成:

  • url:要请求的URL
  • method:HTTP方法(GET、POST等)
  • headers:HTTP头
  • body:请求体 
  • meta:请求元数据,可用于在请求和回调之间传递数据 
  • callback:处理响应的回调函数
  • errback:处理请求错误的回调函数 

Responses 由以下主要属性组成:

  • url:响应的URL
  • status:HTTP状态码
  • headers:HTTP头
  • body:响应体
  • request:生成此响应的请求
  • meta:响应元数据,继承自请求的meta

中间件 | Middlewares

  • 下载器中间件 | Downloader Middleware。下载器中间件位于引擎和下载器之间,可以处理请求在发送到下载器之前和响应在返回到引擎之后的过程。
  • 爬虫中间件 | Spider Middleware。爬虫中间件位于引擎和爬虫之间,可以处理爬虫生成的请求和下载器返回的响应。

管道 | Pipelines。管道是处理爬虫提取的数据的组件,它们可以执行以下操作:

  • 清洗数据 
  • 验证数据 
  • 检查重复
  • 存储数据到数据库

下载器 | Downloaders。

  • 下载器负责从互联网获取网页和其他资源。AioScrapy支持多种下载处理程序,可以根据需要选择不同的HTTP客户端。

信号 | Signals。AioScrapy 使用信号系统允许组件在特定事件发生时触发操作。常用信号包括: 

  • spider_opened:爬虫开始时触发
  • spider_closed:爬虫关闭时触发
  • item_scraped:项目被爬取时触发
  • request_scheduled:请求被调度时触发
  • response_received:响应被接收时触发

设置 | Settings。AioScrapy提供了丰富的配置选项,允许您自定义爬虫的行为。

安装:pip install aio-scrapy
安装最新版:pip install git+https://github.com/ConlinH/aio-scrapy

AioScrapy 提供了多个可选依赖包,以支持不同的功能:

pip install aio-scrapy[all]      # 安装所有可选依赖
pip install aio-scrapy[redis]    # 安装Redis支持
pip install aio-scrapy[mysql]    # 安装MySQL支持
pip install aio-scrapy[mongo]    # 安装MongoDB支持
pip install aio-scrapy[pg]       # 安装PostgreSQL支持
pip install aio-scrapy[rabbitmq]   # 安装RabbitMQ支持 

验证安装:aioscrapy version

安装不同的下载器。AioScrapy 支持多种 HTTP 客户端作为下载器。默认使用 aiohttp

pip install httpx        # 安装httpx支持
pip install playwright   # 安装 playwright 支持( 用于 JavaScript 渲染 )
python -m playwright install
pip install pyhttpx      # 安装 pyhttpx 支持
pip install curl_cffi    # 安装 curl_cffi 支持
pip install DrissionPage # 安装 DrissionPage 支持

2025 年初setuptools 发布了 82.0.0 版本。在此版本中,官方正式调整了内部模块结构,将存在了十几年的 pkg_resources 彻底从默认导入路径中移除或剥离。

  • 方案一:强制降级(最快、最稳妥,推荐!)。由于 pkg_resources 在 81.x 版本中仍保留了兼容层,我们只需要将 setuptools 强制锁定在 82.0.0 版本以下 即可。强制重新安装 81.0.0 版本:python -m pip install "setuptools<82.0.0"  验证是否修复:python -c "import pkg_resources; print('Found it!')"
  • 方案二:修改项目源码(不推荐,仅限紧急情况)。如果无法降级环境,你需要找到报错的文件,将 import pkg_resources 修改为:
    try:
        import pkg_resources
    except ImportError:
        import importlib.metadata as pkg_resources

简单示例:

from aioscrapy import Spider, logger


class MyspiderSpider(Spider):
    name = 'myspider'
    custom_settings = {
        "CLOSE_SPIDER_ON_IDLE": True
    }
    start_urls = ["https://quotes.toscrape.com"]

    @staticmethod
    async def process_request(request, spider):
        """ request middleware """
        pass

    @staticmethod
    async def process_response(request, response, spider):
        """ response middleware """
        return response

    @staticmethod
    async def process_exception(request, exception, spider):
        """ exception middleware """
        pass

    async def parse(self, response):
        for quote in response.css('div.quote'):
            item = {
                'author': quote.xpath('span/small/text()').get(),
                'text': quote.css('span.text::text').get(),
            }
            yield item

    async def process_item(self, item):
        logger.info(item)


if __name__ == '__main__':
    MyspiderSpider.start()

文档目录

安装指南

快速入门

核心概念

爬虫指南

下载器

中间件

管道

队列

请求过滤器

代理

数据库连接

分布式部署

配置参考

API参考

示例

scrapy-zyte-api

通过 Zyte API 处理 url。实现 Zyte API 无缝集成到 Scrapy 的插件。Zyte API(前身 Crawlera),由 Scrapy 框架开发团队 Zyte(原 Scrapinghub) 推出的一体化反爬抓取 API

简单一句话: 你只需要发送一个请求给到 Zyte 接口,所有反爬难题全部交给云端处理:IP 轮换、TLS 指纹、Cloudflare 5 秒盾 JS 挑战、Turnstile、reCAPTCHA v2/v3、JS 渲染、会话 Cookie 管理,最后直接返回渲染完成的网页源码。

scrapy-impersonate-rnet

Scrapy 下载处理程序:通过 rnet 来转发请求,从而实现与浏览器相同的 TLS/HTTP2 指纹识别功能。它将所有的 HTTP/HTTPS 请求都通过 rnet 进行转发。这样一来,你的爬虫就能通过 BoringSSL 技术,获得与浏览器相同的 TLS 和 HTTP/2 协议相关的数据。

如果没有 scrapy-rnet ,Scrapy 将使用 Python 的标准 urllib3 / twisted 堆栈来处理请求。这样一来,发送的请求很容易被识别为机器人发送的信号。而使用 scrapy-rnet 后,无论是在 TLS 还是 HTTP/2 层面上,发送的请求都与真正的 Chrome、Firefox 或 Safari 浏览器发出的请求无法区分。

pypi:https://pypi.org/project/scrapy-impersonate-rnet/

scrapy-common-downloadhandler (重要)

这是一个复合型的 Scrapy 下载处理程序,它将 cloudscraper、curl_cffi(通过 scrapy-impersonate 实现)以及 Twisted HTTP/1.1 集成到了一个处理程序中。同时,该处理程序还支持通过 request.meta 实现针对每个请求的路由处理。

pypi:https://pypi.org/project/scrapy-common-downloadhandler/

HTTP11DownloadHandler                   <- Twisted HTTP/1.1 (fallback)
  └── ImpersonateDownloadHandler       <- curl_cffi ( meta["impersonate"])
        └── CommonDownloadHandler      <- cloudscraper ( meta["use_cloudscraper"] )

hamro-scrapy-playwright

对 scrapy-playwright 进行手动修改,以便使用自定义的隐身版 Chromium 浏览器。

可直接替代现有的 scrapy-playwright 项目。可选的 Cloudflare Turnstile 自动点击功能集成

pypi:https://pypi.org/project/hamro-scrapy-playwright/

安装:pip install hamro-scrapy-playwright

import scrapy
from scrapy import Request


class ExampleSpider(scrapy.Spider):
    name = "example"

    custom_settings = {
        "DOWNLOAD_HANDLERS": {
            "http": "hamro_scrapy_playwright.handler.HamroPlaywrightDownloadHandler",
            "https": "hamro_scrapy_playwright.handler.HamroPlaywrightDownloadHandler",
        },
        "TWISTED_REACTOR": "twisted.internet.asyncioreactor.AsyncioSelectorReactor",
        "PLAYWRIGHT_BROWSER_TYPE": "chromium",
        "PLAYWRIGHT_LAUNCH_OPTIONS": {
            "headless": False,
        },
        "PLAYWRIGHT_PROCESS_REQUEST_HEADERS": None,
    }

    async def start(self):
        yield Request(
            "https://www.example.com/",
            meta={
                "playwright": True,
                "playwright_include_page": True,
            },
            callback=self.parse,
        )

    async def parse(self, response):
        page = response.meta["playwright_page"]

        try:
            await page.wait_for_load_state("networkidle")

            await page.evaluate(
                """
                () => {
                    document.body.style.backgroundColor = "#225577";
                }
                """
            )

            await page.wait_for_timeout(4000)

            self.logger.info("Title: %s", await page.title())

        finally:
            await page.close()

全局模式(推荐用于开发)

import tempfile

custom_settings = {
    "DOWNLOAD_HANDLERS": {
        "http": "hamro_scrapy_playwright.handler.HamroPlaywrightDownloadHandler",
        "https": "hamro_scrapy_playwright.handler.HamroPlaywrightDownloadHandler",
    },
    "TWISTED_REACTOR": "twisted.internet.asyncioreactor.AsyncioSelectorReactor",
    "PLAYWRIGHT_BROWSER_TYPE": "chromium",
    "PLAYWRIGHT_LAUNCH_OPTIONS": {
        "headless": False,
    },
    "PLAYWRIGHT_CONTEXTS": {
        "persistent": {
            "user_data_dir": tempfile.mkdtemp(),
        }
    },
    "HAMRO_CF_AUTOCLICK": True,
    "HTTPERROR_ALLOWED_CODES": [403, 429],
}

scrapy-seleniumbase-cdp

这款 Scrapy 下载中间件利用 SeleniumBase 的纯 CDP 模式来发送请求,从而能够绕过大多数反机器人程序的防护机制(例如 CloudFlare)。

pypi:https://pypi.org/project/scrapy-seleniumbase-cdp/ 

scrapy-zenrows-middleware

一款 Scrapy 中间件,可轻松实现与 ZenRows Scraper API 的连接,无需繁琐的配置。

通过 Premium Proxies 服务,ZenRows 为用户提供了来自 190 多个国家的 5500 多万个住宅类 IP 地址。该服务能够确保 99.9%的正常运行时间,从而实现高度可靠的网页数据抓取功能。我们的系统还具备先进的 IP 识别规避机制、请求头信息调整功能以及 IP 地址管理功能,因此用户无需手动配置各种参数,就能轻松抓取那些保护措施最严格的网站的数据。

ZenRows 使得绕过复杂的反机器人程序变得容易,同时也能轻松处理那些包含大量 JavaScript 代码的网站,还能动态地与网页元素进行交互——当然,这一切都需要在正确启用相关功能的前提下才能实现。

scrapy-download-handlers

用于 Scrapy 的半官方下载处理程序。更多信息请参阅 Scrapy download handler 相关文档。

这些处理器的基准是 Scrapy 的默认处理器 HTTP11DownloadHandler。该处理器基于 Twisted 框架开发,支持 HTTP/1.1 协议。与 HTTP11DownloadHandler 的功能兼容性是开发目标之一,但这一目标并非总能实现。此外,并非所有功能都存在于所有的处理器中(这一情况将来也可能会发生变化)。有些 HTTP11DownloadHandler 不支持的功能,比如对 HTTP/2 协议的支持,以及某些处理器独有的功能,可能会被实现,也可能不会被实现。详情请参阅各处理器的具体描述。

一些差异:

Handler

HTTP/2

Proxies

Impersonation

TLS implementation

(HTTP11DownloadHandler)

Not possible

Yes

Not possible

cryptography

AiohttpDownloadHandler

Not possible

Yes

Not possible

Stdlib ssl

CurlCffiDownloadHandler

Yes

Yes

No

libcurl

HttpxDownloadHandler

Yes

Yes

Not possible

Stdlib ssl

NiquestsDownloadHandler

Yes

Yes

Not possible

Stdlib ssl

PyreqwestDownloadHandler

Yes

Not possible

Not possible

rustls

scrapy-rotating-proxy-middleware

一种 Scrapy 下载中间件,能够自动切换代理服务器,并在遇到 Cloudflare/DataDome/PerimeterX 的封锁时重新尝试连接。

这是一个即用型的 Scrapy 下载中间件,能够自动切换代理服务器,并在遇到封锁时重新尝试连接——可以应对 403 、 429 、Cloudflare 的“请稍候”提示、DataDome 以及 PerimeterX 带来的封锁问题。只需将此中间件指向静态代理列表或某个循环使用的代理服务器,就能避免蜘蛛程序因封锁而无法正常运行。

crawl-bot

Crawl Bot 是一个基于 Scrapy 开发的项目,用于爬取一个或多个域名上的页面内容,并提取页面的元数据、标题、图片和链接等信息。

scrapyx-pkgs

ScrapyX 软件包套件,默认会安装 scrapyx-core 和 scrapyx-mw 这两个组件。

  • scrapyx-core - 共享的 Scrapy 基础爬虫框架 + 预定义的服务配置管理机制
  • scrapyx-mw – 可组合的 Scrapy 中间件(API/会话管理、调试功能、验证码处理、代理服务器切换等)

spidermoon、spidermon

spidermoon 是一个面向 Python 爬虫的通用监控 SDK。它可以安装到 Scrapy、Requests、aiohttp、Selenium 或自研爬虫项目中,统一采集爬虫运行指标、数据产物、文件下载、代码来源、数据质量、上报状态和爬虫运行维度。

  • 请求监控:请求数、成功率、失败率、响应时间、QPS。
  • 本地指标:/metrics/health
  • 主动上报:Reporter 定时 POST 到监控平台。
  • 数据血缘:记录 item、文件、输出目标、来源 URL、代码位置。
  • 数据质量:记录校验失败、重复、丢弃、解析失败。
  • 上报诊断:Reporter 状态、端点测试、手动 flush。
  • 爬虫运行维度:任务队列、任务重试、代理失败、封禁、限流、验证码、资源、日志、断点续爬。
  • 框架接入:装饰器、上下文管理器、Requests、Scrapy、aiohttp、Selenium 适配器。

omk-crawl

智能爬虫工具箱——Web 自动扩展、自适应 API 发现、TLS 轮换、Android/iOS 表面、Naver/Baemin 客户端

ohfuck

不同于 requests/scrapy 等基于 HTTP 请求的爬虫,OhFuck 通过 Chrome DevTools Protocol 直接控制 Chromium 浏览器。拥有完整的 JavaScript 执行环境、真实渲染引擎,以及独有的「人机验证弹窗」功能。

crawlingo 

Crawlingo --- 下一代网络抓取和监控框架。

pypi:https://pypi.org/project/crawlingo/ 

coocan 

Coocan 是一个简洁、高效的 Python 异步爬虫框架,专为快速开发而设计。它基于 httpx 和 asyncio,提供了简单易用的 API,让你能够快速构建高性能的网络爬虫。

  • 轻量级 - 核心代码简洁,依赖少,易于理解和扩展
  • 异步高效 - 基于 asyncio,充分利用异步 I/O 提升爬取效率
  • 简单易用 - 类 Scrapy 的 API 设计,上手即用
  • 功能完善 - 内置请求重试、优先级队列、代理支持、数据处理等功能
  • 开箱即用 - 自带 XPath/CSS 选择器,随机 User-Agent,命令行工具等
from coocan import Request, Response, MiniSpider


class TestSpider(MiniSpider):
    start_urls = ["https://github.com/markadc/coocan"]
    max_concurrency = 10

    def middleware(self, request: Request):
        request.headers["Referer"] = "https://github.com"

    def parse(self, response: Response):
        print(response.status_code)
        print(response.get_one("//title/text()"))


if __name__ == '__main__':
    s = TestSpider()
    s.go()

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值