彻底解决DrissionPage中page.quit()退出后僵尸线程问题:从原理到根治方案

彻底解决DrissionPage中page.quit()退出后僵尸线程问题:从原理到根治方案

【免费下载链接】DrissionPage 基于python的网页自动化工具。既能控制浏览器,也能收发数据包。可兼顾浏览器自动化的便利性和requests的高效率。功能强大,内置无数人性化设计和便捷功能。语法简洁而优雅,代码量少。 【免费下载链接】DrissionPage 项目地址: https://gitcode.com/g1879/DrissionPage

引言:你是否也被僵尸线程困扰?

在使用DrissionPage进行网页自动化开发时,你是否遇到过这样的情况:调用page.quit()后,程序看似已经退出,但在任务管理器中仍然能看到残留的Chromium进程?这些"僵尸线程"不仅会占用系统资源,还可能导致后续自动化任务失败或状态异常。本文将深入分析这一问题的根源,并提供一套完整的解决方案,帮助你彻底摆脱僵尸线程的困扰。

读完本文后,你将能够:

  • 理解DrissionPage中僵尸线程产生的根本原因
  • 掌握三种不同层面的解决方案:临时规避、代码修复和终极根治
  • 学会如何验证僵尸线程是否被彻底清除
  • 了解DrissionPage浏览器管理的内部工作机制

问题分析:僵尸线程是如何产生的?

僵尸线程的表现特征

僵尸线程(Zombie Thread)通常表现为以下特征:

  • 调用page.quit()后,程序已退出,但Chromium进程仍在后台运行
  • 任务管理器中出现多个chrome.exechromium.exe进程
  • 再次运行程序时可能出现"端口已被占用"等错误
  • 系统资源占用逐渐增加,最终可能导致系统变慢或程序崩溃

DrissionPage退出流程分析

要理解僵尸线程的产生原因,我们首先需要了解DrissionPage的退出流程。以下是page.quit()方法的调用链:

mermaid

从上述流程可以看出,DrissionPage在退出时理论上应该:

  1. 发送CDP命令关闭浏览器
  2. 停止浏览器驱动
  3. (可选)强制终止所有相关进程
  4. (可选)删除用户数据目录

根本原因定位

通过分析chromium.pychromium_page.py的源代码,我们发现了几个可能导致僵尸线程的关键点:

  1. 进程终止不彻底:在Chromium.quit()方法中,虽然尝试终止进程,但依赖于SystemInfo.getProcessInfo获取进程ID,这可能无法获取到所有相关进程。

  2. 资源清理顺序问题:驱动停止和进程终止的顺序可能存在问题,导致部分资源未能正确释放。

  3. 多线程/多进程管理问题:浏览器实例和标签页的管理采用了全局字典_BROWSERS_PAGES,在某些情况下可能无法正确清理所有引用。

  4. 信号处理缺失:程序未正确处理系统信号(如SIGINT、SIGTERM),导致在用户强制终止程序时无法执行清理操作。

解决方案:从临时规避到彻底根治

方案一:临时规避措施

如果你需要立即解决问题,可以采用以下临时规避措施:

import psutil
from DrissionPage import ChromiumPage

def safe_quit(page):
    """安全退出页面,确保所有相关进程都被终止"""
    # 获取浏览器进程ID
    browser_pid = page.browser.process_id
    
    # 正常调用quit方法
    page.quit()
    
    # 额外检查并终止相关进程
    if browser_pid:
        try:
            # 获取主进程
            main_process = psutil.Process(browser_pid)
            # 获取所有子进程
            children = main_process.children(recursive=True)
            # 终止所有子进程
            for child in children:
                child.terminate()
            # 终止主进程
            main_process.terminate()
            
            # 等待进程终止
            gone, still_alive = psutil.wait_procs(children + [main_process], timeout=5)
            # 如果仍有进程存活,强制终止
            for p in still_alive:
                p.kill()
        except Exception as e:
            print(f"清理进程时出错: {e}")

# 使用示例
page = ChromiumPage()
try:
    # 执行自动化任务
    page.get("https://www.example.com")
    # ...其他操作...
