使用Playwright编写Python爬虫时如何解决‘Too many open files’错误?
使用Playwright编写Python爬虫时如何解决‘Too many open files’错误?
看起来你已经尝试了不少常规方法来解决文件句柄耗尽的问题,但还是碰到了瓶颈——我来帮你梳理下现有代码里的潜在问题,再给出几个代码层面的优化方向,不用改系统限制也能搞定。
首先,先分析现有代码里可能遗漏的坑
open_pages队列的并发安全问题:你用deque来管理打开的页面,但异步环境下多个协程同时修改这个队列会有竞态条件,比如A协程刚把页面加入队列,B协程就因为达到max_open_pages把它弹出关闭,而A协程后续的清理逻辑又会尝试移除这个页面,导致页面句柄没被正确释放,或者重复关闭引发异常。- 页面清理的顺序问题:你的
fetch_page_content里是先关闭页面再从队列移除,这可能导致队列里残留无效的页面引用,时间长了积累下来就会耗光句柄。 - 浏览器重启时的资源残留:你定期重启浏览器,但重启前没有确保所有打开的页面都被彻底关闭,残留的页面句柄会一直占用资源。
针对性的代码优化方案
1. 给open_pages加异步锁,保证并发安全
异步环境下共享资源必须加锁保护,deque本身不是协程安全的,我们给它加个锁来避免竞态:
首先在你的爬虫类里初始化一个异步锁:
import asyncio class YourCrawler: def __init__(self): # 其他初始化代码... self.open_pages = deque() self.open_pages_lock = asyncio.Lock() # 新增异步锁
然后修改fetch_page_content里操作open_pages的部分,所有读写都加锁:
async def fetch_page_content( url: str, browser: Browser = None, context: BrowserContext = None, open_pages: deque = None, open_pages_lock: asyncio.Lock = None, # 新增锁参数 max_open_pages: int = 100, timeout: int = 60000, headless: bool = True, logger: logging.Logger = None, ) -> str | None: should_close_browser = browser is None should_close_context = context is None # 省略前面的浏览器/上下文初始化代码... if open_pages is not None and open_pages_lock is not None: async with open_pages_lock: if len(open_pages) >= max_open_pages: old_page = open_pages.popleft() try: await old_page.close() except Exception as e: if logger: logger.warning(f"Failed to close old page: {e}") page = await context.new_page() if open_pages is not None and open_pages_lock is not None: async with open_pages_lock: open_pages.append(page) try: response = await page.goto(url, timeout=timeout, wait_until="load") if not response or response.status >= 400: if logger: logger.error(f"Failed to fetch {url}") return None html = await page.content() return html except Exception as e: if logger: logger.warning(f"Error fetching {url}: {e}") return None finally: # 先从队列移除页面,再关闭 if open_pages is not None and open_pages_lock is not None: async with open_pages_lock: if page in open_pages: open_pages.remove(page) # 关闭页面并捕获异常 try: await page.close() except Exception as e: if logger: logger.warning(f"Failed to close page for {url}: {e}") # 省略上下文/浏览器关闭代码...
调用这个函数的时候,把锁传进去:
tasks = [ fetch_page_content( url=url, context=context, open_pages=self.open_pages, open_pages_lock=self.open_pages_lock, # 传入锁 max_open_pages=self.max_open_pages, logger=self.logger, ) for url, depth in batch ]
2. 用信号量主动限制并发页面数
比起“创建后再关闭旧页面”的被动方式,用asyncio.Semaphore可以从源头控制同时打开的页面数量,更可靠:
在爬虫类里初始化信号量:
class YourCrawler: def __init__(self): # 其他初始化代码... self.page_semaphore = asyncio.Semaphore(self.max_open_pages) # 限制同时打开的页面数
然后修改任务创建的逻辑,每个任务都先获取信号量再执行:
tasks = [] for url, depth in batch: async def fetch_task(u): async with self.page_semaphore: return await fetch_page_content( url=u, context=context, open_pages=self.open_pages, open_pages_lock=self.open_pages_lock, max_open_pages=self.max_open_pages, logger=self.logger, ) tasks.append(fetch_task(url))
这样就能确保同时运行的页面请求不会超过你设定的max_open_pages,从根源上减少句柄占用。
3. 优化浏览器重启时的资源清理
在重启浏览器和上下文之前,先彻底关闭所有残留的页面,清空队列:
if total_pages % self.restart_interval == 0 and total_pages != 0: self.logger.info("Restarting browser and context...") # 先关闭所有剩余页面 async with self.open_pages_lock: while self.open_pages: page = self.open_pages.popleft() try: await page.close() except Exception as e: self.logger.warning(f"Error closing page during restart: {e}") self.open_pages.clear() # 再关闭上下文和浏览器 await context.close() await browser.close() # 重新创建上下文时禁用磁盘缓存 browser = await p.chromium.launch(headless=True) context = await browser.new_context( user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36", cache_enabled=False, # 禁用磁盘缓存,减少文件句柄使用 )
禁用磁盘缓存可以减少不必要的文件操作,进一步降低句柄占用。
4. 检查其他可能的资源泄露
- 确认你的日志配置:如果用了
FileHandler,要确保是单例的,不要每次日志都打开新文件。 - 避免在循环里重复创建logger实例,确保整个爬虫只用一个logger对象。
最后,验证优化效果
做完这些修改后,你可以在爬虫运行时用lsof -p <你的爬虫进程ID>命令查看打开的文件数,对比优化前后的变化,确认句柄不再持续增长。
备注:内容来源于stack exchange,提问作者Sean
相关产品推荐
相关产品推荐

