☰
同一页面四种读法:invisible playwright MCP 的 text、HTML、snapshot 与 screenshot 如何取舍
2026/10/1 2:35:57 网站建设 项目流程
  • 人工智能
  • AI Agent
  • 浏览器控制
  • GUI 自动化
  • MCP 服务

【免费下载链接】invisible_playwright_mcp

Playwright MCP server undetected by anti-bots and captchas: AI agent browses the web on anti-detect stealth Firefox, Python, undetected browser automation, scraping, computer use.

项目地址:https://gitcode.com/GitHub_Trending/jo/invisible_playwright_mcp
点击查看免费下载

一个浏览器 MCP server 向模型呈现页面的方式不止一种,而这几种方式并不能互相替代:在 invisible playwright MCP 中,同一个页面既可以作为纯文本、作为清洗后的 HTML、作为可交互元素清单(snapshot)返回,也可以作为一张截图进入上下文。它们的字节成本相差近九十倍,而且这个比例会随页面类型翻转。本文以 2026-09-17 在本地对实际页面所做的逐工具实测为主线,结合仓库源码(读取工具实现、工具注册、HTML 清洗管线)说明四种读法的成本结构、截断行为与适用场景,读完你可以在自己的任务里按一条规则选对读取方式,避免"十次截图烧掉整个上下文窗口"这类典型失败。

同一个小页面,四种读法的账单

先把四种工具在同一个小页面上依次调用,记录返回负载的字节数。该页面由 3,665 字节的 HTML 服务而来:一个标题、一个段落、二十项导航列表、一个含五个控件的表单和一张四十行的表格,没有图片,每个工具跑三次取中位数:

页面如何返回字节数相对倍数
browser_read_text1,0211x
browser_read_html1,3421.3x
browser_snapshot2,1862.1x
browser_take_screenshot90,76889x

表格里每一行都是同一个页面。最后一行是第一行的八十九倍,原因在于:一张文字的图片并不是文字。截图以 base64 形式(此处 90,768 字节)进入模型上下文,并且在它到达的那一轮和之后每一轮都会持续占位——除非你的客户端主动裁剪它。

