🎯 カスタマイズ可能で検出回避型のクラウドブラウザ。自社開発のChromiumを搭載し、ウェブクローラーAIエージェント向けに設計されています。👉今すぐ試す
ブログに戻ります

PixelRAG + Scrapeless: スクリーンショットタイルのリモート化で何が壊れるか

Daniel Kim
Daniel Kim

Lead Scraping Automation Engineer

07-Aug-2026

TL;DR:

  • Visual RAGは、解析されたテキストの代わりにスクリーンショットタイルとしてページをインデックス化するため、タイルのジオメトリは見た目だけのものではなく、埋め込みモデルが実際に見るものを決定します。
  • PixelRAGのレンダラーは、Emulation.setDeviceMetricsOverrideを使って875×8192のキャプチャビューポートを設定し、その後、すべてのスクリーンショットを875ワイドにクリップします。
  • クラウドブラウザでは、リポートが成功を上書きし、要求した幅が適用されることはありません。セッションを通じて、結果として得られるレイアウトの幅は945から5120の間で変動し、時には変わらず、通常は広がり、875にはなりません。
  • クリップは正確に尊重されます。これが危険な理由です:正確なサイズのタイルが、ページの幅が広くレイアウトされている左側のスライスを含みます。
  • 実際のキャプチャでは、1920ワイドのレイアウトの875ワイドタイルが1つの製品列を持ち、ページは4つの製品列をレイアウトし、カードがボタンを通過してスライスされます。
  • rawFilePathのファストパスは、リモートブラウザに対してエラーなしで受け入れられ、代わりにインラインデータを返し、ローカルファイルは書き込みません。これにより、そのファイルを読み取るパイプラインは何も取得せず、すべての呼び出しが成功したように見えます。
  • 上書き前にinnerWidthに対して要求した幅と比較してから、単一のタイルを信頼してください。
  • Scrapelessの無料プランから始めて、独自のキャプチャジオメトリを測定してください。

Scrapeless Scraping BrowserはCDP WebSocketを介して操作するクラウドブラウザであり、以下のすべての測定は、ローカルレンダラーが使用するのと同じCDP呼び出しを使用して行われました。

ビジュアルリトリーバル増強生成はHTML解析をスキップします。ページをテキストに変換してチャンクを埋め込む代わりに、ページを画像にレンダリングし、画像を埋め込み、ビジョンモデルがピクセルから答えを読み取れるようにします。テーブル、チャート、マルチカラムレイアウトは生き残ります。何もテキストストリームにフラット化されることはなかったからです。

これは、画像が正しい場合にのみ機能します。タイルレンダラーは特定の約束をします — ページは固定幅でレイアウトされ、その後固定高さのタイルにスライスされます — そして、ビジュアルインデックスはその約束と同じだけ良いものです。PixelRAGは現在の最も明確な例であり、そのレンダラーを注意深く読む価値があります。なぜなら、それが持つ前提は、すべてのスクリーンショットパイプラインが持つ前提だからです。この投稿では、あなたが起動していないブラウザ上でその前提に何が起こるかを測定します。

タイリングがどのように機能するべきか

PixelRAGのファストレンダラーは、ドライブラリを介さずにWebSocketを介して生のCDPを使用します。render/src/pixelrag_render/backends/fast_cdp.pyの先頭で2つの定数を設定します:

python Copy
VIEWPORT_WIDTH = 875
TILE_HEIGHT = 8192

次に、ワーカーごとに1回、それらをキャプチャビューポートとして適用します:

python Copy
await conn.cdp("Emulation.setDeviceMetricsOverride", {
    "width": VIEWPORT_WIDTH,
    "height": tile_height,
    "deviceScaleFactor": 1,
    "mobile": False,
})

タイルごとに、同じ幅でクリップされた長方形をキャプチャし、ページを下に進めます:

python Copy
await conn.cdp("Page.captureScreenshot", {
    "clip": {"x": 0, "y": t * tile_height, "width": VIEWPORT_WIDTH,
             "height": clip_h, "scale": 1},
})

