劇作家 + Scrapeless Scraping Browser: CDP経由でライブWebSocketデータをキャプチャする
Expert Network Defense Engineer
ライブ価格ティッカー、スポーツブックのオッズボード、リアルタイムダッシュボードは、すべて共通の特性を持っています:画面上の数字はページのリロードから来ることはありません。ブラウザは一度だけ単一のWebSocket接続を開き、それ以来サーバーは同じソケットを通じてフレームとしてすべての更新をプッシュしています — 繰り返しのリクエストも、新しいHTMLを解析することもありません。DOMを読み取るだけのツールやHTMLを取得するツールはそのトラフィックを視認することはなく、データはマークアップではなくフレームの中に存在しています。
PlaywrightをScrapeless Scraping Browserにwss://browser.scrapeless.com/api/v2/browser経由で接続すると、その下のCDPセッションは、フレームが到着するたびにそれを読むための2つの異なる方法を提供します:Playwright独自のWebSocketイベントAPI、そしてChrome DevTools Protocol自体の生のNetwork.webSocketFrameReceivedイベントです。このガイドでは、そのクラウドブラウザに接続し、公開の認証なしのマーケットデータストリームにWebSocketを開き、両方の方法でフレームをキャプチャし、すべてのコードパスがライブエンドポイントに対して実行されます。
なぜWebSocketトラフィックは異なるキャプチャパスが必要なのか
page.query_selector_all()や似た呼び出しは、呼び出した瞬間のDOMに含まれるものを読み取ります。ソケットから供給されるウィジェットは、フレームが到着するに従ってノードごとに自分自身の表示を変化させるため、単一のDOM読取は最後に到着した値だけを捉えます。ストリーム自体を読み取ること — フレーム、すなわちレンダリングされたピクセルではなく — が、ティッカー、オッズフィード、ライブダッシュボードが実際に受信したすべての更新を見る唯一の方法です。
Chrome DevTools Protocolは、そのストリームを直接公開しています。Networkドメインは、ページのWebSocketが送信または受信するフレームごとにイベントを発火させ、その後ページがそのフレームで何をするかとは独立しています。Scrapeless Scraping Browserは、CDP経由でのみアクセス可能なクラウドChromiumセッションです — Puppeteer、Playwright、その他のCDPクライアントがそれに接続していますが、代わりに操作するためのSelenium/WebDriverのエンドポイントは存在しません。Playwrightはその同じCDPイベントの2つのレイヤーを公開しています:自身のフレームコールバックを持つページレベルのwebsocketイベントと、一レベル下で、Network.webSocketFrameReceivedに自分自身でサブスクライブできるようCDPセッションへの直接アクセスです。
前提条件
Python 3.9以上、playwrightパッケージ、及びapp.scrapeless.comの無料プランからのScrapeless APIキーが必要です。ローカルのChromeバイナリは必要ありません — connect_over_cdpはScrapelessのクラウドにすでに存在するブラウザにアクセスするため、playwright install chromiumはここではオプションです。
以下の例は、Binanceの公開スポットマーケットデータストリームに直接WebSocketを開きます。このストリームはBinanceのWebSocketマーケットストリームのリファレンスに記載されています。APIキーやアカウントは必要なく、公開で認証なしのマーケットデータです。両方の例は24時間ティッカーストリームにサブスクライブし、これが1秒ごとに1つの更新を送信し、インページスクリプトは到着した5番目のフレームの直後にソケットを自動的に閉じるため、どちらの実行もキャプチャが機能することを証明する時間だけ接続を開いた状態にします。
インストール
bash
pip install playwright
bash
export SCRAPELESS_API_KEY="your_scrapeless_api_key"
CDP経由で接続
Playwright-to-Scraping-Browserスクリプトが使用する同じURLビルダーパターンを再利用します:1つのWSSエンドポイントに3つのクエリパラメータ。
python
import os
from urllib.parse import urlencode
API_KEY = os.environ["SCRAPELESS_API_KEY"]
def scraping_browser_url(proxy_country="DE", session_ttl=60):
params = urlencode({"token": API_KEY, "sessionTTL": session_ttl, "proxyCountry": proxy_country})
return f"wss://browser.scrapeless.com/api/v2/browser?{params}"
proxyCountryは、その目的地特有の理由で重要です:アメリカの出口IPを経由する接続はBinanceのスポットストリームに対してハンドシェイクに失敗します — ソケットは開いてすぐにWebSocketクローズコード1006で閉じます。ドイツやほかのアメリカ以外の地域を経由すると、ハンドシェイクは通常通り完了し、フレームが到着し始めます。以下の例ではその理由からproxyCountryを"DE"に固定しています;実際にあなたのターゲットが提供している地域に応じて変更してください。
PlaywrightのWebSocketイベントを使ったフレームのキャプチャ
Playwrightは、ページがソケットを開くとき(そのソケットがあなた自身のpage.evaluate()呼び出しから来るか、ターゲットサイトの独自のJavaScriptから来るかに関わらず)Pageオブジェクト上でwebsocketイベントを発火させます。渡されたWebSocketオブジェクトは、受信したすべてのフレームに対してframereceivedを発火し、すべての送信されたフレームに対してframesentを発火させます。
python
import json
import os
from urllib.parse import urlencode
from playwright.sync_api import sync_playwright
API_KEY = os.environ["SCRAPELESS_API_KEY"]
FRAME_LIMIT = 5
def scraping_browser_url(proxy_country="DE", session_ttl=60):
params = urlencode({"token": API_KEY, "sessionTTL": session_ttl, "proxyCountry": proxy_country})
text
return f"wss://browser.scrapeless.com/api/v2/browser?{params}"
frames = []
def handle_websocket(ws):
print("websocket opened:", ws.url)
ws.on("framereceived", lambda payload: frames.append(payload))
with sync_playwright() as p:
browser = p.chromium.connect_over_cdp(scraping_browser_url())
page = browser.new_page()
page.on("websocket", handle_websocket)
page.goto("about:blank")
page.evaluate(
f"""() => {{
window.__frameCount = 0;
const ws = new WebSocket("wss://stream.binance.com:9443/ws/btcusdt@ticker");
ws.onmessage = () => {{
window.__frameCount += 1;
if (window.__frameCount >= {FRAME_LIMIT}) {{ ws.close(); }}
}};
window.__ws = ws;
}}"""
)
page.wait_for_function("window.__ws && window.__ws.readyState === 3", timeout=20000)
page.wait_for_timeout(300)
browser.close()
print(f"キャプチャしたフレームの数: {len(frames)} via page.on('websocket')")
print(json.dumps(json.loads(frames[0]), indent=2))
ウェブソケットが開かれました: wss://stream.binance.com:9443/ws/btcusdt@ticker
ページ.on('websocket')を介してキャプチャしたフレームの数: 5
{
"e": "24hrTicker",
"E": 1785154788015,
"s": "BTCUSDT",
"p": "516.30000000",
"P": "0.800",
"w": "65153.97639938",
"x": "64558.24000000",
"c": "65074.53000000",
"Q": "0.00022000",
"b": "65074.53000000",
"B": "0.00376000",
"a": "65074.54000000",
"A": "6.01063000",
"o": "64558.23000000",
"h": "65744.60000000",
"l": "64414.00000000",
"v": "12430.45583000",
"q": "809893625.78133730",
"O": 1785068388012,
"C": 1785154788012,
"F": 6534229591,
"L": 6536075962,
"n": 1846372
}
インページの onmessage ハンドラーはフレームを自分で数え、5つ目が到着するとすぐに ws.close() を呼び出します。したがって、制限はソースで強制され、Pythonからのポーリングによってではありません。もしストリームが秒間1回以上の速さで更新をプッシュする場合、Python側のチェックが追いつく前にいくつかの追加フレームが利用可能になるかもしれません。page.wait_for_function は、readyState がクローズドを報告するまでブロックされます。about:blank と page.evaluate はここでは十分です。ターゲットはソケットであり、ページのマークアップではありません。page.on("websocket") は、実際のページがスクリプトを通じて接続を開くときにも同様に発火します。
CDPネットワークドメインから生のフレームをキャプチャする
Playwrightの framereceived イベントは、すでにフレームをデコードしてくれます。より下のレイヤー、CDPセッションを通じて直接アクセスすることで、DevToolsネットワークパネルが使用するのと同じイベントが公開されます。これには、フレームがテキストかバイナリかを示す opcode が含まれています。
python
import base64
import json
import os
from urllib.parse import urlencode
from playwright.sync_api import sync_playwright
API_KEY = os.environ["SCRAPELESS_API_KEY"]
FRAME_LIMIT = 5
def scraping_browser_url(proxy_country="DE", session_ttl=60):
params = urlencode({"token": API_KEY, "sessionTTL": session_ttl, "proxyCountry": proxy_country})
return f"wss://browser.scrapeless.com/api/v2/browser?{params}"
cdp_frames = []
def on_frame(event):
frame = event["response"]
payload = frame["payloadData"] if frame["opcode"] == 1 else base64.b64decode(frame["payloadData"])
cdp_frames.append((frame["opcode"], payload))
with sync_playwright() as p:
browser = p.chromium.connect_over_cdp(scraping_browser_url())
page = browser.new_page()
cdp = page.context.new_cdp_session(page)
cdp.send("Network.enable")
cdp.on("Network.webSocketFrameReceived", on_frame)
page.goto("about:blank")
page.evaluate(
f"""() => {{
window.__frameCount = 0;
const ws = new WebSocket("wss://stream.binance.com:9443/ws/btcusdt@ticker");
ws.onmessage = () => {{
window.__frameCount += 1;
if (window.__frameCount >= {FRAME_LIMIT}) {{ ws.close(); }}
}};
window.__ws = ws;
}}"""
)
page.wait_for_function("window.__ws && window.__ws.readyState === 3", timeout=20000)
page.wait_for_timeout(300)
browser.close()
print(f"キャプチャした生CDPフレームの数: {len(cdp_frames)}")
opcode, payload = cdp_frames[0]
print(f"opcode={opcode}")
print(json.dumps(json.loads(payload), indent=2))
opcodeはこれがテキストフレームであることを確認し、ペイロードは他のキャプチャパスからの同一のティッカー構造を含んでいます:
text
キャプチャした生CDPフレームの数: 5
opcode=1
{
"e": "24hrTicker",
"E": 1785154852015,
"s": "BTCUSDT",
"p": "525.37000000",
"P": "0.814",
"w": "65154.69931666",
"x": "64529.61000000",
"c": "65054.99000000",
"Q": "0.00430000",
"b": "65054.99000000",
"B": "4.56302000",
"a": "65055.00000000",
"A": "1.44666000",
"o": "64529.62000000",
"h": "65744.60000000",
"l": "64414.00000000",
"v": "12421.62669000",
"q": "809327352.01071850",
"O": 1785068452012,
"C": 1785154852012,
"F": 6534232202,
"L": 6536078668,
"n": 1846467
}
page.context.new_cdp_session(page)は、プロトコルコマンド用のsend()と、プロトコルイベント用のon()を公開するセッションを開きます。Network.enableはフレームレポートをオンにし、その後に受信するすべてのNetwork.webSocketFrameReceivedイベントは、同じrequestId、timestamp、およびDevTools Networkタブが読み込むresponseの形状を持ちます。オペコード1は、WebSocketプロトコル</a}におけるテキストフレームを示すため、ペイロードはすでにUTF-8文字列です。その他のオペコードは、プロトコルがバイナリフレームをbase64として運ぶため、base64.b64decode()ステップが必要です。これは、すべてのイベントがJSON安全な形で保持されるためです。
返されるもの
両方のパスは、このストリームに対して同じのティッカーペイロードを返します。なぜなら、両方とも同じ基盤となるフレームを読み込んでいるからです。1つはPlaywrightの抽象化を通じて、もう1つはプロトコルから直接です。
| フィールド | タイプ | 意味 |
|---|---|---|
e |
string | イベントタイプ(このストリームの場合は"24hrTicker") |
E |
integer | イベント時間、エポックミリ秒 |
s |
string | 取引シンボル |
c |
string | 最終取引価格 |
o |
string | 24時間前の価格 |
h / l |
string | 24時間の高値 / 安値 |
v |
string | 24時間のベース資産ボリューム |
b / a |
string | 現在の最高入札 / 売り価格 |
n |
integer | 24時間ウィンドウ内の取引回数 |
同じ接続でのトレードストリーム(btcusdt@tradeの代わりにbtcusdt@ticker)は、ローリングサマリーの代わりに実行された各取引ごとに1つのメッセージを返します。これは、より小さいペイロードを持ち、p(価格)、q(数量)、およびt(取引ID)が上記のティッカーフィールドの代わりに配置されるため、アクティブなペアでは1秒ごと以上に頻繁に到着します。キャプチャコードは変更されません。変更されるのはストリーム名だけで、WebSocket URLの中でです。
無料プランでAPIキーを取得してください:app.scrapeless.com
実際のページからのフレームの読み取り
上記のすべての例は、空白のページからWebSocket自体を開きます。これにより、目標が小さく、公開されるままです。ライブオッズボードや取引所のダッシュボードも、自身のバンドルされたJavaScriptから、ページがロードされるとすぐに、同様の方法で独自のソケットを開きます。そして、page.on("websocket")はどちらの方法でも同じように発火します。実際のターゲットでpage.goto()を呼び出す前に同じハンドラーを追加し、フレームがページの独自のスクリプトが受信するようにすれば、キャプチャロジックは変更されません。変更されるのは発見です。一度ターゲット自身のNetworkパネルを開き、手動でソケットURLとフレーム形状を見つけ、それをframereceivedハンドラーに変換します。
結論
WebSocket接続は、DOM読み取りやHTTPフェッチが決して見ることのできないデータを運びます。なぜなら、サーバーは異なるリクエストに応じるのではなく、1つのオープンソケットを通じてフレームをプッシュし続けるからです。Playwrightのpage.on("websocket")イベントと生のCDPNetwork.webSocketFrameReceivedイベントは、その同じストリームを読み取ります。一方は便利なラッパーを通じて、もう一方はプロトコルから直接です。また、両方ともScrapeless Scraping BrowserのCDP接続を介して実際の公開市場データストリームに対して同様に動作しました。proxyCountryをターゲットが実際に提供している地域に固定し、ソケットを閉じる前にキャプチャするフレームの数を制限し、残りのスクリプトはすでに知っているPlaywright呼び出しの少数です。Scraping Browser製品ページで現在のセッションと出口制限を確認し、料金ページでプランの制限を確認してください。このガイドが構築している接続メカニズムについては、Chrome DevToolsプロトコルの説明が、Networkドメインを超えてCDPが公開する内容を説明しています。
私たちのコミュニティに参加して、無料プランを請求し、ブラウザ自動化を構築している他の開発者とメモを比較してください:Discord · Telegram。
FAQ
Q: この方法でWebSocketフレームをキャプチャするためにSeleniumやWebDriverは必要ですか?
いいえ。Scrapeless Scraping BrowserはChrome DevTools Protocolを介してのみアクセス可能であるため、CDPに対応する任意のクライアント—ここではPlaywrightまたはPuppeteer—が接続し、フレームイベントを読み取ることができます。WebDriverエンドポイントはないため、Seleniumはこの接続を制御することはできません。
Q: page.on("websocket")と生のCDP Network.webSocketFrameReceivedイベントの違いは何ですか?
Playwrightのwebsocketイベントとそのframereceivedコールバックは、すでにフレームを文字列としてデコードしており、このガイドのようなテキストベースのJSONストリームをカバーしています。その下にあるCDPイベントはopcodeフィールドを追加するので、デコード方法を決定する前に、テキストフレームがバイナリフレームかを識別できます。
Q: ページが送信するフレームをどうやって読み取るのですか?受信するフレームだけでなく。
PlaywrightのWebSocketオブジェクトは、framereceivedとともにframesentを発火させ、CDPセッションは受信バリアントと同じrequestId、timestamp、およびresponseの形状を持つ対応するNetwork.webSocketFrameSentイベントを発火させます。
Q: バイナリWebSocketフレームでは何が起こりますか?
バイナリフレームは、opcodeが1以外に設定されてCDPを通じて到着し、payloadDataにはUTF-8文字列ではなくbase64でエンコードされたバイトが含まれています—ターゲットが使用するバイナリフォーマットを解析する前にbase64.b64decode()でデコードしてください。Playwright独自のframereceivedイベントは、バイナリフレームをstrではなくbytesとして通過させます。
Q: 退出IPがアメリカのときに、なぜ接続が閉じるコード1006で失敗したのですか?
閉じるコード1006は、接続が異常に終了したことを意味し、適切なクローズフレームがありませんでした—この場合、出口IPがアメリカにあったときにBinanceのストリームへのハンドシェイクが完了しませんでした。同じリクエストを異なるproxyCountryの値を通してルーティングすると、ハンドシェイクが完了し、フレームの配信を開始しました。
Q: 自分でソケットを開くのではなく、ページからWebSocketトラフィックをキャプチャできますか?
はい。実際のターゲットに対してpage.goto()を呼び出す前に、page.on("websocket")ハンドラをアタッチするか、CDPのNetworkドメインを有効にすれば、ページのスクリプトが開くすべてのソケットに対して同じイベントが発火します—キャプチャコードに関する事柄は変更されません。
Q: WebSocketはどれくらいの間オープンのままでいられますか?
sessionTTLクエリパラメータは、ブラウザセッションを秒単位で制限します。短い値がこのガイドのような制約されたキャプチャには十分であり、長い値は、セッションおよびオープンしたソケットを持続させ、より長く実行されるストリームを保持します。
Q: 見つけたどんなWebSocketエンドポイントに対してもこれを実行して安全ですか?
公に利用でき、認証されていないエンドポイントに対してのみ、またそのエンドポイントのドキュメントが許可するボリュームでのみ可能です。こちらの例は、Binanceの文書化された公のマーケットデータストリームをターゲットにしており、キーは不要で、無限にオープンにするのではなく、5つのフレームで接続を閉じます。
Q: フレームスキーマは異なるストリームタイプ間で同じままですか?
いいえ—このガイドのティッカーストリームは毎秒1回の間隔で24時間の概要を返すのに対し、同じ接続のトレードストリームは実行されたトレードごとに1つの小さなメッセージを返し、多くの場合毎秒数回返します。キャプチャメカニズムはどちらでも同じですが、サブスクライブするストリームによって各フレーム内のJSONの形は変わります。
Scrapelessでは、適用される法律、規制、およびWebサイトのプライバシーポリシーを厳密に遵守しながら、公開されているデータのみにアクセスします。 このブログのコンテンツは、デモンストレーションのみを目的としており、違法または侵害の活動は含まれません。 このブログまたはサードパーティのリンクからの情報の使用に対するすべての責任を保証せず、放棄します。 スクレイピング活動に従事する前に、法律顧問に相談し、ターゲットウェブサイトの利用規約を確認するか、必要な許可を取得してください。



