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

使用Playwright编写Python爬虫时如何解决‘Too many open files’错误?

使用Playwright编写Python爬虫时如何解决‘Too many open files’错误?

看起来你已经尝试了不少常规方法来解决文件句柄耗尽的问题,但还是碰到了瓶颈——我来帮你梳理下现有代码里的潜在问题,再给出几个代码层面的优化方向,不用改系统限制也能搞定。

首先,先分析现有代码里可能遗漏的坑

  1. open_pages队列的并发安全问题:你用deque来管理打开的页面,但异步环境下多个协程同时修改这个队列会有竞态条件,比如A协程刚把页面加入队列,B协程就因为达到max_open_pages把它弹出关闭,而A协程后续的清理逻辑又会尝试移除这个页面,导致页面句柄没被正确释放,或者重复关闭引发异常。
  2. 页面清理的顺序问题:你的fetch_page_content里是先关闭页面再从队列移除,这可能导致队列里残留无效的页面引用,时间长了积累下来就会耗光句柄。
  3. 浏览器重启时的资源残留:你定期重启浏览器,但重启前没有确保所有打开的页面都被彻底关闭,残留的页面句柄会一直占用资源。

针对性的代码优化方案

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.04.13 18:49:50