ロジックは健全で標準です。Emulation.setDeviceMetricsOverrideは、CSSビューポート仕様の概念にマッピングされるCDP呼び出しであり、ビューポートを設定するときにドライバライブラリが内部で呼び出すものです。ページを875でレイアウトし、8192高さのストリップにカットし、ストリップを埋め込みます。あなた自身が起動したブラウザでは、まさにそれが起こります。

前提条件

  • Python 3.10以降とwebsocketsパッケージ。
  • ダッシュボードからのScrapeless APIキー、SCRAPELESS_KEYとしてエクスポート。
  • ローカルブラウザはありません。以下のすべての呼び出しはWebSocket接続を介してクラウドブラウザに送信されます。
bash Copy
pip install "websockets==15.0.1"
export SCRAPELESS_KEY="your_api_key_here"

信頼するのではなく上書きを測定する

上書きはエラーなしで結果オブジェクトを返します。これが問題の全てです — 応答にはリクエストが尊重されていないことを示唆するものは何もありません。代わりにページからレイアウト幅を読み取ってください。

python Copy
import asyncio, json, os

import websockets

CDP = ("wss://browser.scrapeless.com/api/v2/browser"
       f"?token={os.environ['SCRAPELESS_KEY']}&sessionTTL=120&proxyCountry=ANY")
WIDTH, HEIGHT = 875, 8192


async def call(ws, state, method, params=None, session=None):
    state["id"] += 1
    msg = {"id": state["id"], "method": method}
    if params:
        msg["params"] = params
    if session:
        msg["sessionId"] = session
    await ws.send(json.dumps(msg))
    while True:
        reply = json.loads(await ws.recv())
        if reply.get("id") == state["id"]:
            return reply


async def one_session(run):
    async with websockets.connect(CDP, max_size=50 * 1024 * 1024) as ws:
        state = {"id": 0}
        target = await call(ws, state, "Target.createTarget", {"url": "about:blank"})
        tid = target["result"]["targetId"]
        attached = await call(ws, state, "Target.attachToTarget",
                              {"targetId": tid, "flatten": True})
        sid = attached["result"]["sessionId"]
        await call(ws, state, "Page.enable", session=sid)
        await call(ws, state, "Page.navigate",
                   {"url": "https://books.toscrape.com/"}, session=sid)
        await asyncio.sleep(2.5)

        async def inner_width():
            r = await call(ws, state, "Runtime.evaluate",
                           {"expression": "innerWidth", "returnByValue": True},
                           session=sid)
            return r["result"]["result"]["value"]

        before = await inner_width()
        result = await call(ws, state, "Emulation.setDeviceMetricsOverride",
                            {"width": WIDTH, "height": HEIGHT,
                             "deviceScaleFactor": 1, "mobile": False}, session=sid)
        await asyncio.sleep(1)
        after = await inner_width()
        print(f"run{run}: error={result.get('error')} before={before} "
              f"after={after} requested={WIDTH} applied={after == WIDTH}")
        await call(ws, state, "Target.closeTarget", {"targetId": tid})
        return after


async def main():
    widths = [await one_session(run) for run in range(1, 4)]
    print(f"distinct widths: {sorted(set(widths))} · requested {WIDTH} ever applied: "
          f"{WIDTH in widths}")


asyncio.run(main())
text Copy
run1: error=None before=945 after=1280 requested=875 applied=False
run2: error=None before=945 after=1600 requested=875 applied=False
run3: error=None before=945 after=945 requested=875 applied=False
distinct widths: [945, 1280, 1600] · requested 875 ever applied: False

再度実行すると、after列は異なる数値を保持します。その不安定さはノイズではなく結果であり、押さえておくべきラインは最後のものです:要求した幅は適用された幅ではありません。

ここで分けるべきは3つの事柄です。呼び出しは失敗しません。最終的に到達する幅はセッションごとに異なります — 別々の実行の間に、値は945、1240、1280、1400、1440、1600、1680、1920、2560、および5120として返されました。そして、どのセッションにおいても、求められた875ではありませんでした。