finally:
    safe_quit(page)

这种方法通过额外的进程清理步骤,确保所有相关进程都被终止,从而避免僵尸线程的产生。

方案二:修改DrissionPage源代码

要彻底解决问题,我们需要修改DrissionPage的源代码。以下是具体的修改方案:

  1. 增强Chromium.quit()方法
--- a/DrissionPage/_base/chromium.py
+++ b/DrissionPage/_base/chromium.py
@@ -313,18 +313,35 @@ class Chromium(object):
         try:
             self._run_cdp('Browser.close')
         except PageDisconnectedError:
             pass
         self._driver.stop()
 
         drivers = list(self._all_drivers.values())
         for tab in drivers:
             for driver in tab:
                 driver.stop()
+        
+        # 增强的进程清理逻辑
+        self._cleanup_processes(timeout)
+
+        if not self.address.startswith('127.0.0.1'):
+            return
+
+        if del_data and not self._chromium_options.is_auto_port and self._chromium_options.user_data_path:
+            path = Path(self._chromium_options.user_data_path)
+            rmtree(path, True)
+
+    def _cleanup_processes(self, timeout):
+        """增强的进程清理逻辑"""
+        # 停止驱动
+        self._driver.stop()
+        
+        # 清理所有驱动
+        drivers = list(self._all_drivers.values())
+        for tab in drivers:
+            for driver in tab:
+                driver.stop()
 
         if force:
             pids = None
             try:
                 pids = [pid['id'] for pid in self._run_cdp('SystemInfo.getProcessInfo')['processInfo']]
             except:
                 pass
  1. 添加进程清理辅助方法
--- a/DrissionPage/_base/chromium.py
+++ b/DrissionPage/_base/chromium.py
@@ -355,6 +372,33 @@ class Chromium(object):
                 if ok:
                     break
 
         if del_data and not self._chromium_options.is_auto_port and self._chromium_options.user_data_path:
             path = Path(self._chromium_options.user_data_path)
             rmtree(path, True)
+    
+    def _cleanup_processes(self, timeout):
+        """增强的进程清理逻辑"""
+        import psutil
+        from time import perf_counter
+        
+        # 如果有进程ID,尝试通过psutil终止所有相关进程
+        if self._process_id:
+            try:
+                # 获取主进程
+                main_process = psutil.Process(self._process_id)
+                # 获取所有子进程
+                children = main_process.children(recursive=True)
+                # 终止所有子进程
+                for child in children:
+                    child.terminate()
+                # 终止主进程
+                main_process.terminate()
+                
+                # 等待进程终止
+                end_time = perf_counter() + timeout
+                while perf_counter() < end_time:
+                    if not any(p.is_alive() for p in children + [main_process]):
+                        break
+                    psutil.sleep(0.1)
+                    
+                # 强制终止仍存活的进程
+                for p in children + [main_process]:
+                    if p.is_alive():
+                        p.kill()
+            except Exception:
+                pass
  1. 修改ChromiumPage.quit()方法
--- a/DrissionPage/_pages/chromium_page.py
+++ b/DrissionPage/_pages/chromium_page.py
@@ -76,7 +76,12 @@ class ChromiumPage(ChromiumBase):
         self.browser.close_tabs(tabs_or_ids=tabs_or_ids, others=others)
 
     def quit(self, timeout=5, force=True, del_data=False):
-        self.browser.quit(timeout, force, del_data=del_data)
+        """退出浏览器并清理资源"""
+        # 调用浏览器的quit方法
+        self.browser.quit(timeout, force, del_data=del_data)
+        
+        # 从全局缓存中移除
+        ChromiumPage._PAGES.pop(self.browser.id, None)

方案三:终极根治方案

为了彻底解决僵尸线程问题,我们需要实现一个完整的资源管理和进程清理机制:

  1. 实现上下文管理器:为ChromiumPage和相关类实现上下文管理器接口,确保资源能够被正确释放。
from contextlib import contextmanager

