アクセシビリティツリーは安価なページではない: エージェントが読むものを測定する
Expert Network Defense Engineer
TL;DR:
- アクセシビリティツリーは、ブラウザエージェントが生のHTMLの代わりにモデルにフィードするものであり、
Accessibility.getFullAXTreeを介してChrome DevTools Protocolから取得されます。 - それはページの圧縮ではありません。ライブカタログページ上では、生の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 DevTools Protocolのアクセシビリティドメインによって公開されており、それは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: /
それらの七行のうち二つは同じスラッシュです。その重複が全体のコストストーリーです。
代替に対して測定
同一ページの3つの表現のトークンをカウントします:
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
完全なアクセシビリティツリーはinnerTextの582に対して5,951トークンのコストがかかります。それは**同じページのプレーンテキストの10.2×**であり、置き換えるはずだった生のHTMLの約60%です。
ロールカウントがそれを説明します。1,401ノードのうち267はStaticText、391はInlineTextBoxです — 658ノードが、そのページにすでに一度含まれているテキストに捧げられています。InlineTextBoxノードはレイアウトフラグメントです:1つの文が2つの表示行にまたがって破られると、それらのうちの2つになります。完全なツリーを送信することは、すべての文字列に対して少なくとも二重の支払いを意味します。
はっきり言っておくべきこと:何も失われていません。価格£51.77はHTMLに存在し、innerTextにあり、アクセシビリティツリーにあります。このページでは、ツリーはより高価ですが、より完全ではありません。
エージェントが操作可能なものにフィルタリング
ページを読んでいるエージェントにはテキストが必要です。ページを操作するエージェントには、クリックしたり入力したりできるものが必要です。それらは小さなロールのセットです:
python
INTERACTIVE = {"link", "button", "textbox", "combobox", "checkbox", "radio", "menuitem", "tab"}
そのセットを同じ関数に渡すと、ツリーは114ノードと742トークンに縮小されます — 1.3× innerText、フィルタなしのバージョンに対して8×安いです。すべてのリンクとコントロールはそのアクセシブル名を保持しており、それがクリック指示に参照されるものです。
それは選択よりも分割を示唆しています。モデルが読み取りと抽出を行う必要があるときには innerText を使用し、何を操作するかを決定する必要があるときには役割フィルタリングツリーを使用し、タスクに両方が必要なときには両方を送信します — それらを合わせると1,324トークンで、未加工のHTMLの約8分の1です。エージェントループガイド では、その決定が下された後に何が起こるかについて説明しています。
スクレイピングブラウザで実行する
上記の内容はローカルブラウザを必要としません。Scrapeless Scraping Browserは同じプロトコルを話すので、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 として保持し、残りのセッションパラメータについてはScraping Browserのイントロダクション を参照してください。
始めるのは1分で済みます — 無料の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"] にあります。
同じページの2回の実行でノード数が異なります。 InlineTextBox ノードは行の折り返しに従うため、異なるビューポート幅がいくつあるかを変えます。カウント自体が重要な場合は明示的なビューポートを設定してください。
ツリーは画面上で可視の何かを省略します。 aria-hidden でマークされたコンテンツや、純粋にCSS ::before/::after によって生成されたテキストは故意に欠如しています。それらはDOMから読み取ってください;アクセシビリティツリーはそれの代わりにはなりません。
役割がなじみがありません。 StaticText、InlineTextBox、および RootWebArea は、ARIAのものではなく内部Chromeの役割です。これが、ARIA専用のホワイトリストでフィルタリングするとツリーの大半が落ちる理由です。コアアクセシビリティAPIマッピングは、ブラウザが公開しなければならない役割を定義しています;それを超えるものはエンジン固有です。
結論
アクセシビリティツリーは、エージェントに与えるべき良い答えですが、その生の形式では悪いデフォルトです。ここで測定されたページでは5,951トークンのコストがかかります — よく圧縮されると想定される平文の10倍のコストです — なぜなら、そのノードのほぼ半分はページがすでに一度述べたテキストを説明するために存在するからです。
エージェントが行動できる役割にフィルターされた同じツリーは742トークンで、すべてのコントロールに名前を付けています。それはプロンプトに入れるべきバージョンであり、タスクが読み取りを必要とする場合は innerText と組み合わせるべきです。選択する前に、あなた自身のターゲットで両方を測定してください:ここでの数値は1つのカタログページから来ており、比率はページのどれだけが散文でどれだけがコントロールであるかに完全に依存します。
試してみる準備はできましたか?Scrapelessの無料プランを始める で、高いボリュームのための現在の料金を確認してください。
FAQ
Q: アクセシビリティツリーをHTMLの代わりに送信すべきですか?
フィルタリングされたバージョンを送信してください。未フィルタリングの場合、未加工のHTMLが9,824に対して測定され、5,951トークン — 節約にはなりますが、期待よりはるかに少ないです。インタラクティブな役割に制限すると742まで落ち込み、これが本当の削減です。
Q: アクセシビリティツリーは平文より安いですか?
いいえ、これが一般的な誤解です。ここで測定されたページでは10.2× innerText のコストがかかりました。ツリーの価値は追加される役割ラベルにあり、より小さいペイロードではありません。
Q: なぜそんなに多くの InlineTextBox ノードがありますか?
それらはレイアウトの断片であり、テキストの描画された行ごとに1つずつ存在します。2行にわたる文章は、同じ文字列を保持するStaticTextノードの上に2つの断片を生成します。これは、テストページの1,401ノードのうち658がテキストの重複であった理由です。
Q: ツリーはページ上のすべてを含んでいますか?
必ずしもそうではありません。aria-hiddenコンテンツやCSSの擬似要素によって生成されたテキストは意図的に除外されています。ここで測定されたページでは必要なものはすべて揃っていました - 価格は3つの表現すべてに表示されました - ですが、仮定するのではなく自分のターゲットで確認してください。
Q: アクセシビリティツリーを読むためにローカルChromeが必要ですか?
いいえ。このガイドでのクラウド実行は、ローカルのChromiumとバイト同一の数値を生成しました。なぜなら、両者は同じプロトコルを使用しているからです。接続ラインだけが変わります。
Scrapelessでは、適用される法律、規制、およびWebサイトのプライバシーポリシーを厳密に遵守しながら、公開されているデータのみにアクセスします。 このブログのコンテンツは、デモンストレーションのみを目的としており、違法または侵害の活動は含まれません。 このブログまたはサードパーティのリンクからの情報の使用に対するすべての責任を保証せず、放棄します。 スクレイピング活動に従事する前に、法律顧問に相談し、ターゲットウェブサイトの利用規約を確認するか、必要な許可を取得してください。



