按下Ctrl+C后Pyppeteer脚本崩溃并陷入无限循环的问题
先贴出你的代码方便参考:
import asyncio import pyppeteer async def hi(): ret = await pyppeteer.launch() await asyncio.sleep(10) return ret browser = asyncio.get_event_loop().run_until_complete(hi())
问题现象
运行这段代码后,如果在10秒内按下Ctrl+C,会疯狂输出一堆报错,核心错误是RuntimeError: This event loop is already running,还有一堆WebSocket连接关闭的异常,甚至会陷入类似无限循环的报错状态。
问题根源
你说的没错,Pyppeteer确实注册了atexit处理程序来清理Chrome进程,但这个处理程序的实现有个致命问题:
当你按下Ctrl+C时,Python会抛出KeyboardInterrupt,此时asyncio.get_event_loop().run_until_complete(hi())还没执行完,事件循环仍然处于运行状态。而Pyppeteer的atexit回调(也就是launcher.py里的_close_process方法)直接调用了:
asyncio.get_event_loop().run_until_complete(self.killChrome())
在Python 3.6及以上版本中,事件循环是不允许嵌套调用run_until_complete的,这就直接抛出了RuntimeError。
更糟的是,这个异常会打断正常的资源清理流程,导致Chrome进程的WebSocket连接异常关闭,未处理的任务异常不断被抛出,最终形成你看到的无限报错场景。
修复方案
这里给你几个可行的解决办法,按推荐程度排序:
1. 使用Python 3.7+的asyncio.run(最简洁)
asyncio.run会自动管理事件循环的生命周期,包括异常时的优雅清理,完全避免嵌套运行的问题。同时我们加上finally块确保浏览器一定会被关闭:
import asyncio import pyppeteer async def hi(): browser = await pyppeteer.launch() try: await asyncio.sleep(10) return browser finally: # 不管有没有异常,都关闭浏览器 await browser.close() if __name__ == "__main__": asyncio.run(hi())
2. 手动处理SIGINT信号(兼容Python 3.6)
如果你还在使用Python 3.6,可以自己注册信号处理函数,在收到Ctrl+C时先优雅关闭浏览器,再终止事件循环:
import asyncio import pyppeteer import signal browser_instance = None def handle_ctrl_c(signum, frame): # 提交关闭任务到事件循环 asyncio.create_task(shutdown()) async def shutdown(): global browser_instance if browser_instance: await browser_instance.close() # 停止事件循环 asyncio.get_event_loop().stop() async def hi(): global browser_instance browser_instance = await pyppeteer.launch() await asyncio.sleep(10) return browser_instance if __name__ == "__main__": loop = asyncio.get_event_loop() # 注册SIGINT信号处理 signal.signal(signal.SIGINT, handle_ctrl_c) try: loop.run_until_complete(hi()) finally: loop.close()
3. 修改Pyppeteer源码(不推荐)
如果你不想改自己的代码,可以尝试修改Pyppeteer的launcher.py文件,把_close_process方法里的run_until_complete改成create_task:
把原来的:
asyncio.get_event_loop().run_until_complete(self.killChrome())
替换成:
asyncio.get_event_loop().create_task(self.killChrome())
不过这种方式需要修改第三方库的源码,后续升级Pyppeteer时会覆盖你的修改,所以只作为临时方案。
总结
核心问题就是Pyppeteer的atexit处理程序没有考虑到事件循环仍在运行的情况,错误地嵌套调用了run_until_complete,引发了连锁错误。通过上面的方案都能解决这个问题,个人推荐优先用asyncio.run的方案,最省心。
内容的提问来源于stack exchange,提问作者d33tah