上記の実行3に注意してください:時々幅は全く動かず、945の前が945の後になります。したがって、上書きは信頼できるノーオペではなく、信頼できる変更でもありません。すべてのセッションで保持されている唯一の特性は有用なもので、要求した数は取得する数ではありません。
実用的な解釈は、リモートブラウザが自分自身のウィンドウを所有しており、オーバーライドはあなたの数値ではなくそのウィンドウに対して再レイアウトされるということです。ローカルではプロセスを所有しているため、同じ呼び出しが動作します。

タイルが実際に含むもの

今、タイルレンダラーがするようにタイルをキャプチャし、あなたが設定したと思った幅で明示的にクリップします。

python Copy
import asyncio, base64, json, os, pathlib, struct

import websockets

CDP = ("wss://browser.scrapeless.com/api/v2/browser"
       f"?token={os.environ['SCRAPELESS_KEY']}&sessionTTL=120&proxyCountry=ANY")
WIDTH = 875


async def call(ws, state, method, params=None, session=None):
    state["id"] += 1
    msg = {"id": state["id"], "method": method}
    if params:
        msg["params"] = params
    if session:
        msg["sessionId"] = session
    await ws.send(json.dumps(msg))
    while True:
        reply = json.loads(await ws.recv())
        if reply.get("id") == state["id"]:
            return reply


async def main():
    async with websockets.connect(CDP, max_size=100 * 1024 * 1024) as ws:
        state = {"id": 0}
        tid = (await call(ws, state, "Target.createTarget",
                          {"url": "about:blank"}))["result"]["targetId"]
        sid = (await call(ws, state, "Target.attachToTarget",
                          {"targetId": tid, "flatten": True}))["result"]["sessionId"]
        await call(ws, state, "Page.enable", session=sid)
        await call(ws, state, "Page.navigate",
                   {"url": "https://books.toscrape.com/"}, session=sid)
        await asyncio.sleep(2.5)
        await call(ws, state, "Emulation.setDeviceMetricsOverride",
                   {"width": WIDTH, "height": 8192,
                    "deviceScaleFactor": 1, "mobile": False}, session=sid)
        await asyncio.sleep(1)

        layout = await call(ws, state, "Runtime.evaluate",
                            {"expression": "innerWidth", "returnByValue": True},
                            session=sid)
        shot = await call(ws, state, "Page.captureScreenshot",
                          {"format": "png", "captureBeyondViewport": True,
                           "clip": {"x": 0, "y": 0, "width": WIDTH,
                                    "height": 1200, "scale": 1}}, session=sid)
        image = base64.b64decode(shot["result"]["data"])
        png_w, png_h = struct.unpack(">II", image[16:24])
        print(f"layout width: {layout['result']['result']['value']}")
        print(f"tile size:    {png_w}x{png_h} (requested {WIDTH}x1200)")
        pathlib.Path("tile.png").write_bytes(image)
        await call(ws, state, "Target.closeTarget", {"targetId": tid})


asyncio.run(main())
text Copy
layout width: 1920
tile size:    875x1200 (requested 875x1200)

ここに印刷される幅と高さは、仮定されるのではなく、画像ヘッダーから直接読み取られます。なぜなら、PNG仕様が両方ともファイルの最初のチャンクに配置しているためです。タイルは要求されたサイズと正確に一致しています。それに対するページはかなり幅広くレイアウトされており、セッションごとに定まる幅が変わるため、キャプチャするページの部分も固定ではありません。

この実行から保存されたtile.pngは、そのコストを示しています。レイアウト幅が1920の場合、タイルはページが4つレイアウトする場所に1つの製品列を保持し、表示されるカードは「カートに追加」ボタンを通して切り取られており、ヘッダーのテキストは右端を超えています。ページ幅の半分以上が画像の外にあります。

テキストスクレーパーには関係ありません。なぜなら、それはDOMを読み取り、DOMは完全だからです。ビジュアルインデックスは完全に気にします:タイルは入力であり、ページの約半分は埋め込みモデルに到達しません。パイプラインのどこにもエラーがない場合、取得は劣化します。これが、要求された幅をinnerWidthと比較する価値がある理由です。

クラウドブラウザに対して自分のキャプチャジオメトリを確認したいですか? 無料のScrapelessアカウントを作成し、上記の2つのブロックを実行してください。

