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

Python中Celery任务被撤销时如何关闭Selenium WebDriver

解决Celery任务终止后WebDriver残留的问题

你遇到的这个问题很常见——app.control.revoke()只是终止了Celery Worker进程,但WebDriver是一个独立的子进程/浏览器实例,Celery的撤销操作不会自动清理它。下面是几个靠谱的解决思路,按实用性排序:

方案1:利用Celery的task_revoked信号做清理

Celery提供了任务生命周期的信号,其中task_revoked会在任务被撤销时触发。你可以在这个信号的处理函数里,针对被撤销的任务ID,找到对应的WebDriver实例并关闭它。

首先,你需要在任务运行时,把任务ID和对应的WebDriver实例关联存储起来(单Worker场景用全局字典即可,多Worker建议用Redis这类共享存储):

# 比如在你的任务模块里
from celery import signals
from selenium import webdriver

# 单Worker场景用全局字典,多Worker换Redis/数据库
driver_map = {}

@signals.task_revoked.connect
def handle_task_revoked(sender=None, request=None, **kwargs):
    task_id = request.id
    if task_id in driver_map:
        driver = driver_map[task_id]
        try:
            driver.quit()  # 用quit()而非close(),quit会彻底终止驱动进程
            del driver_map[task_id]
            print(f"已为任务{task_id}关闭WebDriver")
        except Exception as e:
            print(f"关闭WebDriver失败: {str(e)}")

# 你的Celery任务
@app.task(bind=True)
def run_selenium_task(self):
    driver = webdriver.Chrome()
    # 绑定当前任务ID与Driver实例
    driver_map[self.request.id] = driver
    try:
        # 这里写你的Selenium业务逻辑
        driver.get("https://example.com")
        # ... 其他操作
    finally:
        # 任务正常完成时也要清理字典和Driver
        driver.quit()
        if self.request.id in driver_map:
            del driver_map[self.request.id]

如果是多Worker分布式场景,全局字典不生效,这时候可以存储任务ID和Driver的进程ID,在信号处理函数里调用系统命令(比如pkill/taskkill)杀死对应的浏览器进程。

方案2:在任务内部捕获终止信号

当你用terminate=True调用revoke时,Celery会给Worker进程发送SIGTERM信号(Windows下是类似的终止信号)。你可以在任务里注册信号处理函数,收到终止信号时主动关闭WebDriver。

示例代码:

import signal
from selenium import webdriver
from celery import Celery

app = Celery('proj', broker='redis://localhost:6379/0')

def handle_termination(signum, frame, driver):
    print("收到终止信号,正在关闭WebDriver...")
    driver.quit()
    exit(0)

@app.task(bind=True)
def selenium_task(self):
    driver = webdriver.Chrome()
    # 注册SIGTERM信号处理函数,传入driver实例
    signal.signal(signal.SIGTERM, lambda s, f: handle_termination(s, f, driver))
    
    try:
        # 业务逻辑
        driver.get("https://example.com")
        # ... 耗时操作
    except Exception as e:
        driver.quit()
        raise e

注意:Windows系统需要调整信号类型(比如signal.CTRL_BREAK_EVENT),如果Worker用了solo以外的进程池(比如prefork),信号处理逻辑可能需要额外适配。

方案3:用上下文管理器做兜底

不管任务是正常完成还是被终止,用上下文管理器(with语句)管理WebDriver的生命周期,确保资源被释放。虽然强制终止时finally块可能不执行,但上下文的__exit__方法多数情况下会触发。

示例:

from selenium import webdriver
from celery import Celery

app = Celery('proj', broker='redis://localhost:6379/0')

# 自定义WebDriver上下文管理器
class WebDriverContext:
    def __init__(self):
        self.driver = None
    
    def __enter__(self):
        self.driver = webdriver.Chrome()
        return self.driver
    
    def __exit__(self, exc_type, exc_val, exc_tb):
        if self.driver:
            self.driver.quit()
            print("WebDriver已关闭")

@app.task
def selenium_task():
    with WebDriverContext() as driver:
        # 业务逻辑
        driver.get("https://example.com")
        # ... 操作

建议把这个方案和前面的信号处理结合使用,作为正常流程的兜底。

方案4:撤销任务时主动查找并关闭Driver

如果你不想用信号机制,也可以在调用revoke后,主动通过任务ID找到对应的Driver进程并杀死。比如在任务启动时把Driver的进程ID存在Redis,撤销时取出PID并终止进程。

任务代码:

import os
import redis
from selenium import webdriver
from celery import Celery

app = Celery('proj', broker='redis://localhost:6379/0')
r = redis.Redis(host='localhost', port=6379, db=0)

@app.task(bind=True)
def selenium_task(self):
    driver = webdriver.Chrome()
    # 获取Chrome驱动的进程PID
    driver_pid = driver.service.process.pid
    # 存储任务ID与PID的映射
    r.set(f"task_{self.request.id}_driver_pid", driver_pid)
    
    try:
        # 业务逻辑
        driver.get("https://example.com")
        # ...
    finally:
        driver.quit()
        r.delete(f"task_{self.request.id}_driver_pid")

撤销任务时的处理:

from proj.celery import app
import redis
import os
import signal

r = redis.Redis(host='localhost', port=6379, db=0)

def revoke_task_and_close_driver(task_id):
    # 先撤销Celery任务
    app.control.revoke(task_id, terminate=True)
    # 获取Driver的PID
    driver_pid = r.get(f"task_{task_id}_driver_pid")
    if driver_pid:
        driver_pid = int(driver_pid)
        try:
            os.kill(driver_pid, signal.SIGTERM)
            print(f"已杀死WebDriver进程{driver_pid}")
            r.delete(f"task_{task_id}_driver_pid")
        except OSError as e:
            print(f"杀死进程失败: {str(e)}")

总结一下,最推荐方案1+方案3的组合:用Celery信号处理撤销时的清理,用上下文管理器做正常流程的兜底,这样不管任务是正常结束还是被撤销,WebDriver都能被正确关闭。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.04.30 20:39:04