Scrapy下载中间件返回响应致__del__不执行 无法关闭Selenium浏览器
问题:Scrapy对接Selenium的下载中间件返回Response时__del__方法不执行
现象描述
编写Selenium对接的Scrapy下载中间件时,会出现两种完全不同的运行表现:
- 当
process_request方法返回自定义的HtmlResponse对象时,控制台仅输出打印的1和爬虫解析到的<200 http://httpbin.org/>,中间件的__del__方法完全不执行,Chrome浏览器进程残留无法正常关闭。 - 注释掉返回
HtmlResponse的代码、让process_request返回None时,程序可以正常执行__del__方法,输出Browser close~并关闭浏览器。
原因说明
出现差异的核心逻辑原因有两点:
- Python的
__del__方法本身没有可靠的执行时机保障,它只会在对象的引用计数降到0、被垃圾回收时才会触发,解释器正常退出阶段、存在循环引用、对象被框架持有引用的场景下,都可能不会执行。 - 当
process_request返回自定义的HtmlResponse时,这个响应对象会沿着Scrapy引擎、爬虫的全链路流转,整个爬虫运行周期内,这条请求/响应的处理链会隐式持有对创建响应的中间件实例的引用,导致中间件对象的引用计数始终无法归零;加上Scrapy主流程退出阶段的清理逻辑优先级更高,就会出现爬虫全部跑完,__del__也没被触发的情况。 - 当不返回自定义响应、
process_request返回None时,Scrapy不会在后续处理链路中持有中间件的引用,运行结束后中间件对象引用计数清零,垃圾回收时就可以正常触发__del__。
可行的收尾方案
不要依赖__del__做资源清理,直接利用Scrapy内置的spider_closed信号,在爬虫完全运行结束时触发浏览器关闭逻辑即可,不管中间件是否返回自定义响应都能稳定执行。
修改步骤
- 引入Scrapy的信号模块
- 在中间件的
from_crawler类初始化方法中,把自定义的浏览器关闭方法绑定到spider_closed信号 - 实现绑定的关闭方法,替代原来的
__del__逻辑
对应的核心修改代码如下:
from selenium import webdriver from selenium.webdriver.common.by import By from selenium.webdriver.support.wait import WebDriverWait from selenium.webdriver.support import expected_conditions as EC from selenium.common.exceptions import TimeoutException from scrapy.http import HtmlResponse from scrapy import signals class SeleniumMiddleware: """对接Selenium的下载中间件""" def __init__(self): self.browser = webdriver.Chrome() self.browser.maximize_window() self.wait = WebDriverWait(self.browser, 10) @classmethod def from_crawler(cls, crawler): # 初始化中间件实例 middleware = cls() # 绑定爬虫关闭信号到浏览器关闭方法 crawler.signals.connect(middleware.spider_closed, signal=signals.spider_closed) return middleware def process_request(self, request, spider): try: # 匹配需要Selenium访问的链接 if request.url in spider.start_urls: self.browser.get(request.url) # 等待页面元素加载完成 self.wait.until(EC.presence_of_element_located(( By.ID, 'operations-tag-HTTP_Methods'))) page_text = self.browser.page_source print('1') return HtmlResponse( url=request.url, body=page_text, encoding='utf-8', request=request, status=200 ) except TimeoutException: print('2') return HtmlResponse(url=request.url, status=500, request=request) def process_exception(self, request, exception, spider): print(f'Error: {exception}') return None def spider_closed(self): """爬虫运行结束时自动触发,关闭浏览器""" print('Browser close~') self.browser.quit()
注意:原有的
__del__方法可以直接删除,在Web框架场景下依赖它做资源清理非常不可靠。
内容的提问来源于stack exchange,提问作者wananzuiqingfeng
相关产品推荐
相关产品推荐