@contextmanager
def chromium_page_context(*args, **kwargs):
    """ChromiumPage的上下文管理器,确保正确退出"""
    page = None
    try:
        page = ChromiumPage(*args, **kwargs)
        yield page
    finally:
        if page:
            page.quit(force=True)
            # 额外的清理步骤
            ChromiumPage._PAGES.pop(page.browser.id, None)
            Chromium._BROWSERS.pop(page.browser.id, None)

# 使用示例
with chromium_page_context() as page:
    page.get("https://www.example.com")
    # ...执行操作...
  1. 添加信号处理:在程序中添加信号处理逻辑,确保在收到终止信号时能够正确清理资源。
import signal
import sys

def handle_exit_signal(signum, frame):
    """处理退出信号"""
    print(f"收到退出信号 {signum},正在清理资源...")
    
    # 清理所有ChromiumPage实例
    for page in list(ChromiumPage._PAGES.values()):
        try:
            page.quit(force=True)
        except Exception as e:
            print(f"清理页面时出错: {e}")
    
    # 清理所有Chromium实例
    for browser in list(Chromium._BROWSERS.values()):
        try:
            browser.quit(force=True)
        except Exception as e:
            print(f"清理浏览器时出错: {e}")
    
    # 清空全局缓存
    ChromiumPage._PAGES.clear()
    Chromium._BROWSERS.clear()
    
    sys.exit(0)

# 注册信号处理器
signal.signal(signal.SIGINT, handle_exit_signal)  # Ctrl+C
signal.signal(signal.SIGTERM, handle_exit_signal) # 终止信号
  1. 实现资源监控工具:创建一个简单的资源监控工具,帮助用户识别和清理僵尸进程。
def list_drission_processes():
    """列出所有可能与DrissionPage相关的进程"""
    import psutil
    
    drission_processes = []
    for proc in psutil.process_iter(['pid', 'name', 'cmdline']):
        try:
            # 查找可能与Chromium/Chrome相关的进程
            if 'chrome' in proc.name().lower() or 'chromium' in proc.name().lower():
                # 检查命令行参数中是否包含DrissionPage特征
                cmdline = ' '.join(proc.cmdline())
                if '--remote-debugging-port' in cmdline or 'DrissionPage' in cmdline:
                    drission_processes.append(proc)
        except (psutil.NoSuchProcess, psutil.AccessDenied):
            continue
    
    return drission_processes

def kill_all_drission_processes():
    """终止所有可能与DrissionPage相关的进程"""
    processes = list_drission_processes()
    if not processes:
        print("未找到DrissionPage相关进程")
        return
    
    print(f"找到{len(processes)}个DrissionPage相关进程,正在终止...")
    for proc in processes:
        try:
            print(f"终止进程: {proc.pid} ({proc.name()})")
            proc.terminate()
        except Exception as e:
            print(f"终止进程{proc.pid}失败: {e}")
    
    # 检查是否还有存活的进程
    gone, alive = psutil.wait_procs(processes, timeout=5)
    if alive:
        print(f"以下进程无法正常终止,将强制终止: {[p.pid for p in alive]}")
        for proc in alive:
            try:
                proc.kill()
            except Exception as e:
                print(f"强制终止进程{proc.pid}失败: {e}")

验证方案:如何确认僵尸线程已被清除

为了验证我们的解决方案是否有效,我们可以采用以下方法:

1. 进程监控法

import psutil
import time
from DrissionPage import ChromiumPage

def monitor_processes():
    """监控DrissionPage相关进程"""
    # 获取初始进程列表
    initial_processes = set(psutil.pids())
    
    # 创建页面
    page = ChromiumPage()
    page.get("https://www.example.com")
    
    # 获取创建的进程
    created_processes = set(psutil.pids()) - initial_processes
    print(f"创建的进程: {created_processes}")
    
    # 等待一段时间
    time.sleep(2)
    
    # 退出页面
    page.quit(force=True)
    
    # 检查进程是否已终止
    remaining_processes = []
    for pid in created_processes:
        try:
            psutil.Process(pid)
            remaining_processes.append(pid)
        except psutil.NoSuchProcess:
            continue
    
    if remaining_processes:
        print(f"警告: 以下进程未被终止: {remaining_processes}")
        return False
    else:
        print("所有进程均已被终止")
        return True

