Scrapy+Selenium爬虫WebDriver实例过多致崩溃,求调度控制方法
控制Scrapy调度器配合Selenium的并发WebDriver实例数
这个问题我之前帮朋友解决过,核心就是要限制同时运行的WebDriver进程数量,并且让Scrapy的调度逻辑配合这个限制——当达到最大并发数时,暂时停止调度新的Selenium请求,直到有空闲的进程释放出来。
下面是最实用的两种方案,从简单到进阶:
方案一:用信号量(Semaphore)+ 下载中间件控制并发
这是最直接的实现方式,通过Scrapy的下载中间件拦截需要用Selenium处理的请求,用threading.Semaphore来限制同时执行的Selenium任务数。当信号量被占满时,新的请求会被阻塞,直到有之前的任务完成并释放信号量,自然就不会让Scrapy无限制调度新URL导致进程爆炸。
具体实现步骤:
- 在你的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()
- 在
settings.py中配置中间件和最大并发数:
# 启用自定义的Selenium中间件,权重设为543(在默认中间件之后) DOWNLOADER_MIDDLEWARES = { 'your_project_name.middlewares.SeleniumConcurrentMiddleware': 543, } # 设置最大并发WebDriver进程数,根据你的服务器内存调整,比如5个 SELENIUM_MAX_CONCURRENT = 5 # 同时建议调整Scrapy的全局并发数,避免调度过多等待的请求 CONCURRENT_REQUESTS = 5
- 在你的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
相关产品推荐
相关产品推荐

