可访问性树不是一个更便宜的页面:衡量代理阅读的内容
Expert Network Defense Engineer
TL;DR:
- 可访问性树是大多数浏览器代理提供其模型的内容,而不是通过
Accessibility.getFullAXTree从原始 HTML 中获取的内容。 - 它 不是 页面内容的压缩。在一个实时目录页面上:原始 HTML 9,824 个令牌,
innerText582,完整的可访问性树 5,951 — 这棵树的成本是10.2× 普通文本。 - 原因在节点角色中可见:在 1,401 个节点中,267 是
StaticText和 391 是InlineTextBox,所以大多数字符串被存储了两次。 - 过滤到交互角色则有 742 个令牌覆盖 114 个节点 — 1.3×
innerText— 同时保留代理需要点击的所有内容。 - 通过本地 Chromium 和 Scrapeless Scraping Browser 测量,每个数字都是相同的,因此代理的页面表现不会在不同环境之间漂移。
- Scrapeless 免费计划覆盖本指南中运行的云浏览器。
每个浏览器代理在执行任何操作之前必须回答一个问题:你会向模型提供什么?一个 51,004 字符的 HTML 文档不适合一个合理的提示预算,而截图会消耗图像令牌并丢失精确字符串。通常的第三种答案是可访问性树。
这个答案在形式上是正确的,但在价格上是错的。树携带角色和名称 — link, button, heading — 这些正是代理决定在哪里点击所需的内容。它也是未过滤的,比页面的普通文本贵一个数量级。
本指南通过 CDP 拉取树,将其与同一实时页面上的替代方案进行比较,并展示使其值得传送的过滤器。
可访问性树是什么
浏览器在 DOM 旁边构建了第二棵树,以供屏幕阅读器使用。每个节点都有一个 role — 按照 WAI-ARIA 规范 定义的控件类型之一 — 以及一个 name,这是屏幕阅读器宣布的字符串,根据 可访问名称与描述计算 中的算法导出。展示性包装会被合并,aria-* 属性会被解析,交互元素会被标记。
这种结构就是代理喜欢它的原因。link: Books to Scrape 是直接可操作的,而 <div class="col-sm-8 h1"><a href="..."> 则不是。该树通过 Chrome 开发者工具协议 的可访问性域公开,且它与 Chrome 在其自身可访问性面板中渲染的数据相同。如果 CDP 本身是新的, 协议入门 可以覆盖本文所依赖的传输。
安装
bash
pip install playwright tiktoken
playwright install chromium
tiktoken 只是用来计数令牌;提取仅需要 Playwright。验证运行使用了 Playwright 1.59.0 和 tiktoken 0.12.0。
拉取树
Playwright 暴露一个原始的 CDP 会话,这就是你如何访问它不包装的域:
python
cdp = page.context.new_cdp_session(page)
nodes = cdp.send("Accessibility.getFullAXTree")["nodes"]
每个节点都是一个字典,其 role 和 name 本身是具有 value 键的对象。大多数节点根本没有名称 — 容器、被忽略的节点和布局框 — 因此有用的投影是角色加上名称,以用于有命名的节点:
python
def ax_lines(nodes, roles=None):
lines = []
for node in nodes:
role = (node.get("role") or {}).get("value", "")
name = ((node.get("name") or {}).get("value") or "").strip()
if not name:
continue
if roles is not None and role not in roles:
continue
lines.append(f"{role}: {name}")
return lines
在一个实时书籍目录上,它产生的行如下:
text
RootWebArea: All products | Books to Scrape - Sandbox
heading: All products
link: Books to Scrape
StaticText: We love being scraped!
link: Home
StaticText: /
InlineTextBox: /
那七行中的两行是相同的斜杠。该重复性是整个成本故事。
将其与替代方案进行比较
计算三种相同页面表示的令牌数:
python
def measure(page, label):
html = page.content()
text = page.evaluate("document.body.innerText")
cdp = page.context.new_cdp_session(page)
nodes = cdp.send("Accessibility.getFullAXTree")["nodes"]
full = "\n".join(ax_lines(nodes))
acts = "\n".join(ax_lines(nodes, INTERACTIVE))
roles = [(n.get("role") or {}).get("value", "") for n in nodes]
text
raw html tokens= 9824 chars=51004
innerText tokens= 582 chars=2029
AX full tokens= 5951 named_nodes=864
AX interactive only tokens= 742 nodes=114
AX total nodes 1401
StaticText nodes 267
InlineTextBox nodes 391
price in html/text/ax: True/True/True
完整的可访问性树的成本为 5,951 个令牌,而 innerText 的成本为 582。它是 相同页面普通文本的 10.2×,并且大约是其应替代的原始 HTML 的 60%。
角色计数解释了原因。在 1,401 个节点中,267 是 StaticText 和 391 是 InlineTextBox — 658 个节点,几乎是树的一半,致力于页面已包含一次的文本。InlineTextBox 节点是布局片段:一个单句被分成两行渲染,变成两个这样的节点。发送完整树意味着为每个字符串至少支付两次费用。
值得明确指出的是:没有任何内容丢失。价格 £51.77 在 HTML 中存在,在 innerText 中存在,也在可访问性树中存在。在此页面上,树更贵,而不是更不完整。
过滤到代理可以操作的内容
读取页面的代理需要文本。操作 页面 的代理需要可以点击和输入的内容。这是一小组角色:
python
INTERACTIVE = {"link", "button", "textbox", "combobox", "checkbox", "radio", "menuitem", "tab"}
将该集合传递给相同的函数会将树简化为 114 个节点和 742 个令牌 — 1.3× innerText,且比未过滤的版本便宜 8 倍。每个链接和控件保留其可访问名称,这正是点击指令所指的内容。
这表明分工而非选择。当模型必须读取和提取时使用 innerText,当它必须决定操作什么时使用角色过滤树,并在任务需要两者时发送两者——它们加起来是 1,324 个标记,仍然是原始 HTML 的八分之一。代理循环指南 介绍了做出该决定后发生的事情。
在抓取浏览器上运行
以上内容无需本地浏览器。Scrapeless 抓取浏览器 使用相同协议,因此 CDP 会话和可访问性调用保持不变——仅连接不同:
python
endpoint = (
"wss://browser.scrapeless.com/api/v2/browser"
f"?token={os.environ['SCRAPELESS_API_KEY']}&sessionTTL=180&proxyCountry=ANY"
)
with sync_playwright() as p:
browser = p.chromium.connect_over_cdp(endpoint, timeout=90000)
page = browser.new_page()
page.goto(URL, wait_until="domcontentloaded")
measure(page, "Scrapeless Scraping Browser")
browser.close()
云运行在每一个指标上返回相同的数字:9,824 / 582 / 5,951 / 742 个标记,1,401 个节点,相同的 StaticText 和 InlineTextBox 计数。这有一个实际的后果。一个在你的笔记本电脑和生产环境之间转换的页面表示会使代理行为不可重复;而这个不是。将密钥保存在环境中作为 SCRAPELESS_API_KEY,并查看抓取浏览器介绍以获取其余会话参数。
开始使用只需一分钟——创建一个免费的 Scrapeless 账户,而免费计划覆盖此运行。
运行它
bash
export SCRAPELESS_API_KEY="your-api-key"
python3 ax_demo.py
验证运行的完整输出:
text
playwright 1.59.0 | tiktoken 0.12.0
[local chromium]
raw html tokens= 9824 chars=51004
innerText tokens= 582 chars=2029
AX full tokens= 5951 named_nodes=864
AX interactive only tokens= 742 nodes=114
AX total nodes 1401
StaticText nodes 267
InlineTextBox nodes 391
price in html/text/ax: True/True/True
[Scrapeless Scraping Browser]
raw html tokens= 9824 chars=51004
innerText tokens= 582 chars=2029
AX full tokens= 5951 named_nodes=864
AX interactive only tokens= 742 nodes=114
AX total nodes 1401
StaticText nodes 267
InlineTextBox nodes 391
price in html/text/ax: True/True/True
ratios vs innerText: html=16.9x ax_full=10.2x ax_interactive=1.3x
故障排除
Accessibility.getFullAXTree 返回的节点非常少。 树是懒惰构建的。在请求内容之前导航并等待, 如果你的客户端没有隐式执行,首先调用 Accessibility.enable。
每个节点都有一个空名称。 你直接读取 node["name"]。 role 和 name 都是对象——字符串在 node["name"]["value"]。
同一页面的两次运行之间节点计数不同。 InlineTextBox 节点遵循行换行,因此不同的视口宽度会改变节点数量。 如果计数本身很重要,请设置明确的视口。
树省略了屏幕上可见的内容。 被标记为 aria-hidden 的内容,以及纯粹由 CSS 生成的文本 ::before/::after,是有意缺失的。请从 DOM 中读取这些;可访问性树并不是它的替代品。
角色看起来不熟悉。 StaticText,InlineTextBox 和 RootWebArea 是内部 Chrome 角色,而不是 ARIA 角色,这就是为什么过滤 ARIA 仅允许列表会丢弃大部分树的原因。核心可访问性 API 映射 定义了浏览器必须公开的角色;超出该集合的任何内容都是特定于引擎的。
结论
可访问性树是代理应给予的良好答案,而在其原始形式中是一个糟糕的默认设置。在这里测量的页面上,它花费 5,951 个标记——是通常假设可以压缩的纯文本的十倍——因为几乎一半的节点存在是为了描述页面已经一次陈述过的文本。
在过滤到代理可以操作的角色后,同一树是 742 个标记,并且仍然命名每个控件。这是放入提示中的版本,当任务还需要读取时可以与 innerText 配对。在选择之前在你自己的目标上测量两者:这里的数字来自一个目录页面,比例完全取决于页面有多少是散文,多少是控件。
准备尝试吗? 从 Scrapeless 免费计划开始,并查看当前定价以获取更高的数量。
常见问题
问:我应该发送可访问性树而不是 HTML 吗?
发送一个过滤过的版本。未过滤版本为 5,951 个标记,而原始 HTML 为 9,824——这是节省,但远低于预期。限制为互动角色后降至 742,真正的减少就在这里。
问:可访问性树比纯文本便宜吗?
不,这是一种常见的误解。在这里测量的页面上,它的成本是 10.2× innerText。树的价值在于它添加的角色标签,而不是更小的负载。
问:为什么有这么多 InlineTextBox 节点?
它们是布局片段 — 每行渲染的文本一个。跨越两行的句子会生成两个片段,位于持有相同字符串的 StaticText 节点上。这就是为什么测试页面上有 1,401 个节点中有 658 个是文本重复。
问:树包含页面上的所有内容吗?
并不完全是。aria-hidden 内容和由 CSS 伪元素生成的文本被故意排除。在这里测量的页面上没有缺少任何需要的内容 — 价格在所有三种表现形式中均出现 — 但请确认您自己的目标,而不是假设。
问:我需要本地 Chrome 来读取可访问性树吗?
不需要。本指南中的云运行产生的字节对本地 Chromium 是完全相同的,因为两者使用相同的协议。只有连接线路发生变化。
在Scrapeless,我们仅访问公开可用的数据,并严格遵循适用的法律、法规和网站隐私政策。本博客中的内容仅供演示之用,不涉及任何非法或侵权活动。我们对使用本博客或第三方链接中的信息不做任何保证,并免除所有责任。在进行任何抓取活动之前,请咨询您的法律顾问,并审查目标网站的服务条款或获取必要的许可。



