You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Scrapy+Selenium爬虫WebDriver实例过多致崩溃,求调度控制方法

控制Scrapy调度器配合Selenium的并发WebDriver实例数

这个问题我之前帮朋友解决过,核心就是要限制同时运行的WebDriver进程数量,并且让Scrapy的调度逻辑配合这个限制——当达到最大并发数时,暂时停止调度新的Selenium请求,直到有空闲的进程释放出来。

下面是最实用的两种方案,从简单到进阶:

方案一:用信号量(Semaphore)+ 下载中间件控制并发

这是最直接的实现方式,通过Scrapy的下载中间件拦截需要用Selenium处理的请求,用threading.Semaphore来限制同时执行的Selenium任务数。当信号量被占满时,新的请求会被阻塞,直到有之前的任务完成并释放信号量,自然就不会让Scrapy无限制调度新URL导致进程爆炸。

具体实现步骤:

  1. 在你的Scrapy项目中创建(或修改)middlewares.py,添加以下代码:
from scrapy import signals
from scrapy.http import HtmlResponse
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
import threading

class SeleniumConcurrentMiddleware:
    def __init__(self, max_concurrent):
        # 初始化信号量,设置最大并发WebDriver数
        self.max_concurrent = max_concurrent
        self.semaphore = threading.Semaphore(max_concurrent)
        # 可选:维护一个WebDriver池,复用实例节省资源
        self.driver_pool = []

    @classmethod
    def from_crawler(cls, crawler):
        # 从settings读取配置的最大并发数,默认3个
        max_concurrent = crawler.settings.getint('SELENIUM_MAX_CONCURRENT', 3)
        middleware = cls(max_concurrent)
        # 爬虫关闭时清理所有WebDriver
        crawler.signals.connect(middleware.spider_closed, signal=signals.spider_closed)
        return middleware

    def process_request(self, request, spider):
        # 只处理标记了需要Selenium的请求(给request加meta标记)
        if not request.meta.get('use_selenium'):
            return None

        # 获取信号量:如果已达最大并发,这里会阻塞直到有空闲
        self.semaphore.acquire()

        # 从池里取空闲的WebDriver,没有就新建
        if self.driver_pool:
            driver = self.driver_pool.pop()
        else:
            # 配置无头模式等参数,减少资源占用
            chrome_options = Options()
            chrome_options.add_argument('--headless=new')
            chrome_options.add_argument('--disable-gpu')
            chrome_options.add_argument('--no-sandbox')
            chrome_options.add_argument('--disable-dev-shm-usage')  # 解决内存不足问题
            driver = webdriver.Chrome(options=chrome_options)

        try:
            driver.get(request.url)
            # 这里可以添加等待逻辑(比如等待元素加载)
            # 把渲染后的页面返回给Scrapy继续处理
            response = HtmlResponse(
                url=driver.current_url,
                body=driver.page_source,
                encoding='utf-8',
                request=request
            )
            request.meta['driver'] = driver
            return response
        except Exception as e:
            # 请求失败时,关闭driver并释放信号量
            driver.quit()
            self.semaphore.release()
            spider.logger.error(f"Selenium请求失败: {str(e)}")
            return request

    def process_response(self, request, response, spider):
        # 请求完成后,回收WebDriver或关闭
        if driver := request.meta.get('driver'):
            # 如果池还没满,就放回池复用;否则直接关闭
            if len(self.driver_pool) < self.max_concurrent:
                driver.delete_all_cookies()  # 清除会话数据,避免影响下一个请求
                self.driver_pool.append(driver)
            else:
                driver.quit()
            # 释放信号量,让新的请求可以执行
            self.semaphore.release()
        return response

    def process_exception(self, request, exception, spider):
        # 发生异常时也要确保释放信号量和关闭driver
        if driver := request.meta.get('driver'):
            driver.quit()
            self.semaphore.release()
        return None

    def spider_closed(self, spider):
        # 爬虫结束时,关闭所有池里的driver
        for driver in self.driver_pool:
            driver.quit()
        self.driver_pool.clear()
  1. 在settings.py中配置中间件和最大并发数:
# 启用自定义的Selenium中间件,权重设为543(在默认中间件之后)
DOWNLOADER_MIDDLEWARES = {
    'your_project_name.middlewares.SeleniumConcurrentMiddleware': 543,
}

# 设置最大并发WebDriver进程数,根据你的服务器内存调整,比如5个
SELENIUM_MAX_CONCURRENT = 5

# 同时建议调整Scrapy的全局并发数,避免调度过多等待的请求
CONCURRENT_REQUESTS = 5
  1. 在你的Spider中,给需要用Selenium处理的请求添加标记:
def parse(self, response):
    # 示例:给需要Selenium渲染的请求加meta标记
    yield scrapy.Request(
        url='https://target-url.com',
        callback=self.parse_detail,
        meta={'use_selenium': True}
    )

方案二:自定义调度器(进阶)

如果你需要更精细地控制Scrapy的调度逻辑(比如不仅限制Selenium请求,还要全局控制),可以自定义Scrapy的调度器。不过这个方案复杂度较高,适合对Scrapy内核熟悉的开发者:

核心思路是重写调度器的enqueue_request方法,在添加新请求前检查当前运行的WebDriver进程数,如果达到阈值就暂时拒绝入队,直到有进程释放。

不过一般来说,方案一已经足够解决你的问题,而且实现简单、维护成本低。

额外注意事项

  • WebDriver复用:上面的代码用到了driver池,复用实例可以大幅减少进程启动/关闭的开销,也更节省内存。如果网站有反爬机制(比如检测会话),记得在复用前清除Cookie和本地存储。
  • 资源监控:可以用psutil库实时监控WebDriver进程的内存使用,调整SELENIUM_MAX_CONCURRENT的数值,找到内存使用和爬取效率的平衡点。
  • 异常处理:一定要确保在任何情况下(请求成功、失败、异常)都释放信号量,否则会导致死锁,爬虫卡住。

内容的提问来源于stack exchange,提问作者Mansion

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.20 08:10:16