# 运行监控
if monitor_processes():
    print("测试通过")
else:
    print("测试失败")

2. 资源占用监控

使用系统工具监控资源占用情况:

  1. 在程序运行前记录CPU和内存占用
  2. 运行程序并执行quit()
  3. 观察CPU和内存占用是否恢复到运行前水平

对于Linux/macOS系统,可以使用tophtop命令;对于Windows系统,可以使用任务管理器。

3. 多轮测试法

连续多次运行自动化任务,观察是否会出现资源累积或端口冲突问题:

def multiple_runs_test(num_runs=10):
    """多轮运行测试"""
    for i in range(num_runs):
        print(f"第{i+1}轮测试...")
        try:
            page = ChromiumPage()
            page.get("https://www.example.com")
            page.quit(force=True)
            print(f"第{i+1}轮测试成功")
        except Exception as e:
            print(f"第{i+1}轮测试失败: {e}")
            return False
    return True

if multiple_runs_test():
    print("多轮测试通过")
else:
    print("多轮测试失败")

最佳实践:避免僵尸线程的预防措施

除了上述解决方案外,遵循以下最佳实践可以帮助你避免僵尸线程问题:

1. 使用上下文管理器

始终使用上下文管理器来创建和管理ChromiumPage实例:

with ChromiumPage() as page:
    page.get("https://www.example.com")
    # ...执行操作...
# 页面会自动关闭,资源会被释放

2. 显式设置force参数

在调用quit()方法时,显式设置force=True

page.quit(force=True)  # 强制退出,确保进程被终止

3. 定期清理全局缓存

定期清理ChromiumPage._PAGESChromium._BROWSERS全局缓存:

# 清理所有页面缓存
ChromiumPage._PAGES.clear()
# 清理所有浏览器缓存
Chromium._BROWSERS.clear()

4. 监控资源使用情况

在长时间运行的程序中,定期监控资源使用情况:

def monitor_resources(interval=60):
    """定期监控资源使用情况"""
    import psutil
    import time
    
    while True:
        # 检查DrissionPage相关进程数量
        drission_procs = list_drission_processes()
        if len(drission_procs) > 5:  # 设置一个合理的阈值
            print(f"警告: DrissionPage相关进程数量过多 ({len(drission_procs)})")
            # 可以在这里添加自动清理逻辑
        
        time.sleep(interval)

# 在后台线程中启动资源监控
import threading
threading.Thread(target=monitor_resources, daemon=True).start()

5. 使用独立的用户数据目录

为每个自动化任务使用独立的用户数据目录,避免资源冲突:

from DrissionPage import ChromiumOptions, ChromiumPage

# 创建自定义选项,使用独立的用户数据目录
opts = ChromiumOptions()
opts.set_user_data_path(f"/tmp/drission_data_{hash(time.time())}")

# 使用自定义选项创建页面
page = ChromiumPage(opts)

结论与展望

僵尸线程问题是DrissionPage使用过程中的一个常见痛点,但通过本文提供的解决方案,你可以彻底解决这一问题。我们从临时规避措施到彻底根治方案,逐步深入,不仅解决了表面问题,还深入理解了DrissionPage的内部工作机制。

未来,我们希望DrissionPage能够:

  1. 改进浏览器进程管理机制,提供更可靠的退出流程
  2. 实现完整的上下文管理器接口,简化资源管理
  3. 添加内置的资源监控和清理工具
  4. 提供更详细的调试日志,帮助用户定位问题

通过这些改进,DrissionPage将成为一个更加健壮和可靠的网页自动化工具,为用户提供更好的使用体验。

最后,我们强烈建议所有DrissionPage用户采用本文提供的最佳实践,以避免僵尸线程和其他资源管理问题的发生。

【免费下载链接】DrissionPage 基于python的网页自动化工具。既能控制浏览器,也能收发数据包。可兼顾浏览器自动化的便利性和requests的高效率。功能强大,内置无数人性化设计和便捷功能。语法简洁而优雅,代码量少。 【免费下载链接】DrissionPage 项目地址: https://gitcode.com/g1879/DrissionPage

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值