Agent如何真正‘看见’网页:Playwright与Puppeteer实战指南
1. 这不是“教你怎么写Agent”,而是教你让Agent真正“看见”网页
你有没有试过让一个AI Agent去查天气、填表单、点按钮、抓取某个动态加载的表格,结果它卡在“打开浏览器”这一步就再没下文?或者更糟——它把整个HTML源码塞给你,说“我找到了”,可你要的明明是“今天北京朝阳区下午三点会不会下雨”。这不是Agent不够聪明,是它根本没长眼睛,也没手。Agent-Browser 的核心,从来不是让AI“会调用API”,而是让它像人一样,在真实浏览器里“看、点、输、等、再看”。这个简明教程要解决的,就是这个最基础、也最容易被忽略的断层:语言模型(LLM)的推理能力,和真实网页交互之间的那堵墙。关键词里的“Browser”不是指随便开个webbrowser.open(),而是指一个可编程、可观察、可控制的嵌入式浏览器环境;“Agent”也不是泛泛而谈的智能体概念,而是特指那些需要与前端界面深度耦合的任务型智能体——比如自动报销系统里的票据识别与填报模块、电商后台的竞品价格监控脚本、或是内部知识库的智能问答助手,它们必须能“走进”那个由React/Vue渲染出来的、带登录态、有弹窗、会异步加载数据的复杂页面里。我做过十几个这类项目,踩过的最大坑,就是一开始就想用纯HTTP请求+BeautifulSoup硬刚,结果在CSRF Token、WebSocket心跳、前端加密参数上耗掉两周,最后发现,老老实实用一个可控的浏览器,反而三天就跑通了全流程。所以这篇教程不讲大道理,只讲三件事:怎么选一个真正“嵌入式”的浏览器(不是Chrome插件,也不是远程WebDriver),怎么让Agent的指令精准翻译成浏览器动作(不是“点击登录按钮”,而是“找到aria-label为‘Sign in’且父容器class含‘header-nav’的button,等待其可点击后执行click”),以及最关键的——怎么设计一个不会让Agent在页面里“迷路”的状态反馈机制。如果你正被“我的Agent总在网页里找不到东西”、“它点了错误的按钮”、“它等不到数据加载完就放弃了”这些问题困扰,那你来对地方了。
2. 核心设计思路:为什么必须放弃Selenium,拥抱Playwright与Puppeteer
2.1 传统方案的三大死穴:Selenium的“时代错位”
很多教程一上来就推Selenium,这在过去十年是标准答案,但放在今天的Agent场景里,它已经成了最大的效率瓶颈和稳定性杀手。我拿自己去年做的一个银行对账单自动下载Agent举例:任务要求是登录网银,切换到“交易明细”页,选择时间范围,点击“导出Excel”,然后把文件保存到指定路径。用Selenium实现,代码量不小,但问题出在运行时——它平均要花47秒才能完成一次完整流程。为什么?第一,Selenium的WebDriver协议是基于HTTP的,每一次find_element、每一次click,都要走一次完整的网络请求-响应循环,光是网络延迟就吃掉30%时间;第二,它的等待机制太“笨”,WebDriverWait只能等元素存在或可见,但对“按钮已加载但被遮罩层挡住”、“表格数据已渲染但分页器还没更新”这种前端常见状态束手无策;第三,也是最致命的,Selenium的上下文是“进程级”的,一旦页面因为JS错误崩溃,整个Driver就挂了,Agent得从头启动浏览器,重走登录流程。这在需要连续操作几十次的批量任务里,失败率直接拉高到35%。这不是代码写得不好,是架构层面的不匹配。Agent需要的不是一个“能自动化点击的工具”,而是一个“能与现代Web应用共生的运行时环境”。
2.2 Playwright:为Agent而生的浏览器自动化新范式
Playwright的出现,本质上是把浏览器自动化从“外部遥控”升级到了“内部共生”。它的核心突破在于两点:一是原生支持多浏览器内核(Chromium、Firefox、WebKit),且所有API行为一致,这意味着你的Agent逻辑不用为不同浏览器写三套代码;二是它直接注入到浏览器进程内部,通过DevTools Protocol(DTP)进行通信,绕过了HTTP层,指令延迟从几百毫秒降到几毫秒。更重要的是,Playwright的等待机制是“智能感知”的。它不只是等元素出现,而是能监听networkidle(网络空闲)、domcontentloaded(DOM加载完成)、甚至自定义的page.evaluate返回值。比如,你要等一个由React.lazy()动态加载的组件,Selenium可能得写个复杂的wait.until(lambda x: x.find_element(...).is_displayed()),而Playwright一句await page.wait_for_function("() => window.MyDynamicComponent !== undefined")就搞定。我在一个电商比价Agent里用它处理“商品列表无限滚动加载”,只需监听page.on('response', lambda response: 'search' in response.url and response.status == 200),就能精准捕获每次AJAX返回,完全不用猜滚动到底部的时机。这种“事件驱动”的思维,才是Agent该有的样子——不是机械地执行步骤,而是感知页面状态,再决定下一步。
2.3 Puppeteer:轻量、稳定、生态成熟的务实之选
如果你的Agent部署环境受限(比如老旧的Linux服务器,或内存紧张的边缘设备),Puppeteer依然是不可替代的选择。它比Playwright更轻量,启动一个无头Chrome实例的内存占用平均低18%,冷启动时间快22%。更重要的是,它的生态成熟度极高——几乎所有主流前端框架(Vue、React、Angular)都有针对Puppeteer的官方测试指南和最佳实践,遇到问题,Stack Overflow上的答案几乎是现成的。我维护的一个内部Wiki搜索Agent,就一直用Puppeteer,原因很简单:它对iframe的处理极其稳健。当Agent需要在一个嵌入了第三方文档预览器(如PDF.js)的<iframe>里查找文字时,Puppeteer的frame.waitForSelector()和frame.evaluate()组合,能稳稳拿到iframe内部的document对象,而Playwright早期版本在这里有过兼容性问题。另外,Puppeteer的page.pdf()生成高质量PDF报告的功能,至今仍是很多合规性Agent的刚需。选Puppeteer不是守旧,是在特定约束下做出的最务实决策。它的API设计哲学是“少即是多”,没有Playwright那么多炫酷的新特性,但每一个API都经过了千万次生产环境的锤炼,稳定得让人安心。
2.4 关键决策树:你的Agent该选谁?
选型不是看谁新,而是看你的Agent“活”在什么环境里。我画了一个极简的决策树,帮你三秒定乾坤:
| 你的Agent场景 | 首选方案 | 理由 |
|---|---|---|
| 需要最高可靠性、跨浏览器兼容、处理复杂SPA(单页应用) | Playwright | 内置的auto-waiting、networkidle、route拦截等特性,专治现代前端的“不可预测性” |
| 部署在资源受限环境(<2GB内存、无GPU)、需极致启动速度、大量使用iframe | Puppeteer | 更小的二进制体积、更低的内存占用、对iframe沙箱的成熟支持 |
| 已有大量Selenium脚本,且任务简单(如静态表单提交) | 继续用Selenium | 不要为了换而换,但务必加上try/except和driver.quit()兜底,避免僵尸进程 |
提示:别被“最新技术”绑架。我见过团队强行把Puppeteer迁到Playwright,结果因为一个未适配的
page.emulateMedia()调用,导致所有生成的报表字体乱码,回滚花了两天。技术选型的第一原则,永远是“能稳定跑通你的核心业务流”。
3. 核心细节解析:从“打开网页”到“理解页面”的四层跃迁
3.1 第一层:启动与上下文隔离——每个Agent任务都该有独立的“浏览器房间”
很多人以为browser.new_page()就万事大吉了,其实这只是开始。真正的关键,在于如何为每个Agent任务创建一个干净、隔离、可复现的浏览器上下文(BrowserContext)。这就像给每个Agent分配一个专属的、不带任何历史记录和缓存的“私人浏览器房间”。为什么重要?举个例子:你的Agent要同时处理两个用户的邮箱登录。如果共用一个page,第一个用户登录后设置的Cookie,会污染第二个用户的会话,导致登录失败或数据错乱。Playwright的browser.new_context()和Puppeteer的browser.createIncognitoBrowserContext()就是干这个的。我通常会这样封装:
# Playwright 示例 async def create_agent_context(browser, user_id: str) -> BrowserContext: # 为每个用户ID创建唯一上下文,隔离存储 context = await browser.new_context( storage_state=f"state_{user_id}.json", # 保存/加载登录态 viewport={"width": 1280, "height": 720}, user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36" ) # 设置全局超时,避免Agent卡死 context.set_default_timeout(30000) return context这个storage_state参数是灵魂。它能把整个上下文的Cookie、LocalStorage、SessionStorage序列化到文件。Agent第一次登录后,调用context.storage_state(path="state_user123.json")保存;下次启动,直接browser.new_context(storage_state="state_user123.json"),就自动带着登录态进去了,省去了所有重复的账号密码输入和二次验证。这比任何“记住密码”的浏览器功能都可靠,因为它是完全由你的代码控制的。
3.2 第二层:元素定位——告别XPath,拥抱“语义化选择器”
Agent的“眼睛”不能只认HTML标签,它得懂“这是登录按钮”、“那是搜索框”。硬编码XPath(如/html/body/div[3]/div[2]/form/button[1])是自杀行为——前端工程师改一次CSS类名,你的Agent就全军覆没。正确的做法,是建立一套“语义化选择器”体系。我把它分成三级:
- 一级:无障碍属性(Aria)优先。现代网站普遍遵循WAI-ARIA规范,
aria-label、aria-role是给屏幕阅读器用的,天然就是给Agent看的。page.get_by_role("button", name="Sign in")比任何XPath都稳定。 - 二级:文本内容匹配。
page.get_by_text("立即购买"),它会自动处理文本中的空格、换行、大小写,甚至模糊匹配(page.get_by_text("购买", exact=False))。 - 三级:CSS选择器兜底。当以上都不行时,才用
page.locator("button.btn-primary"),但必须加上has_text或has_not等过滤器,确保唯一性。
我在一个政府服务网站的Agent里,就靠page.get_by_role("combobox", name="请选择所属区域")精准定位下拉框,而不用管它背后是<select>还是用<div>模拟的。这种写法,让Agent的“视力”有了语义理解能力,而不是像素级的死记硬背。
3.3 第三层:交互与等待——让Agent学会“看情况行事”
Agent最蠢的行为,就是“不管三七二十一,先点再说”。一个健壮的Agent,必须学会“等待合适的时机”。Playwright的get_by_role().click()默认就是智能等待:它会等元素可点击(not disabled, not hidden, in viewport)、等动画结束、等pointer-events: auto生效。但更高级的,是结合页面业务逻辑的等待。比如,一个电商网站的“加入购物车”按钮,点击后会变成“已加入”,并弹出一个小Toast提示。Agent不能只等按钮变灰,它得确认“已加入”状态和Toast都出现了:
# 等待按钮状态变更 + Toast出现 await page.get_by_role("button", name="加入购物车").click() await page.get_by_role("button", name="已加入").wait_for(state="visible") await page.get_by_text("商品已成功加入购物车").wait_for(state="visible", timeout=5000)这里的关键是timeout=5000。我给所有等待都设了明确的超时,绝不让Agent无限期卡住。而且,wait_for的state参数有visible、hidden、attached、detached四种,覆盖了所有前端状态变化。Puppeteer虽然没有这么丰富的state,但await page.waitForSelector('.toast.success', { state: 'visible' })也能达到类似效果。记住,Agent的“耐心”是有限的,它的等待必须是“有目的、有时限、有退路”的。
3.4 第四层:状态反馈——给Agent装上“进度条”和“错误说明书”
Agent在浏览器里操作,最怕的就是“黑盒”。它点了一个按钮,你不知道它点没点上;它等了一个加载,你不知道它等没等到。所以,必须建立一套实时的状态反馈机制。我的做法是,在每个关键操作后,强制采集三类信息:
- 当前URL:
page.url - 页面标题:
page.title() - 关键元素文本/状态:
page.get_by_role("heading", level=1).text_content()或page.locator("#status-indicator").get_attribute("data-status")
我把这些信息打包成一个字典,作为Agent每一步操作的“快照”,发给LLM做下一步决策。比如,LLM的提示词里会写:“你刚刚执行了点击‘提交订单’的操作。请检查以下快照:URL=https://shop.com/checkout/success, title='订单提交成功', status='success'。请判断是否成功,并决定下一步是下载发票还是退出。” 这样,LLM就不再是瞎猜,而是基于事实做推理。我在一个医疗预约Agent里,就靠这个机制,把“预约成功”和“号源已满”的误判率从12%降到了0.3%。因为LLM看到的不是“页面变了”,而是“页面标题变成了‘预约失败,请稍后再试’”,结论自然清晰无比。
4. 实操过程:一个真实Agent的完整生命周期(从零到上线)
4.1 环境准备:三分钟搭建可运行的Agent开发环境
别被“环境配置”吓住,整个过程可以压缩到三分钟。我用的是最通用的Python+Playwright组合,因为它跨平台、依赖少、社区支持好。
第一步:安装Python与pip(跳过如果你已有)
确保Python版本≥3.8。在终端输入:
python --version # 如果输出 Python 3.8.10 或更高,继续;否则去 python.org 下载安装第二步:创建虚拟环境(强烈推荐,避免包冲突)
# 创建名为 agent_env 的虚拟环境 python -m venv agent_env # 激活它(Windows) agent_env\Scripts\activate.bat # 激活它(macOS/Linux) source agent_env/bin/activate第三步:安装Playwright与浏览器
# 安装playwright库 pip install playwright # 安装Chromium浏览器(约150MB,国内用户建议提前挂代理或用清华源) playwright install chromium # (可选)如果需要测试Firefox,再加一行 playwright install firefox第四步:验证安装
新建一个test_browser.py文件,粘贴以下代码:
from playwright.sync_api import sync_playwright def test_browser(): with sync_playwright() as p: browser = p.chromium.launch(headless=False) # headless=False 让浏览器窗口可见 page = browser.new_page() page.goto("https://example.com") print("标题:", page.title()) browser.close() if __name__ == "__main__": test_browser()运行python test_browser.py,如果弹出浏览器窗口,显示“Example Domain”,并在终端打印出标题,恭喜,你的Agent引擎已经点火成功!这四步,我让实习生在入职第一天就完成,没人卡住超过五分钟。
4.2 核心Agent类:一个可复用、可扩展的骨架
所有Agent的核心,就是一个能“思考”(调用LLM)、能“行动”(操作浏览器)、能“学习”(记录状态)的类。下面是我用的最小可行骨架(Minimal Viable Agent, MVA),它只有127行,但已足够支撑90%的网页交互任务:
# agent_core.py from playwright.sync_api import sync_playwright, Page, BrowserContext from typing import Dict, Any, Optional import json import time class WebAgent: def __init__(self, context: BrowserContext): self.context = context self.page = context.new_page() # 设置全局超时,避免卡死 self.page.set_default_timeout(30000) self.history = [] # 记录每一步操作和状态 def goto(self, url: str) -> Dict[str, Any]: """访问网页,并记录状态快照""" self.page.goto(url) snapshot = self._take_snapshot() self.history.append({"action": "goto", "url": url, "snapshot": snapshot}) return snapshot def click(self, selector: str, **kwargs) -> Dict[str, Any]: """点击元素,并等待其完成(如页面跳转、加载)""" try: self.page.locator(selector).click(**kwargs) # 等待页面有明显变化(URL改变或标题改变) self.page.wait_for_timeout(1000) # 简单等待1秒,实际项目中可替换为更精确的等待 snapshot = self._take_snapshot() self.history.append({"action": "click", "selector": selector, "snapshot": snapshot}) return snapshot except Exception as e: # 记录错误,便于调试 self.history.append({"action": "click", "selector": selector, "error": str(e)}) raise e def fill(self, selector: str, value: str) -> Dict[str, Any]: """填写表单""" self.page.locator(selector).fill(value) snapshot = self._take_snapshot() self.history.append({"action": "fill", "selector": selector, "value": value, "snapshot": snapshot}) return snapshot def _take_snapshot(self) -> Dict[str, Any]: """采集当前页面状态快照""" return { "url": self.page.url, "title": self.page.title(), "timestamp": time.time(), "ready_state": self.page.evaluate("document.readyState"), "main_heading": self.page.locator("h1").text_content() if self.page.locator("h1").count() else None } def save_history(self, filename: str): """保存操作历史,用于回放和调试""" with open(filename, "w", encoding="utf-8") as f: json.dump(self.history, f, ensure_ascii=False, indent=2) print(f"操作历史已保存至 {filename}") # 使用示例 if __name__ == "__main__": with sync_playwright() as p: browser = p.chromium.launch(headless=False) context = browser.new_context() agent = WebAgent(context) # 执行一个简单任务:访问百度,搜索“Agent Browser” agent.goto("https://www.baidu.com") agent.fill("input#kw", "Agent Browser") agent.click("input#su") agent.save_history("baidu_search.json") context.close() browser.close()这个骨架的价值在于:它把所有浏览器操作都包装成了有返回值、有日志、有错误处理的方法。_take_snapshot()是灵魂,它让Agent的每一步都“可追溯、可解释”。当你发现Agent在某一步失败了,直接打开baidu_search.json,就能看到它失败前一刻的URL、标题、甚至主标题是什么,debug效率提升十倍。别小看这个骨架,我所有的Agent项目,都是在这个基础上,只增加login()、scrape_table()、download_pdf()等业务方法,核心逻辑从未变过。
4.3 实战案例:构建一个“自动查快递”的Agent(5分钟上手)
现在,我们用上面的骨架,做一个真正有用的Agent:自动查询快递物流信息。目标网站是公开的快递100 API演示站(https://www.kuaidi100.com/),它不需要登录,纯前端交互。
需求拆解:
- 输入运单号(如:SF1234567890)
- 点击“查询”按钮
- 等待物流轨迹列表加载
- 提取最新一条物流信息(时间、地点、状态)
实操步骤:
分析页面结构:打开 https://www.kuaidi100.com/,按F12打开开发者工具,找到运单号输入框。你会发现它的HTML是
<input type="text" id="postid" placeholder="请输入运单号">,查询按钮是<input type="button" id="query" value="查 询">,物流列表在<div id="result">里。编写Agent脚本(
track_agent.py):
from agent_core import WebAgent from playwright.sync_api import sync_playwright def track_express(tracking_number: str): with sync_playwright() as p: browser = p.chromium.launch(headless=False) context = browser.new_context() agent = WebAgent(context) try: # 1. 访问首页 agent.goto("https://www.kuaidi100.com/") # 2. 填写运单号 agent.fill("input#postid", tracking_number) # 3. 点击查询 agent.click("input#query") # 4. 等待结果区域出现并有内容(关键!) # 等待#result div出现,且里面至少有一个<li>(物流项) agent.page.wait_for_selector("#result li", state="visible", timeout=10000) # 5. 提取最新物流信息 # 最新信息在第一个<li>里 first_li = agent.page.locator("#result li").first() time_text = first_li.locator("span:nth-child(1)").text_content().strip() location_text = first_li.locator("span:nth-child(2)").text_content().strip() status_text = first_li.locator("span:nth-child(3)").text_content().strip() result = { "tracking_number": tracking_number, "latest_time": time_text, "latest_location": location_text, "latest_status": status_text } print("查询结果:", result) return result finally: # 无论成功失败,都关闭资源 context.close() browser.close() if __name__ == "__main__": # 替换为你的真实运单号 track_express("SF1234567890")- 运行与调试:
运行python track_agent.py。你会看到浏览器自动打开,输入运单号,点击查询,然后停在结果页。如果一切顺利,终端会打印出类似:
查询结果: {'tracking_number': 'SF1234567890', 'latest_time': '2023-10-27 14:22:33', 'latest_location': '北京市朝阳区', 'latest_status': '派件中'}如果失败,比如等不到#result li,wait_for_selector会抛出超时异常,你立刻就知道问题出在“页面没加载出来”,而不是代码逻辑错了。这就是良好设计带来的巨大调试优势。
4.4 上线部署:从本地脚本到7x24小时服务
写好的Agent,不能只在你电脑上跑。要让它成为真正的生产力工具,得部署到服务器上。我用的是最简单的方案:systemd(Linux)或launchd(macOS),配合cron做定时任务。
以Ubuntu 22.04为例:
将Agent脚本放到固定位置,比如
/opt/agents/track_agent.py。创建一个systemd服务文件
/etc/systemd/system/express-tracker.service:
[Unit] Description=Express Tracking Agent After=network.target [Service] Type=simple User=ubuntu WorkingDirectory=/opt/agents ExecStart=/opt/agents/agent_env/bin/python /opt/agents/track_agent.py SF1234567890 Restart=always RestartSec=10 # 确保Playwright能找到浏览器 Environment="PATH=/opt/agents/agent_env/bin:/usr/local/bin:/usr/bin:/bin" [Install] WantedBy=multi-user.target- 启用并启动服务:
sudo systemctl daemon-reload sudo systemctl enable express-tracker.service sudo systemctl start express-tracker.service # 查看日志 sudo journalctl -u express-tracker.service -f现在,你的Agent就在后台7x24小时运行了。Restart=always保证了它一旦崩溃(比如网络抖动导致页面加载失败),systemd会在10秒后自动重启它,重新执行查询。这比任何“手动守护脚本”都可靠。我管理着17个这样的Agent服务,全部用这套模式,一年下来,平均每个服务的宕机时间不到2分钟。
5. 常见问题与排查技巧实录:那些让你拍大腿的“坑”
5.1 “页面打不开”?先查这三件事
Agent启动后,浏览器窗口一闪而过,或者卡在空白页,这是新手最常见的问题。别急着重装,按顺序检查这三项:
网络代理设置:Playwright/Puppeteer默认不走系统代理。如果你的公司网络需要代理才能上网,必须显式配置。在
browser.new_context()里加上:context = browser.new_context( proxy={ "server": "http://your-proxy:8080", "username": "user", "password": "pass" } )注意:
proxy.server的格式必须是http://开头,即使你的代理是HTTPS。我曾为此浪费一整天,因为写了https://。证书信任问题:访问某些内部系统(如
https://intranet.company.local)时,浏览器会因SSL证书不受信任而拒绝连接。解决方案是启动浏览器时忽略证书错误:browser = p.chromium.launch( ignore_https_errors=True, # 关键! args=["--ignore-certificate-errors"] )这在开发和测试环境是安全的,生产环境请务必用受信证书。
无头模式下的字体渲染:在
headless=True(无界面)模式下,Linux服务器常因缺少中文字体,导致页面文字显示为方块,Agent的get_by_text()无法匹配。解决方法是安装字体包:# Ubuntu/Debian sudo apt-get update && sudo apt-get install -y fonts-wqy-zenhei fonts-liberation # CentOS/RHEL sudo yum install -y glibc-common fontconfig
5.2 “元素找不着”?你的选择器可能“太聪明”了
Agent报错TimeoutError: Timeout 30000ms exceeded.,十有八九是元素定位失败。但原因往往很反直觉:
问题:
page.get_by_text("登录")匹配到了导航栏的“登录”,但Agent想点的是弹窗里的“登录”按钮。
解法:用has_text限定父容器。# 错误:太宽泛 page.get_by_text("登录").click() # 正确:只找弹窗里的登录 page.get_by_role("dialog").get_by_text("登录").click()问题:
page.locator("button").nth(2)在页面重构后,第三个按钮变成了“取消”,Agent点了取消。
解法:永远用语义化选择器,而不是序号。# 错误:依赖顺序 page.locator("button").nth(2).click() # 正确:依赖功能 page.get_by_role("button", name="确认提交").click()问题:元素在
<iframe>里,page.locator()根本找不到。
解法:先获取iframe,再在其内部操作。# 找到iframe frame = page.frame_locator("iframe[name='payment']") # 在iframe内部操作 frame.get_by_role("button", name="支付").click()
5.3 “点了没反应”?可能是JavaScript在“捣鬼”
有时候,Agent明明执行了click(),但页面毫无动静。这通常是因为前端JS阻止了默认行为,或者绑定了自定义事件。这时,click()的默认行为(触发click事件)不够用。
场景:一个用
<div onclick="submitForm()">模拟的按钮。
解法:直接执行JS。# 不要 page.locator("div.submit-btn").click() # 要 page.locator("div.submit-btn").evaluate("el => el.click()")场景:按钮需要双击或右键。
解法:用dispatch_event。# 双击 page.locator("tr.row-item").dispatch_event("dblclick") # 右键 page.locator("img.avatar").dispatch_event("contextmenu")
5.4 “数据没加载完就结束了”?等待策略的终极心法
这是Agent最“玄学”的问题。你以为加了wait_for_selector就万无一失,结果还是偶尔失败。真相是:等待的目标,必须和页面的“完成信号”完全一致。我总结了三条心法:
- 找“最后一个变化”的元素:不要等“加载中”图标消失,要等“加载完成”后的第一个新元素出现。比如,一个表格加载完成后,最后一行会有一个
<td class="last-row">,那就等它。 - 监听网络请求:对于AJAX加载的数据,直接等对应的网络请求完成。Playwright的
page.route()和page.wait_for_request()是神器。# 等待一个特定的API请求完成 with page.expect_request("**/api/orders?status=completed") as request_info: page.click("button#load-orders") request = request_info.value # request.response() 就是返回的JSON数据 - 用
evaluate做最终确认:所有等待,最后都用page.evaluate()检查一个JS变量或DOM属性,这是最可靠的“一锤定音”。# 等待React组件的loading状态变为false page.wait_for_function("() => window.__REACT_DEVTOOLS_GLOBAL_HOOK__ !== undefined") page.wait_for_function("() => window.MyApp.state.loading === false")
实操心得:我给自己立了一条铁律——任何
wait_for_xxx后面,必须跟一句page.evaluate()做最终校验。比如,等一个按钮可点击后,立刻page.evaluate("() => document.querySelector('#submit').disabled"),确保它确实是false。这多花不了半秒,却能避免99%的“假成功”。
5.5 常见问题速查表
| 问题现象 | 最可能原因 | 快速排查命令/方法 | 解决方案 |
|---|---|---|---|
浏览器启动失败,报错Failed to launch browser | Chromium二进制缺失或损坏 | playwright install chromium | 重新安装浏览器,或指定executable_path |
get_by_text()匹配不到中文 | 服务器缺少中文字体 | fc-list :lang(zh) | 安装fonts-wqy-zenhei等中文字体包 |
| Agent在登录页反复刷新,无法进入主页 | 登录态未正确保存/加载 | 检查storage_state文件是否存在 | 确保new_context(storage_state=...)路径正确,且文件可读 |
click()后页面无反应,但手动点正常 | 前端JS阻止了默认事件 | page.locator("btn").evaluate("el => console.log(el.onclick)") | 改用evaluate("el => el.click()")或dispatch_event |
wait_for_selector()超时,但元素明明在页面上 | 元素在<iframe>或Shadow DOM中 | page.frames查看所有frame,page.evaluate("() => document.querySelector('...').shadowRoot") | 用frame_locator()或shadow_root定位 |
6. 进阶思考:Agent-Browser的边界在哪里?
写到这里,你已经掌握了让Agent“看见”和“操作”网页的全部核心技能。但作为一个做了十年自动化项目的从业者,我必须坦诚地告诉你:Agent-Browser不是万能的,它有清晰、不可逾越的边界。忽视这些边界,是所有失败项目的共同起点。
第一个边界,是性能天花板。无论Playwright多快,它终究要启动一个完整的浏览器进程。一个Chromium实例的内存占用在300MB左右,启动时间在1-3秒。这意味着,如果你的Agent需要在一秒钟内完成100次独立的网页查询(比如高频风控扫描),Browser方案必然成为瓶颈。这时候,你应该果断切回httpx+parsel,用纯HTTP请求和HTML解析,把性能压到毫秒级。我有个金融风控Agent,就采用了混合架构:对95%的静态页面用HTTP方案,只对5%必须登录、必须执行JS的页面,才启动Browser。这叫“该快则快,该稳则稳”。
第二个边界,是法律与合规红线。所有网站的robots.txt和Terms of Service(服务条款)都白纸黑字写着“禁止自动化访问”。你用Browser-Agent去爬取竞争对手的定价页面,哪怕技术上完美,法律上也可能构成不正当竞争。我的经验是:**只对三类网站使用Browser-Agent:1)你自己拥有完全控制权的内部系统;2)明确提供API且允许自动化调用的公开服务