生キャプチャパスが消える

PixelRAGのスループットトリックは、ブラウザでのPNGエンコーディングをスキップすることです。Chromeに生ピクセルを共有メモリにrawFilePathパラメータでダンプさせ、その後別のプロセスプールで圧縮します。このリポジトリにはまさにこれのためのChromiumパッチファイルが出荷されており、圧縮ワーカーはopen(raw_path, "rb")でファイルを読み込みます。

リモートブラウザに対して、そのパラメータは受け入れられ、静かに無視されます。

python Copy
import asyncio, json, os, pathlib

import websockets

CDP = ("wss://browser.scrapeless.com/api/v2/browser"
       f"?token={os.environ['SCRAPELESS_KEY']}&sessionTTL=120&proxyCountry=ANY")
RAW = pathlib.Path("/dev/shm/tile_probe.raw")


async def call(ws, state, method, params=None, session=None):
    state["id"] += 1
    msg = {"id": state["id"], "method": method}
    if params:
        msg["params"] = params
    if session:
        msg["sessionId"] = session
    await ws.send(json.dumps(msg))
    while True:
        reply = json.loads(await ws.recv())
        if reply.get("id") == state["id"]:
            return reply


async def main():
    RAW.unlink(missing_ok=True)
    async with websockets.connect(CDP, max_size=100 * 1024 * 1024) as ws:
        state = {"id": 0}
        tid = (await call(ws, state, "Target.createTarget",
                          {"url": "about:blank"}))["result"]["targetId"]
        sid = (await call(ws, state, "Target.attachToTarget",
                          {"targetId": tid, "flatten": True}))["result"]["sessionId"]
        await call(ws, state, "Page.enable", session=sid)
        await call(ws, state, "Page.navigate",
                   {"url": "https://books.toscrape.com/"}, session=sid)
        await asyncio.sleep(2.5)

        shot = await call(ws, state, "Page.captureScreenshot",
                          {"fromSurface": True, "optimizeForSpeed": True,
                           "rawFilePath": str(RAW),
                           "clip": {"x": 0, "y": 0, "width": 875,
                                    "height": 1000, "scale": 1}}, session=sid)
        print("error:", shot.get("error"))
        print("inline data returned:", len(shot["result"].get("data", "")))
        print("local file exists:", RAW.exists())
        await call(ws, state, "Target.closeTarget", {"targetId": tid})


asyncio.run(main())
text Copy
error: None
inline data returned: 245576
local file exists: False

エラーなし、応答には実際の画像データがあり、ディスクには何もありません。パスが手がかりです:/dev/shmはブラウザを実行しているマシンを指し、それはあなたのコードを実行しているマシンではありません。期待されるファイルを開くワーカーは、すべてのタイルにFileNotFoundErrorを発生させ、その失敗が発生するのではなくカウントされる場合、実行は書き込まれたタイルを報告しながら出力ディレクトリは空のままです。

これは2つの発見の中で鋭いもので、品質の回帰ではありません。これは何も生産せず、機能したと述べるパイプラインです。

代わりに何をすべきか

どちらの発見もリモートレンダリングに反対の立場を取ってはいません。彼らはローカルの仮定を持ち込むことに反対しています。

設定するのではなく、ジオメトリを読み取ります。オーバーライド後のinnerWidthは、あなたのタイルの数学が使用するべき数値です。あなたが渡した数値ではありません。タイルがその幅でトレーニングされたモデルに対して固定幅である必要がある場合、ブラウザにあなたの数値でレイアウトさせるのではなく、キャプチャした画像を後からスケールまたはパッドしてください。

生のパスではなく、エンコードされた画像を取得します。rawFilePathを失うことはPNGまたはJPEGエンコーディング時間を消費し、ブラウザがリモートのときにのみ役立つ場所であるプロセスにバイトを戻します。

タイルジオメトリを主張すべきものとして扱います。要求された幅と測定された幅を比較する1つのチェックを、環境が変更されたときに1回実行することで、インデックスが構築される前にこれらの両方を捕捉します。どちらもエラーとして表示されず、モデルが見るものを変更します。

結論

