最近在折腾一套网页自动化体系核心思路是用 Playwright 把一个技能模块的感知边界往外扩了一层。以前做 Web 自动化大家习惯的是找元素、点按钮、填表单脚本按预想路线执行但页面到底变成了什么样、接口返回了什么、用户真实看到的是什么脚本一概不知。把 Playwright 真正接进这个体系之后最大的变化不是自动化跑得更快而是自动化开始“有感觉”了能看到元素状态能拦到网络请求能比对视觉差异甚至能把手感传递给大模型去判断下一步动什么。这篇文章我想把整套思路和落地中出现的问题都摊开来讲适合在做自动化测试、爬虫采集、RPA 脚本或者尝试给 AI Agent 接入网页操作能力的人参考。不管你是刚接触 Playwright 的新手还是已经在业务里跑着脚本的老手应该都能从里面的接口设计、代码模式和踩坑记录里找到点有用的东西。1. 从普通自动化到感知型自动化1.1 传统自动化工具的“盲区”最早我写爬虫的时候习惯用 HTTP 请求直接拉 HTML。后来碰到越来越多需要交互才能拿到数据的页面比如列表点“加载更多”才刷新或者内容由 JS 异步渲染请求一次拿到的就是一页空壳。于是换成自动化浏览器驱动能点击能输入了但随之而来的问题是“脚本在盲跑”——元素定位出了问题脚本不会告诉你为什么只会报一个超时。页面弹了个遮罩挡住了按钮、出现了 loading 动画占位、数据接口返回了 500 但页面还挂着旧数据这些情况在传统模式里基本都感知不到。我记得自己在做一个数据后台巡检脚本的时候遇到过极其隐蔽的问题页面看起来一切正常所有元素都能定位到表格也有数据但数字其实是昨天缓存的。光靠传统断言永远发现不了后来把接口响应接进来一对比才发现请求已经失败两天了。这个例子足以说明感知边界不是可有可无的增强项而是自动化能不能产生真实价值的底线。脚本自己看不到的东西自然也校验不了最终你会发现所有线上疑难杂症的根源都是感知盲区。1.2 感知边界到底指什么先说清楚什么叫感知边界。我这里把感知边界定义为自动化执行体在运行过程中能够获取并理解的外部状态信息的范围。传统脚本的感知边界非常窄顶多知道“我正在哪个页面上、这个元素能不能点、点击后 URL 变了没有”。而一套真正可用的自动化体系至少要把感知边界扩展到四个层面DOM 结构感知、视觉感知、网络感知、运行态感知。下面这张表可以看得更直观。感知维度传统模式下的表现扩展后能做什么DOM 结构只能按预写定位符找元素主动提取当前页面的按钮、输入框、可见文本摘要视觉感知完全不关心用户看到什么截图、录屏、像素级差异比对网络感知只关心结果不关心请求记录每个接口的状态码、耗时、返回体大小运行态感知脚本只管自己跑没跑能感知上下文、登录态、浏览器健康状态这四个维度覆盖了从“功能是否可用”到“用户是否满意”的完整链路。举个例子一个按钮能点不代表流程就成功了点击之后接口是否返回正确数据、界面有没有发生预期的变化、跳转之后的页面视觉上是否正常这些才构成完整的业务闭环。把这些都能感知到自动化脚本才真正从执行工具变成了观察工具。1.3 为什么选择 Playwright 作为感知底座市面上的方案不少Selenium 老牌但配置重Cypress 适合纯前端测试但受限于浏览器而 Playwright 在“可观测性”上是下了功夫的自动等待、上下文隔离、网络拦截、Tracing、截图录屏这些能力被设计成一等公民。它最大的一个好处是能让我把感知逻辑直接挂在框架的原生事件上而不是自己打补丁。比如想记录所有接口返回无非是监听一个 response 事件接口层面的信息就全到手了不需要借助外部抓包工具。另外一个实际体验是Playwright 的安装和驱动管理对使用者来说几乎无感。浏览器的二进制会自动下载和脚本代码之间的版本匹配由框架自己处理不像以前那样还要单独维护一套驱动路径环境一变就报“无法找到浏览器驱动”。对于要长期维护的自动化项目来说这种“少一层环境心智负担”的价值非常实在。当然它也不是没有学习成本但从感知能力扩展的角度看投入产出比相当可观。2. 感知能力从哪来Playwright 的核心特性再解读2.1 等待机制从“睡了3秒”到“等到条件满足”先说这个因为几乎所有人都踩过 sleep 的坑。之前用其他框架写脚本最常见的代码就是time.sleep(3)等加载。写成 3 秒页面没出来写成 5 秒又拖慢整体速度。Playwright 默认的自动等待不是基于固定睡眠而是渲染引擎不断重试直到元素可见、可用、稳定或者超出超时时间。这个机制的底层逻辑是“条件轮询”不是“祈祷式等待”所以脚本的稳定性有一个硬保障。我在用 expect 做断言的时候会习惯把超时时间设置成 10 秒而不是默认的 5 秒。理由是线上环境的网络波动普遍超出 5 秒能覆盖的范围尤其当页面里嵌套了多个第三方脚本时渲染完成时间很容易拉到 8 秒以上。自动等待做得好不代表超时时间可以随便设它只是让脚本在等待期间不白白浪费资源但合理的超时上限还是需要结合实际场景去调。等待机制本身就是感知能力的一部分因为“等到了”意味着状态客观上发生了变化。2.2 选择器体系从固定路径到语义定位感知边界要想扩大第一步其实是“知道用什么去锚定页面元素”。Playwright 支持的选择器细分下来有 CSS、文本、角色、XPath 等我个人最常用的是 get_by_role 和 get_by_text。原因很实在页面结构调整太频繁XPath 里一长串 div 层级经常一改版就断而角色加可见文本的组合类似于人在看页面时“这个按钮是提交”“这个区域是导航”语义更稳。用 get_by_role 还有个额外好处它天然会去理解可访问性树而不是只看 DOM 结构。这意味着页面里即使是自定义组件只要暴露了正确的角色信息脚本就能精确感知到它的存在。这比单纯用 class 名要可靠太多因为 class 是给样式用的随时可能被重构。如果你的项目里有前端团队可以约定在组件上补充>from abc import ABC, abstractmethod class Skill(ABC): abstractmethod def name(self) - str: 技能的标识符 ... abstractmethod def description(self) - str: 给LLM或调用方看的说明 ... abstractmethod async def run(self, page, **kwargs) - dict: 统一入口 page 是 Playwright 的 Page 对象 返回值必须是 JSON 可序列化的 dict ...为什么要强制返回 JSON 可序列化的 dict因为感知结果的消费者不只是代码还可能是大模型。模型只认字符串和结构化 JSON所以感知边界最终要以文本化的方式送达决策层。另外统一接口还有一个隐藏好处所有技能可以天然地互相嵌套。一个技能内部可以调用另一个技能比如“提交订单”技能内部先调用“页面快照”技能确认当前状态再执行点击操作这种组合式设计大大降低了复杂度。3.2 实现页面状态感知模块第一个模块我起名 page_snapshot作用是拍下当前页面的“状态快照”。它做的不是把整个 DOM dump 出来而是输出一个页面摘要包括当前 URL、标题、主按钮、可见文本片段、关键元素状态等。这样既保留了上下文又不会因为内容过长把下游的模型淹死。async def page_snapshot(page) - dict: title await page.title() url page.url buttons await page.get_by_role(button).all_text_contents() buttons [b.strip() for b in buttons if b.strip()][:10] inputs await page.locator(input:visible).evaluate_all( els els.map(el el.placeholder || el.name || el.type) ) body_text await page.locator(body).inner_text() return { url: url, title: title, buttons: list(set(buttons)), inputs: list(set(inputs)), summary: body_text[:500], }这个模块的价值在于任何一次点击之后我不需要人眼去看页面代码就能告诉我“现在页面是谁、有哪些可以操作的东西”。截取摘要前 500 个字符是一个刻意设计。因为模型上下文窗口有限如果把整篇文章几千字的正文全塞进去关键按钮和输入框的信息反而会被淹没。摘要把最重要的结构信息显式提取出来又把原始文本截断作为背景补充算是在信息密度和上下文开销之间取了一个平衡。实际调试的时候你会发现这个 500 字阈值对大多数操作型任务完全够用。3.3 接入网络与视觉感知第二块是网络与视觉感知。网络感知我直接在页面创建时挂一个 response 监听把接口的 URL、状态码、耗时、响应长度记录下来。视觉感知则是对比截图。为了不让比对结果过于敏感我会把像素差异阈值设在 0.02 到 0.1 之间超过阈值才告警。async def record_responses(page, result_box): async def on_response(response): if response.request.resource_type in (xhr, fetch): result_box.append({ url: response.url, status: response.status, size: len(await response.body()) if response.status ! 204 else 0, }) page.on(response, on_response)视觉感知的做法是把当前页面截图保存到指定目录然后和基线图做比对。我建议保存截图时带上时间戳方便回溯。比对算法不一定要多高级如果是颜色差异明显的变化像素级差异率就够了如果是局部细小的文案变化可能需要接 OCR 或者更精细的特征检测。这里的关键是“感知到什么程度才算问题”所以阈值的选择比算法本身更重要。我把 0.05 作为默认值因为经过多次实验低于这个值的基本是字体渲染差异高于这个值大概率是真实视觉变化。3.4 让大模型能“看懂”网页结构化页面描述前几个模块是给人来看的这一节是很关键的扩展把感知结果整理成自然语言描述喂给大模型。比如“当前页面标题是 xxx包含按钮提交、取消、重置输入框有用户名、密码页面摘要如下……”。大模型拿到这段文字后就可以自主决定下一步点击哪个按钮、在哪个输入框里填什么。这意味着感知边界不再只服务于固定脚本而是把决策权移交给了模型。def build_page_description(snapshot: dict) - str: description f当前页面标题{snapshot[title]}\n description f页面URL{snapshot[url]}\n description 页面上的按钮 、.join(snapshot[buttons]) \n description 页面上的输入框 、.join(snapshot[inputs]) \n description 页面正文摘要 snapshot[summary] \n return description这段描述直接作为 Prompt 的一部分传给模型模型就能对页面形成基本认知。实际操作中我还会在描述里加上“可见性”标签比如某个按钮在视口内还是视口外某个输入框是否被禁用这些状态信息对模型决策非常关键。因为一个聪明的 Agent 应该知道按钮存在不代表能点输入框存在不代表能输入。把这类感知信息显式写进描述里模型的规划和执行才会更贴近真实场景。4. 打通 AI 决策感知结果如何真正被用起来4.1 从 Web 自动化到 Agent 工具调用很多人问我为什么强调把技能封装成“Skill”而不是直接写成函数。区别在于普通函数知道调用时机而 Agent 场景里调用时机是模型决定的所以技能必须能被“描述清楚”。我设计接口时强制要求每个技能都有 description 字段就是要让模型能判断“什么情况下该用这个工具”。例如 page_snapshot 的描述我会写成获取当前页面的结构化摘要包括标题、URL、可见按钮与输入框。当你需要了解当前页面状态或决定下一步操作时调用。这里涉及到一个细节技能的 description 写得越具体模型的工具选择准确率就越高。如果只写“页面快照”模型可能不知道什么时候调用它如果写清楚“当用户询问当前页面有什么内容时调用”“当点击后需要验证页面变化时调用”模型的调用行为就会更精准。我建议把每个技能支持的关键场景写两三条这样既不会过度膨胀又能保证模型在大多数情况下做出正确选择。4.2 自动化的自动失败与重试感知边界扩宽以后脚本的失败模式也变了不再是简单的元素找不到而是能区分出“页面还没加载完”“接口报错导致数据为空”“按钮被遮罩挡住”等不同原因。针对不同原因重试策略应该不一样。我封装了一个通用的重试助手接收一个条件函数只要条件不满足就重试同时允许设置最大次数和间隔时间。async def retry_until(condition_func, max_retries3, interval_seconds1.5): for attempt in range(max_retries): result await condition_func() if result: return result if attempt max_retries - 1: await asyncio.sleep(interval_seconds) raise RuntimeError(f条件在 {max_retries} 次尝试内未满足)这里的关键是条件函数要“有感知”。比如检查一个页面是否加载完条件不能只是“某个元素出现了”最好同时检查对应接口是否返回成功、页面是否有报错提示。这样重试就不是盲目的而是在明确知道“还差什么”的情况下给系统一个补齐的机会。真正跑线上任务时这套机制能救回大量因为瞬时抖动导致的失败最终成功率从 92% 提升到 99% 左右。4.3 感知结果如何反哺业务决策感知的最终目的是决策。我做一个定时巡检系统时让脚本每天早上七点跑到数据后台先看接口平均延迟如果延迟超过阈值且页面出现告警文案就自动截全屏留存证据并触发告警通知。整个过程不需要人盯着屏幕因为感知边界接管了“发现问题、留证、通知”的完整链路。这里的核心不是自动化本身而是自动化能感知到“什么算异常”。类似地在爬虫场景里感知结果可以用来判断采集的数据质量。如果页面主体区域出现异常文案比如“系统繁忙”“内容已删除”脚本能主动终止任务而不是继续抓一堆垃圾数据。这个能力让自动化系统具备了最基本的“自我判断力”。我习惯在每个关键节点后都把感知摘要写入日志这样事后排查时可以回放系统当时的“所见所得”定位问题会快很多。5. 常见问题与排查技巧实录5.1 动态加载内容抓不到怎么办动态内容抓不到的典型场景是列表在滚动到页面底部时才加载新数据或者点击某个按钮后才异步插入内容。这时候不能只靠等待元素出现因为元素可能一开始不存在。我的解决办法是组合使用滚动和网络感知先监听接口请求然后执行滚动操作最后等待特定接口响应完成再读取页面内容。滚动可以用 page.mouse.wheel 或者 page.evaluate 操作 window.scrollBy关键是每次滚动后留出渲染时间。还有一个容易忽略的细节某些页面的动态加载依赖 IntersectionObserver 机制元素必须真实进入视口才会触发请求。这种情况下直接用 locator 去查即使元素存在于 DOM 里也不一定触发加载。正确做法是先把目标元素滚动到可视区域等待网络请求发生后再重新获取元素列表。我习惯用 expect 等待接口响应事件来作为加载完成的信号比单纯等元素稳定要可靠得多。5.2 选择器今天能用明天就挂这是最让人头疼的问题。页面改个 class、调个层级脚本就全废了。我的策略是分层处理优先用角色和文本选择器其次用>
阅读完成 · 觉得有帮助?