无头浏览器选型指南:Playwright、Puppeteer与Selenium实战对比

无头浏览器选型指南:Playwright、Puppeteer与Selenium实战对比 把无头浏览器塞给Agent用这件事我最近和几个做自动化项目的老朋友反复聊过好几次。大家遇到的痛点高度一致模型已经把任务拆解得很像样了结果一到“打开页面、点击按钮、取回数据”这一步手里没有一套趁手的浏览器操作工具整个Agent就只能干瞪眼。而所谓无头浏览器说白了就是一个没有界面的浏览器内核它依然会加载JavaScript、渲染页面、维护Cookie只是所有动作都通过代码驱动运行在服务器或者容器里。Agent有了它才算真正长出“眼睛和手”能去真实网页里完成登录、查询、填表、下载、截图这一系列操作。这篇文章我会把几款主流方案逐个拆开讲清楚它们各自适合什么场景、有什么坑并给出我在真实项目里反复验证过的配置写法。不管你是在搭自己的Agent项目还是正在给现有系统补自动化能力都值得往下看。1. 为什么Agent离不开无头浏览器场景与选型逻辑1.1 Agent到底需要浏览器做什么很多人一开始会误以为Agent要浏览器就是为了爬数据。实际上在我接触到的项目里Agent操作浏览器的需求比这宽得多。最典型的是这几类一是自动登录并维持会话比如每天早上定时去后台拉经营报表二是在交互复杂的SPA应用里完成流程比如创建订单、审核工单、填写表单提交三是需要读取JavaScript渲染后的真实页面内容因为很多网站的数据接口加密或者鉴权复杂直接发HTTP请求拿不到完整数据四是对页面状态做断言与监控比如定时检查核心页面是否可用、关键文案是否被篡改。这些场景如果硬用requests或者httpx去模拟很快就会碰壁。现代网站的登录流程往往带设备指纹校验、滑块验证、加密参数页面数据又常由异步接口动态渲染你如果不让Agent操作一个真实的浏览器环境很多请求根本到达不了业务层。无头浏览器在这里扮演的是一个“真正的人坐在电脑前用Chrome操作”的等价物。1.2 无头浏览器与普通HTTP客户端的分界线我自己的判断标准很简单如果你只需要稳定调用公开接口或能轻松复现的API直接走HTTP请求省时省力。一旦目标页面依赖登录态、反爬参数、动态渲染或者你的Agent需要做多步骤交互再犹豫就不值当了直接上无头浏览器。两者不是替代关系而是互补关系。一个特别直观的例子某些系统导出Excel报表的按钮实际上是前端在拿到数据后通过JavaScript库在本地生成文件再触发下载。这种情况下你直接请求接口拿到的可能是一串加密数据而用无头浏览器点击导出按钮文件就自然出现在你指定的目录里。Agent的无头浏览器价值恰恰体现在这些“不那么标准”的网页交互上。1.3 选型前先想清楚的四件事我在给项目做技术选型之前一定会让团队回答四个问题你们的主力开发语言是Python还是Node这会直接锁定主方案。业务页面是传统多页应用还是重度SPA后者对等待策略和渲染完成判断要求更高。并发规模有多大是单Agent串行执行任务还是一次需要拉起几十上百个并发浏览器实例这决定了你需不需要引入浏览器实例池。是否已经有Selenium/WebDriver体系的历史资产如果有迁移成本必须算进去。把这四个问题答完选型方向基本就清晰了大半。下面进入正题聊聊我实测过的主流方案。2. 主流方案横评Playwright、Puppeteer、Selenium以及Agent专用浏览器框架2.1 PlaywrightAgent场景的综合首选如果今天有人问我第一次给Agent配无头浏览器该选什么我大概率会推荐Playwright。倒不是因为它功能最多而是它在“贴近真实用户”这件事上做得最省心。Playwright有几个点对Agent开发极其友好。第一是自动等待机制它会持续重试元素定位直到元素可操作不用你在代码里到处Sleep。这对LLM决策驱动的Agent来说太重要了——模型给出的操作指令有时并不精确到像素级自动等待能消掉很多不确定性。第二是浏览器上下文隔离一个BrowserContext就相当于一个独立的浏览器会话Cookie、LocalStorage、缓存全部分开。Agent并行跑多个任务时给每个任务一个独立Context互不污染比自己在代码里管理会话干净得多。第三是多浏览器内核支持Chromium、Firefox、WebKit都行遇到只在WebKit渲染下有问题的页面也不用推翻重来。下面是一个基础用法我用的是Python版本from playwright.sync_api import sync_playwright with sync_playwright() as p: browser p.chromium.launch(headlessTrue) context browser.new_context( viewport{width: 1280, height: 720}, user_agentMozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/124.0.0.0 Safari/537.36 ) page context.new_page() page.goto(https://example.com, timeout30000) page.get_by_role(button, name登录).click() page.wait_for_load_state(networkidle) html page.content() context.close() browser.close()在Agent场景里我建议用new_context()而不要在同一个页面里反复goto不同任务目标因为历史网页里的脚本可能污染后续操作。2.2 PuppeteerNode生态里的轻量选择如果你的Agent主体是Node.js写的Puppeteer是切入成本最低的选择。它由Chrome团队官方维护API调性和Chrome DevTools Protocol一脉相承文档丰富遇到问题搜起来也方便。Puppeteer默认只支持Chromium系对Firefox和Safari的支持相对弱。这个限制在大多数Agent场景里影响不大毕竟只要产品团队没有“必须兼容Firefox”的硬性要求Chromium内核已经能覆盖绝大多数真实用户环境。它的内存占用和启动速度在几款方案里属于中上水平bug修复也比较及时适合直接嵌入到已有的Node服务里。但要说缺点Puppeteer在稳定性方面比Playwright稍微“糙”一点。你经常要自己处理元素加载、自己判断页面是否完成渲染代码写多了以后等待逻辑会占掉很大篇幅。如果你打算让Agent自动决定操作步骤这种额外的心智负担会被放大。我的建议是Node技术栈且对自动化复杂度要求不高的项目选Puppeteer一旦流程复杂度上来哪怕用Python侧调用Playwright也比在Puppeteer里堆一堆waitForTimeout强。下面是一段Puppeteer的经典写法const puppeteer require(puppeteer); (async () { const browser await puppeteer.launch({ headless: new, args: [--no-sandbox, --disable-setuid-sandbox] }); const page await browser.newPage(); await page.setUserAgent(Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/124.0.0.0 Safari/537.36); await page.goto(https://example.com, { waitUntil: networkidle2, timeout: 30000 }); await page.waitForSelector(button.login-btn, { timeout: 10000 }); await page.click(button.login-btn); await browser.close(); })();注意headless: new这是新版无头模式和旧版相比对现代Web特性的兼容性更好不容易被网站识别为异常环境。2.3 Selenium老牌方案适合什么场景Selenium是很多测试团队的“原配”WebDriver协议也一度是事实上行业标准。如果你们团队之前已经用Selenium维护了大量测试用例或者被测系统里有大量针对WebDriver做的兼容适配那我不会劝你立刻迁移继续用Selenium完全合理。不过放在Agent场景里Selenium的短板比较明显。首先是API风格偏底层写起来啰嗦明明一个简单点击动作要显式等待、定位元素、再调用点击方法代码量比Playwright多出一截。其次是并发性能一般每个WebDriver会话的管理成本较高想在单机跑几十个并发Agent实例会比较吃力。还有就是等待策略不够智能经常需要靠time.sleep或者WebDriverWait配合自定义条件兜底。如果你确实要为已有Selenium体系接入Agent我会建议把Selenium封装成一层工具服务让Agent通过接口调用而不是让模型直接去写Selenium代码。这样既保留历史资产又能把自动化细节和Agent决策逻辑隔离开。不过对新项目来说我不会主动推荐它做首选。2.4 面向Agent的专用框架browser-use与Skyvern过去一年冒出了不少给Agent定制的浏览器框架主题思想都是“让LLM通过自然语言直接操作浏览器”。典型代表有browser-use和Skyvern。browser-use的思路是把Playwright封装成Agent可调用的工具集再配合LLM的视觉或DOM提取能力让模型“看懂”页面然后决定下一步动作。Skyvern则更激进主打免训练的场景理解靠视觉模型识别页面元素完成操作。这类框架的上手体验确实惊艳你只需要写几行声明式配置模型就能完成“去某网站搜索某关键词把第一条结果标题取回来”这类任务。但要注意目前它们离生产级稳定还有距离。LLM决策存在不确定性页面稍微改版或者出现可预期之外的弹窗就可能翻车。我比较推荐的做法是把这类框架用于原型验证或低风险的流程核心业务链路还是用Playwright写好固定逻辑作为兜底Agent只负责在关键分支上做选择。2.5 关键技术指标对比方案支持内核语言生态自动等待并发能力Agent友好度典型适用场景PlaywrightChromium / Firefox / WebKitPython / Node / Java / .NET强高Context隔离高Agent核心执行层、复杂交互、多任务并发PuppeteerChromium为主Node.js中中中Node服务内嵌、轻量自动化SeleniumChrome / Firefox / IE等Python / Java / C#等中中低中低遗留测试资产、跨浏览器兼容测试browser-use基于PlaywrightPython依赖底层依赖底层高LLM驱动的动态决策原型Skyvern基于Playwright/CDPPython依赖模型依赖底层高视觉理解的免训练操作场景说实话给Agent用无头浏览器本质上是选执行层的稳定性方案。模型负责“想”浏览器负责“做”执行层稳定性不够任务拆解得再好都会塌。3. 围绕Agent场景的实操配置环境准备与核心调用模式3.1 环境安装与依赖处理以Playwright为例安装过程有几个常见坑。直接用pip install playwright只能装Python包真正能跑还得加一步playwright install chromium这一步会下载浏览器二进制文件体积不小大概两三百兆。在容器里跑的话还要额外装系统依赖库建议用官方提供的基础镜像或者执行playwright install-deps。如果你发现下载慢可以设置镜像环境变量。不过镜像地址经常变这里我建议优先直接使用官方下载毕竟这部分属于一次性成本稳定比速度重要。装完之后可以用下面这条命令验证环境是否就绪python -c from playwright.sync_api import sync_playwright; sync_playwright().start(); print(ok)没有报错再看Chromium能否拉起无头模式。如果服务器缺少依赖启动时会报类似“libnss3 not found”的错误直接对照错误信息安装对应系统包即可。3.2 浏览器生命周期与上下文隔离Agent项目最容易出问题的是忽略浏览器实例和上下文的生命周期管理。一个Python进程里browser.launch()对应一个浏览器进程而browser.new_context()对应一个独立的会话。每一个new_page()都是会话里的标签页。我建议的模式是浏览器实例全局复用为每个Agent任务创建一个新Context任务结束就context.close()。这样浏览器进程只需启动一次开销小每个任务之间又互不共享Cookie和缓存数据隔离干净。如果直接把多个任务塞进同一个Page里轮番goto很可能会出现登录态串号、存储污染、元素定位互相干扰等诡异问题排查成本极高。from playwright.sync_api import sync_playwright class BrowserAgent: def __init__(self): self._pw sync_playwright().start() self.browser self._pw.chromium.launch( headlessTrue, args[--disable-blink-featuresAutomationControlled] ) def new_session(self): context self.browser.new_context( localezh-CN, timezone_idAsia/Shanghai ) page context.new_page() return context, page def close_session(self, context): context.close() def shutdown(self): self.browser.close() self._pw.stop()当然这只是一个极简骨架。实际项目里你还需要给Context挂载请求拦截、注入脚本、绑定事件回调等能力。3.3 页面信息的结构化提取Agent拿到页面后不能只靠原始HTML做推理token太长噪声也大。我更推荐把页面转成结构化数据再喂给Agent。常用的做法有两种一种是靠选择器精准提取关键字段另一种是把整个DOM简化成带语义标签的文本骨架。第一种适合固定页面、固定流程比如提某个工单列表的标题和状态def extract_order_list(page): rows page.locator(table.order-table tbody tr).all() result [] for row in rows: cells row.locator(td).all() if len(cells) 3: result.append({ order_id: cells[0].inner_text().strip(), title: cells[1].inner_text().strip(), status: cells[2].inner_text().strip() }) return result第二种适合不确定页面结构需要大模型来做理解的情况。我一般会把可见的文本、链接href、按钮文案、输入框placeholder抽成紧凑的JSON或者Markdown树控制长度之后再交给模型。这样既保留了操作所需的关键信息又不会让模型被海量DOM标签淹没。3.4 登录态持久化Agent跑任务不可能每次都重新登录。Playwright提供了storage_state可以把登录后的Cookie和LocalStorage保存到文件下次直接复用。保存阶段context browser.new_context() page context.new_page() # 这里执行一遍真实登录操作 page.goto(https://admin.example.com/login) page.get_by_label(用户名).fill(tester) page.get_by_label(密码).fill(password) page.get_by_role(button, name登录).click() page.wait_for_load_state(networkidle) context.storage_state(pathagent_state.json)复用阶段context browser.new_context(storage_stateagent_state.json)这里有两个要注意的细节一是登录态会过期复用前最好加一个快捷的登录状态校验比如访问某个需要鉴权的接口发现跳转到登录页就重新登录二是如果你跑的是对安全性比较敏感的系统不要把明文密码写在代码里用环境变量或者密钥管理服务去存。3.5 超时、重试与任务级兜底无头浏览器跑在服务器上网络波动、页面假死、CDN限流都是家常便饭。一套健壮的超时重试机制是必备的。我的习惯是给每次导航设置30秒超时元素定位设置10到15秒单步操作失败后最多重试3次前两次间隔较短最后一次把页面截图和DOM快照存下来方便事后排查。更关键的是要给Agent的整体任务设定一个总时间预算。比如单个页面操作序列最多执行5分钟超时就强制关闭Context。这个做法能避免模型在一个错误循环里反复横跳把服务器资源白白耗掉。你可以给Agent提供两个工具一个叫snapshot_page另一个叫close_and_restart让它在自我感知到状态异常时主动恢复。4. 踩坑实录无头浏览器在Agent项目里的高频问题与排查链路4.1 被网站识别为自动化环境怎么定位和处理这是大家在社区里问得最多的一个问题。现象是本地手动浏览器打开页面一切正常换成Playwright无头模式后要么弹验证码要么接口返回异常数据要么直接403。排查看三个点第一User-Agent是否被换成了默认的无头UA第二是否暴露了navigator.webdriver标记第三浏览器的自动化指纹如window.chrome、permissions状态是否和正常Chrome有明显差异。处理方式是逐项修复。先显式设置真实的User-Agent再通过启动参数--disable-blink-featuresAutomationControlled来去掉webdriver标记还有不要一上来就追求隐身和绕过很多网站的防护强度并没有那么高只要UA和行为节奏看起来像真人就能通过。配合随机滚动、可变输入速度、真实鼠标轨迹这些操作比追求指纹完全一致成本低得多也稳定得多。这里必须提醒一句自动化操作必须在你拥有合法授权的前提下进行。技术本身是中性的但越过了授权边界后面的一切都不成立。4.2 内存泄漏和长稳运行问题Agent服务和普通爬虫不太一样它可能会持续跑几天甚至几周。这时候内存泄漏问题会被放大。最常见的原因是全局只创建了一个Page任务循环里反复goto每切换一个页面就积累一批DOM节点或者创建了很多Context但忘记close()导致浏览器进程里残留大量会话数据。排查内存问题的一个有效方式是监控浏览器进程的RSS内存。你可以用psutil在任务循环里定期打印占用import psutil, os def memory_mb(): proc psutil.Process(os.getpid()) return proc.memory_info().rss / 1024 / 1024如果每次任务结束内存都在上涨优先检查Context是否关闭、Page是否被显式释放、事件监听器是否被移除。Playwright提供的事件回调比如page.on(request)、page.on(console)如果绑定后没有解绑也会成为隐藏的内存增长点。我的做法是把所有事件监听都收敛到一个独立的监听管理器里任务结束时统一清理。4.3 等待逻辑与实际渲染状态不一致SPA页面最大的坑是“你以为加载完了其实还没”。goto()返回时只代表导航请求被响应不代表页面里的异步数据都渲染完成。很多时候你急着定位元素就会失败。应对策略是分层次等待。第一步用page.wait_for_load_state(networkidle)等待网络空闲只是个粗粒度保障第二步精确等待你要操作的那个元素可见可点Playwright会自动重试。如果业务页面有骨架屏或者加载动画最好再补一个“动画结束”判断。有一个灵活的技巧是轮询页面里的关键文本或数字等到它符合预期再继续。比如价格、订单数量这类动态数据你直接等它稳定下来再提取。这在做数据采集的Agent任务里非常实用能大幅降低误抓半成品数据的概率。4.4 元素定位在多级iframe和Shadow DOM下的失效很多后台系统里嵌了iframePlaywright定位默认只处理顶层文档对于跨iframe的元素需要先切换到对应frame。我见过不少人在这一步卡住报错倒是直白找不到元素。排查时先确认元素是否在iframe里用page.frames列出当前页所有frame再逐个看内容。Shadow DOM是另一个重灾区。如果页面用了自定义组件元素可能在Shadow Root内部常规选择器碰不到。Playwright支持穿透实现定位符默认可以穿透open类型的Shadow DOM但遇到closed类型的就没办法了。遇到这种页面我的建议是优先和业务侧沟通看能否给关键元素加可访问性属性这比技术绕过省力得多。4.5 并发场景下的浏览器资源水位控制Agent一旦需要并行执行任务CPU和内存会被迅速打满。我跑过20个并发Context的实验单机器很快就出现明显卡顿和响应延迟。后来采用的策略是做一个简单的信号量控制并发上限并根据任务类型动态调整。例如消息采集类任务并发线程控制在8以内报表截图类任务控制在4以内。另外一个便宜好用的优化是拦截无用资源。很多页面会加载一堆统计脚本、字体文件、图片素材而Agent只关心核心DOM和接口数据。通过路由拦截把CSS、图片、字体请求直接放弃页面加载速度和资源占用都能改善不少。def block_unused_resources(route): if route.request.resource_type in (image, font, stylesheet): route.abort() else: route.continue_() context.route(**/*, block_unused_resources)我用这个规则在日常项目中把页面平均加载时间压缩了将近一半效果非常直观。5. 我的选型建议与几个提升Agent体验的偏方5.1 不同体量项目的选型组合我按项目体量给三条明确的建议你照着套就行个人脚本或小工具单看开发效率直接选PlaywrightPython或Node都行。功能完善、API顺手几乎没有需要额外补的轮子。成熟Web平台的Agent服务Playwright做核心执行层再用browser-use这类框架做动态决策的试验田。固定流程写死动态分支交给模型两侧各司其职。有大量遗留前端自动化资产的大型团队先用Selenium把旧用例保住同时新模块逐步切换到Playwright形成双轨过渡。过急的迁移很容易让原本稳定的回归体系崩掉。5.2 降低资源占用的几个参数很多人不知道Playwright启动浏览器时可以通过参数大幅降低资源占用。在我本机压测中下面这个组合能让单实例内存占用降不少browser p.chromium.launch( headlessTrue, args[ --disable-gpu, --disable-dev-shm-usage, --no-sandbox, --disable-extensions, --disable-background-networking, --disable-sync, ] )--disable-dev-shm-usage在容器里几乎必加否则/dev/shm太小容易导致渲染崩溃。--disable-gpu在纯无头环境里没有副作用却能少一块显存占用。--disable-extensions也能避免无关扩展加载带来的开销。5.3 给Agent记忆系统准备的浏览器交互记录Agent的记忆不是一句“记住上次做到哪了”就完了它需要可检索、可回溯的任务状态。我在项目里会把每一次浏览器关键操作记录成结构化的JSON事件存进消息队列或者数据库。事件内容包含动作类型、目标URL、元素描述、截图路径、时间戳、结果摘要。当Agent在后续对话中需要回忆时这些记录就是它的短期记忆。更近一步我还会每隔一段时间对当前页面做全量快照包括页面标题、可见文本、关键表单值、Cookie摘要。这样即使Agent进程中途崩溃重启也能从最近一次快照恢复上下文而不是从头开始。5.4 个人经验的最后补充给Agent配无头浏览器选型只是第一步后面更多功夫花在稳定性和错误恢复上。我自己的体会是不要一开始就追求让模型自由操作一切先用固定脚本把核心链路跑通再逐步把分支决策开放给模型。这个顺序反过来你大概率会收获一堆莫名其妙的重试和报错。还有一个小技巧值得多说一句执行无头浏览器任务的机器最好单独隔离不跟业务应用混部署。浏览器进程的资源占用波动很大它崩溃了不应该把主业务拖垮。用Docker或者Kubernetes Job去承载这类任务配上资源限制和自动重启策略比在裸机进程里裸奔稳得多。无头浏览器的生态更新很快新框架层出不穷但底层逻辑始终没变让Agent能像人一样看到页面、操作页面、从页面里学习。把这个执行层打磨扎实了Agent的能力上限才真正有保障。