スクリーンショットのタイルは、レンダラーが設定したビューポートを尊重することを前提としており、あなたが起動したブラウザ上ではその仮定は安全です。クラウドブラウザでは、オーバーライドは成功し、レイアウト幅は別の場所に落ちます — ここでは6つの異なるセッションで6つの異なる値として測定され、どれも要求された875ではありません — 一方、スクリーンショットクリップはピクセルに対して尊重されます。結果は、間違った領域の正しいサイズのタイルです。

生キャプチャパスは、よりクリーンで深刻に失敗します:受け入れられ、無視され、ファイルなし、エラーなし。ビジュアルインデックスを信頼する前にタイルの数をチェックし、仮定するのではなくジオメトリを読み取ってください。
プロトコルの詳細については、What Is the Chrome DevTools Protocolにあります。接続自体については、Playwright and Scraping Browser guideに記載されています。プランの詳細は、Scrapeless pricing pageにあり、セッションパラメータはScrapeless documentationにあります。

FAQ

Q: これはPixelRAGがクラウドブラウザを使用できないことを意味しますか?

まったくその通りではありません。現在、そのレンダラーはローカルブラウザを想定しています。タイルロジック、埋め込みモデル、インデックスは影響を受けません。変更が必要なのはキャプチャステップで、レイアウト幅を読み戻し、生のファイルパスではなくエンコードされた画像レスポンスを使用する必要があります。

Q: 私の値を適用しないのに、オーバーライドが幅を変更するのはなぜですか?

ブラウザ自身のウィンドウに対してリレイアウトがトリガーされるからです。呼び出し前の幅は、測定されたすべてのセッションで945でしたが、呼び出し後は異なる値になりましたので、呼び出しには影響があります。選択するのはあなたのものではない宛先です。

Q: clipパラメータは信頼できますか?

はい、それがミスマッチを見逃しやすくしている理由です。すべてのキャプチャは、要求したピクセル寸法で正確に戻ってきました。正しい出力サイズは、下にあるページがそのサイズでレイアウトされていたという証拠ではありません。

Q: Playwrightのようなドライバライブラリはこれを避けますか?

いいえ、ドライバは同じCDPコマンドを内部で発行するため、避けられません。このエンドポイントでの以前の測定では、Playwrightのビューポートセッターが報告された寸法を完全に変更せず、症状はクライアントによって若干異なるものの、原因は同じです。The WebDriver BiDi specificationは、リモートブラウザ制御に定義された契約を提供するための標準トラックの取り組みであり、こうした動作が最終的に特定される場所です。

Q: 自分のセットアップをすばやく確認するにはどうすればよいですか?

ビューポートを設定し、次にinnerWidthを評価して、要求した内容と比較してください。一行、一回の往復で、タイル数学が実数で操作されているか希望的なものであるかをすぐに教えてくれます。

Q: 固定幅のトリミングは実際に取得に悪影響を与えますか?

入力分布が変わります。特定の幅でレイアウトされたページのスクリーンショットに調整された埋め込みモデルは、右側にコンテンツが欠けたより広いレイアウトのトリミングを受け取ります。タイルはまだ埋め込まれ、隣接するものを返すため、何も壊れたとは言えませんが、結果として、役立つコンテンツがトリミングの外にあるページの結果は単に悪化します。

Q: タイリング実行のためのsessionTTLはどのくらいであるべきですか?

全ページをカバーできるほど十分長く、タイリングは連続的であり、高いページは1つのセッションで多くのキャプチャが必要です。平均値ではなく、期待される最も遅いページに対してサイズを設定し、実行が終了したらターゲットを閉じてください。

Scrapelessでは、適用される法律、規制、およびWebサイトのプライバシーポリシーを厳密に遵守しながら、公開されているデータのみにアクセスします。 このブログのコンテンツは、デモンストレーションのみを目的としており、違法または侵害の活動は含まれません。 このブログまたはサードパーティのリンクからの情報の使用に対するすべての責任を保証せず、放棄します。 スクレイピング活動に従事する前に、法律顧問に相談し、ターゲットウェブサイトの利用規約を確認するか、必要な許可を取得してください。

最も人気のある記事

カタログ