对照源码可以确认这四种负载的生成方式(actions.py):

  • read_text通过document.querySelector(sel)取元素的innerText,返回的是去掉了标记的可见文字(actions.py#L122-L144);
  • read_html先在浏览器里对页面克隆求值,再交给 Python 侧的清洗函数,返回精简后的标记(actions.py#L396-L414);
  • snapshot在浏览器里执行只读脚本,枚举实际可见的交互元素,序列化为 JSON(actions.py#L378-L393);
  • screenshot_png直接调用session.page().screenshot()返回 PNG 原始字节,再由工具层包装成 MCP Image(actions.py#L417-L423、server.py#L529-L533)。

也就是说:文本行是字符,截图行是像素编码。成本差异不是实现巧合,而是四种表示形式本身的差异。

同一个长页面,排序反了过来

不要把小页面上得到的比例照搬到长页面。同样的四个工具,换到一个含400 个段落、服务端返回 37,538 字节 HTML的页面上重新测量:

页面如何返回字节数说明
browser_read_text,默认参数6,101被截断,并且它自己说了
browser_read_text,max_chars: 5000035,490完整文本
browser_read_html37,531不截断
browser_snapshot78该页面没有任何交互元素
browser_take_screenshot123,940一个视口

这里有三点变化,每一处都推翻了你可能从小页面表格里得出的结论。

文本默认截断,而且会明确告诉你。默认读取的尾部原文是:6000 of 35490 characters. Raise max_chars, or narrow the selector to the part you need.这是最好的行为,因为替代方案是"静默地返回一段残缺答案"——模型会把断在句子中间的文字当成完整的页面来回答。换言之:在一个小页面上看起来完整的读取,在长页面上可能只是全文的十分之一,除非你调高上限或把 selector 收窄到需要的部分。该上限在源码中定义为常量DEFAULT_MAX_CHARS = 6000(actions.py#L26),并同时出现在工具签名与工具描述里;截断标记的生成逻辑见 actions.py#L140-L144。

HTML 不截断,所以长页面上它才是贵的那一个。小页面上它只是文本的 1.3 倍,长页面上它却是四种负载中最大的文本负载(37,531 字节),比完整文本还大。"HTML 是便宜的升级"这条经验只在小页面上成立。原因同样写在注册代码里:browser_read_html明确声明"Unlike browser_read_text this is NOT capped",因为把标记从中间切断会留下毫无意义的残破标签,所以宁可返回完整的数万字符(server.py#L509-L526)。

snapshot 根本不是文本。这个页面返回 78 字节,因为它没有任何交互元素。snapshot 列出的是你可以施加动作的东西,所以它的大小跟随控件的数量而不是内容的多少。在表单密集的页面上它相当可观,在文章页上它近乎为空——这恰恰是正确的行为,与"文本的 2.1 倍"给人的印象完全不是一回事。

这一关系随后又在仅交互控件数量不同、其余完全相同的四个页面上做过测量,结果约为每个交互元素一百字节,详见 what a page snapshot costs, per control。该文档同时给出了四页对照表:交互元素为 0、5、20、60 时,snapshot 分别为 96、574、2,065、6,081 字节,而纯文本始终在 8~179 字节之间——两条线一条随控件陡增、一条几乎平直,快照的成本与页面"说了什么"无关,只与页面"能做什么"有关。

一条规则定取舍

先要能装下答案的最便宜表示,装不下再升级。

  • browser_read_text——当你要的是"页面说了什么"。价格、地址、确认号、一篇文章。它是 snapshot 的十分之一、截图的一百分之一。
  • browser_snapshot——当你要"对页面做点什么"。四种读法里只有它把每个元素的selector 和坐标一并交还,而这正是点击或填写所需的输入。它花的是页面控件的钱,不是页面内容的钱。
  • browser_read_html——当结构本身就是内容:要逐行读的表格、属性、藏在 data 属性里的真实值。在文本失败之后再去拿它,而不是之前;在长页面上要知道它是四种文本负载里最大的,而不是"便宜的升级"。
  • browser_take_screenshot——当答案本质上是视觉的、其他方式都做不到:没有底层表格的图表、布局问题、canvas、意义在于"什么叠在什么上面"的页面。

仓库里"如何驱动页面"的说明把这套顺序落实成了一个行动阶梯(server.py#L160-L190):

  1. 具名工具 + selector:browser_click、browser_type、browser_select_option、browser_press_key。browser_snapshot给每个元素生成一个保证无歧义的 selector,照抄即可;
  2. 坐标:snapshot 为每个元素报告at: [x, y](视口像素),browser_click_at直接吃这组数。这一级服务于 selector 描述不了的东西——canvas、slider、地图、用 div 拼出来的自定义控件;
  3. 你的眼睛:browser_take_screenshot看画面,再用browser_click_at点坐标,用于 snapshot 根本没有列出的元素;
  4. browser_evaluate:只用于读取以上都看不到的东西。

注意第 4 级在"执行"方向上被刻意加了护栏:browser_evaluate拒绝通过赋值value、调用click()、dispatchEvent()等脚本方式操作页面,因为那些路径绕过了真实键盘与指针,事件到达时isTrusted为 false——这正是"有东西在冒充人"的最清晰信号,而这个浏览器的存在意义就是不产生它。读,随你;写,走前三级。

昂贵的那个也有公道:截图该用就用

只盯着字节数就否定截图是错的。截图是唯一能展示"实际呈现出来的是什么"的表示:被浮层盖住的是什么、折叠线以下是什么、视觉上被禁用的是什么、两个一模一样的标签里人眼会去点哪一个。一个模型在推理"总计旁边那个按钮"时,它推理的对象就是一张图片——这时候递给它页面文本,等于递给它另一个问题。

所以规则不是"永远不要截图",而是:当视觉排布本身就是问题所在时,刻意地、一次性截图;其余时间读文本。要避免的失败模式是那种出于谨慎、每步操作后都截一张图的循环——在那里,八十九倍的乘数会直接吃掉整个上下文窗口。

这一点在工具描述里也有印证:browser_take_screenshot的定位是"on demand",按需一次(server.py#L529-L533);而专用于"看 Agent 干活"的browser_watch走的是另一条路径——持续抓取浏览器窗口画面供界面实时预览,其描述明确警告窗口像素不能喂给browser_click_at,要行动还得用browser_take_screenshot(server.py#L536-L553)。看归看、动归动,工具之间职责不混。

snapshot 到底买了什么

这是最容易被跳过的一种读法,值得单独说。snapshot 不是"加了额外重量的文本"。它逐元素报告一次工具调用所需的身份信息:一个被构造为无歧义的 selector,以及该元素在视口像素中的位置。

正是这一点让上面的三级阶梯成立:先是有名字的工具 + selector;当没有 selector 能描述目标时(canvas、slider、地图),用 snapshot 提供的坐标;只有 snapshot 连元素都不列时,才动用截图。该排序背后的设计推理见 How the tools are shaped, and why。

实现上,snapshot 脚本只做读取(注释明确:给它编号就意味着往页面里写属性,而这是一个以"不留检测痕迹"为存在理由的产品,不能有这种表面;见 actions.py#L156-L160)。"可见"的判断用offsetParent !== null,这样被 CSS 藏起来的元素不会混进清单;并且它不是无障碍树——原因是实测出来的:一个真实的注册页面上,单个国家<select>会贡献约两百个<option>节点,足以在字符上限耗尽之前把调用者要找的表单挤出画面(actions.py#L378-L388)。快照只关心"能被操作的元素",这一取舍同时得到测试的保护:tests/mcp_server/test_snapshot_visibility.py守护可见性过滤,tests/mcp_server/test_snapshot_handles.py与tests/mcp_server/test_snapshot_capping.py守护元素句柄与上限行为。

HTML 读法的三种模式

browser_read_html不是把原始 HTML 原样丢给模型。它分两步走,而两步必须分开,因为"什么真的被画出来了"(计算样式与布局)只存在于浏览器里——它在一个克隆上做这件事,绝不写回活动页面;随后字符串回到 Python 侧做结构化清洗,这一步不依赖浏览器、可以纯测试。入口是 clean_page,支持三种模式:

  • mode="form"(默认):交互表面 + 解释它的文本;
  • mode="text":纯正文,标记全部去掉;
  • mode="full":去掉噪声、精简属性,保留结构。

清洗管线依次做:解析(Lexbor HTML parser)、丢噪声、丢内联隐藏、按模式分流(text 直接转纯文本)、精简属性、合并折叠的<option>、按 form 模式裁剪到骨架、解包无意义包装器、丢空节点(clean.py#L599-L622)。不认识的 mode 会抛CleanError。

清洗的实际收益记录在read_html的 docstring 里(actions.py#L405-L407):在真实页面的语料上测量,9.6 MB 的标记被压到 293 KB,缩小 97%,而 1,453 个交互元素全部保留,每页中位耗时 48 ms。这是仓库注释中的实测数字,也是"HTML 值得读"的前提——它读的是被清洗过的结构,不是原始 soup。相关行为由tests/mcp_server/test_read_html_in_a_browser.py与tests/mcp_server/test_clean.py覆盖。

这不是在讲什么

有一个形状相似、容易混为一谈的问题:读 DOM 的 Agent 和点像素的 Agent,在被检测这件事上是否不同。那是关于"网站能看到什么"的问题,不是关于"你的上下文花多少钱"的问题,它有自己独立的答案。本文只谈账单,不谈检测面——这两个话题的混淆会导致你修错方向:以为省上下文能解决被检测,或以为换检测策略能解决烧上下文。

常见问题速答

我的 Agent 该用截图还是 DOM?论成本,用 DOM,小页面上相差约九十倍。凡视觉排布本身即问题所在的场景,用截图,而且刻意地、只用一次。

一张截图占多少上下文?以本文测量的页面为例,90,768 字节 base64,对 1,021 字节文本。它随视口缩放,不随页面内容多少缩放:一个几乎空白的页面照样花费一整张图。

为什么短任务也会耗尽上下文?数一数截图。同一页面截十张就是九十万字节,而任务本身可能只有一千字节。

read_text和snapshot有什么区别?文本是页面说的,快照是页面提供的:元素、selector、位置。前者用来读,后者用来动。

HTML 什么时候才是正确答案?当结构携带文本会丢失的含义时:表格单元格、属性、存在标记里而非展示出来的值。小页面上它是文本的 1.3 倍;长页面上它是四种文本负载里最大的,所以"便宜的升级"只在页面短时成立。

为什么我的读取回来是截断的?文本读取有默认上限。本文的长页面上它返回了 35,490 字符中的 6,000 字符,并附带一行话把这件事说明白。调高max_chars,或收窄 selector。要点是:长页面上的短答案不代表页面本身短。仓库侧的测试也守住了这条承诺——tests/mcp_server/test_json_capped.py验证被截断的 JSON 会如实报告truncated与字符数,而不是静默截短。

延伸阅读:

  • how many MCP tools is too many——统计另一种常驻上下文成本:无论你是否读取页面,每轮都要付出的那一份;
  • how long an AI browser agent takes per step——四种读法在墙上时钟上都是免费的,它们只在上下文上不同;
  • what a page snapshot costs, per control——快照成本约每交互元素一百字节的完整测量。

方法:一分钟在你的页面上重测

本文两张表格故意互相矛盾,而这正是结论:在一张页面上量出的比例,只是关于那张页面的事实。要在自己的任务上验证,方法只需要一分钟:把四种工具各调用一次,比较返回负载的长度。字节数是工具返回的文本或图片负载的长度,截断行为看返回文本的尾部是否带标记;实测环境是经 stdio 连接的 MCP server,页面由127.0.0.1本地服务,小页面数字为中位数三次运行、长页面每种工具各跑一次。在任何新页面上重跑这个实验,比把"八十九倍"或"2.1 倍"当普适常数带进新项目要可靠得多——成本结构随页面类型翻转,唯一稳的规则是:先要最便宜的,装不下再升级。

  • 人工智能
  • AI Agent
  • 浏览器控制
  • GUI 自动化
  • MCP 服务

【免费下载链接】invisible_playwright_mcp

Playwright MCP server undetected by anti-bots and captchas: AI agent browses the web on anti-detect stealth Firefox, Python, undetected browser automation, scraping, computer use.

项目地址:https://gitcode.com/GitHub_Trending/jo/invisible_playwright_mcp
点击查看免费下载

相关推荐

上一篇:3个维度重塑QQ空间记忆:GetQzonehistory如何帮你找回消失的青春印记
下一篇:三步找回QQ空间全部历史说说的完整指南:用Python永久保存你的青春记忆

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询