Nhà biên kịch + Trình duyệt thu thập dữ liệu không có scrapeless: Ghi lại dữ liệu WebSocket trực tiếp qua CDP
Expert Network Defense Engineer
Một bảng giá trực tiếp, bảng tỷ lệ cược của một nhà cái và một bảng điều khiển thời gian thực đều có một đặc điểm chung: con số trên màn hình chưa bao giờ đến từ việc làm mới trang. Trình duyệt chỉ mở một kết nối WebSocket duy nhất một lần, và máy chủ đã đẩy mọi cập nhật xuống cùng một socket đó dưới dạng khung kể từ đó — không có yêu cầu lặp lại, không có HTML mới để phân tích. Một công cụ chỉ đọc DOM hoặc lấy HTML thì không bao giờ thấy được lưu lượng đó, vì dữ liệu sống trong các khung, không phải trong mã.
Kết nối Playwright đến Scrapeless Scraping Browser qua wss://browser.scrapeless.com/api/v2/browser, và phiên CDP bên dưới sẽ cho bạn hai cách riêng biệt để đọc các khung khi chúng đến: API sự kiện WebSocket riêng của Playwright và sự kiện thô Network.webSocketFrameReceived từ chính Chrome DevTools Protocol. Hướng dẫn này kết nối đến trình duyệt đám mây đó, mở một WebSocket đến một luồng dữ liệu thị trường công cộng không cần xác thực, và thu thập các khung theo cả hai cách, với mỗi đường dẫn mã chạy chống lại điểm cuối trực tiếp.
Tại Sao Lưu Lượng WebSocket Cần Một Đường Dẫn Ghi Nhanh Khác
page.query_selector_all() và các lệnh gọi tương tự đọc bất cứ nội dung nào mà DOM chứa tại thời điểm bạn gọi chúng. Một widget được cấp nguồn từ socket biến đổi nút hiển thị của nó từng bước một khi các khung đến, vì vậy một lần đọc DOM duy nhất chỉ bắt được giá trị nào đó mà đã xảy ra cuối cùng. Đọc luồng thực tế — các khung, không phải các pixel được hiển thị — là cách duy nhất để thấy mọi cập nhật mà một bảng giá, một luồng tỷ lệ cược hoặc một bảng điều khiển trực tiếp thực sự nhận được.
Chrome DevTools Protocol trực tiếp hiển thị luồng đó. Miền Mạng phát một sự kiện cho mỗi khung mà WebSocket của một trang gửi hoặc nhận, độc lập với những gì mà trang đó làm với khung sau đó. Scrapeless Scraping Browser là một phiên Chromium đám mây chỉ có thể truy cập thông qua CDP — Puppeteer, Playwright và các khách hàng CDP khác kết nối với nó, nhưng không có điểm cuối Selenium/WebDriver nào để điều khiển nó thay vào đó. Playwright tình cờ hiển thị hai lớp của sự kiện CDP đó: một sự kiện websocket ở cấp độ trang với các callback khung riêng của nó, và, một cấp độ bên dưới, truy cập trực tiếp vào phiên CDP để bạn có thể đăng ký Network.webSocketFrameReceived cho chính mình.
Điều Kiện Tiên Quyết
Bạn cần Python 3.9 hoặc mới hơn, gói playwright, và một khóa API Scrapeless từ gói miễn phí tại app.scrapeless.com. Không yêu cầu nhị phân Chrome cục bộ — connect_over_cdp truy cập một trình duyệt đã tồn tại trong đám mây của Scrapeless, vì vậy playwright install chromium là tùy chọn ở đây.
Ví dụ dưới đây mở một WebSocket trực tiếp đến luồng dữ liệu thị trường công cộng của Binance, được tài liệu tại Tài liệu tham khảo luồng thị trường WebSocket của Binance. Nó không cần khóa API và không cần tài khoản — đây là dữ liệu thị trường công khai, không cần xác thực. Cả hai ví dụ đều đăng ký luồng bảng giá 24 giờ, cái mà đẩy một cập nhật mỗi giây, và kịch bản trong trang tự đóng socket ngay khi khung thứ năm đến, vì vậy không có kết nối nào giữ mở lâu hơn thời gian cần thiết để chứng minh rằng việc ghi nhận hoạt động.
Cài Đặt
bash
pip install playwright
bash
export SCRAPELESS_API_KEY="your_scrapeless_api_key"
Kết Nối Qua CDP
Sử dụng lại cùng một mẫu xây dựng URL mà bất kỳ kịch bản Playwright-to-Scraping-Browser nào cũng dùng: ba tham số truy vấn trên một điểm cuối WSS.
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 quan trọng ở đây vì một lý do cụ thể cho mục tiêu này: một kết nối được định tuyến qua một địa chỉ IP ra khỏi Hoa Kỳ sẽ thất bại trong việc bắt tay với luồng thị trường của Binance — socket mở và sau đó đóng ngay lập tức với mã đóng WebSocket 1006. Định tuyến qua Đức, hoặc hầu hết các khu vực không phải Hoa Kỳ khác, hoàn thành việc bắt tay một cách bình thường và các khung bắt đầu đến. Các ví dụ bên dưới đặt proxyCountry thành "DE" vì lý do đó; thay đổi nó cho khu vực mà mục tiêu của bạn thực sự phục vụ.
Ghi Nhận Các Khung Với Các Sự Kiện WebSocket Của Playwright
Playwright phát một sự kiện websocket trên đối tượng Page mỗi khi trang mở một socket, bất kể socket đó có xuất phát từ lệnh page.evaluate() của bạn hay từ chính JavaScript của trang mục tiêu. Đối tượng WebSocket mà nó cung cấp cho bạn phát ra framereceived cho mỗi khung đến và framesent cho mỗi khung ra.
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})
vi
trả về f"wss://browser.scrapeless.com/api/v2/browser?{params}"
khung = []
def xử_lý_websocket(ws):
in("websocket đã mở:", ws.url)
ws.on("framereceived", lambda payload: khung.append(payload))
với sync_playwright() như p:
trình duyệt = p.chromium.connect_over_cdp(scraping_browser_url())
trang = trình duyệt.new_page()
trang.on("websocket", xử_lý_websocket)
trang.goto("about:blank")
trang.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;
}}"""
)
trang.wait_for_function("window.__ws && window.__ws.readyState === 3", timeout=20000)
trang.wait_for_timeout(300)
trình duyệt.close()
in(f"đã ghi lại {len(khung)} khung thông qua page.on('websocket')")
in(json.dumps(json.loads(khung[0]), indent=2))
Năm bản cập nhật ticker thực tế sẽ trở lại, mỗi cái là một thông điệp JSON hoàn chỉnh:
text
websocket đã mở: wss://stream.binance.com:9443/ws/btcusdt@ticker
đã ghi lại 5 khung thông qua page.on('websocket')
{
"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
}
Bộ xử lý onmessage trong trang tự đếm khung và gọi ws.close() ngay khi khung thứ năm đến, vì vậy giới hạn được thi hành từ nguồn thay vì kiểm tra từ Python — một luồng đẩy cập nhật nhanh hơn một lần mỗi giây sẽ để nhiều khung thêm hạ cánh trước khi một kiểm tra bên phía Python bắt kịp. page.wait_for_function sau đó chặn cho đến khi readyState báo cáo đã đóng. about:blank cộng với page.evaluate đã đủ ở đây vì mục tiêu là socket, không phải đánh dấu của một trang; page.on("websocket") cũng kích hoạt theo cách tương tự khi một trang thực mở kết nối qua kịch bản của riêng nó.
Ghi lại khung thô từ miền mạng CDP
Sự kiện framereceived của Playwright đã giải mã khung cho bạn. Đi một lớp thấp hơn, thông qua phiên CDP trực tiếp, phơi bày sự kiện tương tự mà bảng điều khiển Mạng DevTools sử dụng — bao gồm cả opcode cho biết liệu một khung là văn bản hay nhị phân.
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_khung = []
def on_frame(event):
frame = event["response"]
payload = frame["payloadData"] if frame["opcode"] == 1 else base64.b64decode(frame["payloadData"])
cdp_khung.append((frame["opcode"], payload))
với sync_playwright() như p:
trình duyệt = p.chromium.connect_over_cdp(scraping_browser_url())
trang = trình duyệt.new_page()
cdp = trang.context.new_cdp_session(trang)
cdp.send("Network.enable")
cdp.on("Network.webSocketFrameReceived", on_frame)
trang.goto("about:blank")
trang.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;
}}"""
)
trang.wait_for_function("window.__ws && window.__ws.readyState === 3", timeout=20000)
trang.wait_for_timeout(300)
trình duyệt.close()
in(f"đã ghi lại {len(cdp_khung)} khung CDP thô")
opcode, payload = cdp_khung[0]
in(f"opcode={opcode}")
in(json.dumps(json.loads(payload), indent=2))
opcode xác nhận đây là các khung văn bản, và load là sơ đồ ticker giống hệt từ đường dẫn ghi lại khác:
text
đã ghi lại 5 khung CDP thô
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) mở một phiên mà giao diện phiên CDP cung cấp send() cho các lệnh giao thức và on() cho các sự kiện giao thức — Network.enable bật chế độ báo cáo khung, và mỗi sự kiện Network.webSocketFrameReceived tiếp theo được kích hoạt với cùng một requestId, timestamp, và hình dạng response mà tab Mạng của DevTools đọc. Opcode 1 đánh dấu một khung văn bản trong định dạng khung riêng của giao thức WebSocket, vì vậy nội dung đã là một chuỗi UTF-8; bất kỳ opcode nào khác đều cần bước base64.b64decode(), bởi vì giao thức truyền các khung nhị phân dưới dạng base64 để giữ cho toàn bộ sự kiện an toàn cho JSON.
Những gì Bạn Nhận Được
Cả hai con đường đều trả về cùng một nội dung ticker cho luồng này, vì cả hai đều đang đọc cùng một khung cơ sở — một qua sự trừu tượng của Playwright, một từ trực tiếp từ giao thức.
| Trường | Loại | Nghĩa |
|---|---|---|
e |
chuỗi | Loại sự kiện ("24hrTicker" cho luồng này) |
E |
số nguyên | Thời gian sự kiện, mili giây epoch |
s |
chuỗi | Biểu tượng giao dịch |
c |
chuỗi | Giá giao dịch cuối cùng |
o |
chuỗi | Giá cách đây 24 giờ |
h / l |
chuỗi | Cao / thấp 24 giờ |
v |
chuỗi | Khối lượng tài sản cơ bản trong 24 giờ |
b / a |
chuỗi | Giá hỏi / chào giá tốt nhất hiện tại |
n |
số nguyên | Số lượng giao dịch trong khoảng thời gian 24 giờ |
Một luồng giao dịch trên cùng một kết nối (btcusdt@trade thay vì btcusdt@ticker) trả về một thông điệp cho mỗi giao dịch đã thực hiện thay vì một tóm tắt liên tục — khối lượng nhỏ hơn, với p (giá), q (số lượng), và t (ID giao dịch) thay cho các trường ticker ở trên, đến thường xuyên hơn một lần mỗi giây trên một cặp hoạt động. Mã bắt giữ không thay đổi; chỉ có tên luồng trong URL WebSocket thay đổi.
Nhận khóa API của bạn trên gói miễn phí: app.scrapeless.com
Đọc Khung Từ Một Trang Thực Thay Vì Một Socket Trực Tiếp
Mỗi ví dụ ở trên mở WebSocket chính nó, từ một trang trống, vì điều đó giữ cho mục tiêu nhỏ và công khai. Một bảng tỷ lệ cược trực tiếp hoặc một bảng điều khiển trao đổi mở socket của riêng nó theo cách tương tự — từ JavaScript đóng gói của riêng nó, ngay khi trang tải — và page.on("websocket") kích hoạt giống nhau ở cả hai phía. Gắn cùng một trình xử lý trước khi gọi page.goto() trên mục tiêu thực, và các khung đến khi mã của trang nhận được chúng; không có gì thay đổi về logic bắt giữ vì socket thuộc về trang thay vì thuộc về cuộc gọi page.evaluate() của bạn. Những gì thay đổi là việc phát hiện — mở bảng Mạng của mục tiêu một lần, xác định URL socket và hình dạng khung bằng tay, sau đó chuyển đổi điều đó thành một trình xử lý framereceived thay vì đoán tên luồng trước.
Kết Luận
Một kết nối WebSocket mang dữ liệu mà một lần đọc DOM hoặc một lần lấy HTTP không bao giờ thấy, vì máy chủ liên tục đẩy các khung xuống một socket mở thay vì trả lời các yêu cầu riêng lẻ. Sự kiện page.on("websocket") của Playwright và sự kiện Network.webSocketFrameReceived thô đều đọc cùng một luồng — một qua một gói tiện lợi, một từ trực tiếp từ giao thức — và cả hai đều hoạt động giống nhau đối với một luồng dữ liệu thị trường công khai thực tế qua kết nối CDP của Trình duyệt Scrapeless. Gán proxyCountry cho một khu vực mà mục tiêu của bạn thực sự phục vụ, giới hạn số lượng khung bạn bắt trước khi đóng socket, và phần còn lại của mã là một vài lời gọi Playwright mà bạn đã biết. Đọc giới hạn phiên hiện tại và giới hạn xuất trên trang sản phẩm Trình duyệt Scrapeless và kiểm tra giới hạn kế hoạch trên trang giá. Đối với các cơ chế kết nối mà hướng dẫn này xây dựng dựa trên, giải thích Giao thức DevTools Chrome mô tả những gì CDP công khai ngoài miền Mạng.
Tham gia cộng đồng của chúng tôi để yêu cầu một gói miễn phí và so sánh ghi chú với các nhà phát triển khác đang xây dựng tự động hóa trình duyệt: Discord · Telegram.
Câu Hỏi Thường Gặp
Q: Tôi có cần Selenium hoặc WebDriver để bắt khung WebSocket theo cách này không?
Không. Trình duyệt Scrapeless chỉ có thể truy cập qua Giao thức DevTools Chrome, vì vậy bất kỳ khách hàng nào có thể giao tiếp CDP — Playwright ở đây, hoặc Puppeteer — đều có thể kết nối và đọc các sự kiện khung. Không có điểm cuối WebDriver, vì vậy Selenium không thể điều khiển kết nối này.
Q: Sự khác biệt giữa page.on("websocket") và sự kiện CDP thô Network.webSocketFrameReceived là gì?
Sự kiện websocket của Playwright và callback framereceived của nó đã giải mã một khung thành chuỗi, điều này bao gồm các luồng JSON dựa trên văn bản như trong hướng dẫn này. Sự kiện CDP bên dưới thêm trường opcode, để bạn có thể phân biệt một khung văn bản với một khung nhị phân trước khi quyết định cách giải mã nó.
H: Làm thế nào tôi có thể đọc các khung mà trang web gửi, không chỉ những khung mà nó nhận?
Đối tượng WebSocket của Playwright phát ra framesent cùng với framereceived, và phiên CDP phát ra sự kiện Network.webSocketFrameSent tương ứng với cùng một requestId, timestamp, và định dạng response như biến thể đã nhận.
H: Điều gì xảy ra với các khung WebSocket nhị phân?
Một khung nhị phân đến qua CDP với opcode được đặt thành một giá trị khác ngoài 1, và payloadData chứa các byte được mã hóa base64 thay vì một chuỗi UTF-8 — giải mã nó bằng base64.b64decode() trước khi phân tích bất kỳ định dạng nhị phân nào mà mục tiêu sử dụng. Sự kiện framereceived của Playwright tự động chuyển qua các khung nhị phân dưới dạng bytes thay vì str.
H: Tại sao kết nối thất bại với mã đóng 1006 khi proxyCountry là "US"?
Một mã đóng 1006 có nghĩa là kết nối kết thúc bất thường, mà không có khung đóng thích hợp — trong trường hợp này, quá trình bắt tay với luồng của Binance chưa bao giờ hoàn tất khi IP rời khỏi nằm ở Hoa Kỳ. Định tuyến cùng một yêu cầu qua một giá trị proxyCountry khác đã hoàn tất quá trình bắt tay và bắt đầu gửi các khung.
H: Tôi có thể bắt lưu lượng WebSocket từ một trang thay vì mở socket tự mình không?
Có. Đính kèm trình xử lý page.on("websocket"), hoặc kích hoạt miền CDP Network, trước khi gọi page.goto() trên mục tiêu thực, và cùng một sự kiện sẽ phát ra cho bất kỳ socket nào mà script của trang tự mở — không có gì thay đổi trong mã bắt cuộc.
H: WebSocket có thể mở lâu không?
Tham số truy vấn sessionTTL giới hạn phiên trình duyệt tính bằng giây. Một giá trị ngắn là đủ cho một lần bắt có giới hạn như trong hướng dẫn này; một giá trị dài hơn giữ phiên — và bất kỳ socket nào đang mở — sống lâu hơn cho một luồng đang chạy dài.
H: Chạy điều này chống lại bất kỳ điểm cuối WebSocket nào tôi tìm thấy có an toàn không?
Chỉ đối với các điểm cuối công cộng, không xác thực mà bạn được phép đọc, và chỉ ở mức độ mà tài liệu của điểm cuối đó cho phép. Ví dụ ở đây nhắm đến luồng dữ liệu thị trường công khai đã được tài liệu hóa của Binance, không cần khóa và đóng kết nối sau năm khung thay vì giữ nó mở vô thời hạn.
H: Có phải cấu trúc khung giữ nguyên giữa các loại luồng khác nhau không?
Không — luồng ticker trong hướng dẫn này trả về một tóm tắt 24 giờ lăn liên tục mỗi giây, trong khi một luồng giao dịch trên cùng một kết nối trả về một tin nhắn nhỏ hơn cho mỗi giao dịch được thực hiện, thường là nhiều lần trong một giây. Cơ chế bắt giữ là giống nhau dù theo cách nào; chỉ có hình dạng JSON bên trong mỗi khung thay đổi với luồng bạn đăng ký.
Tại Scrapless, chúng tôi chỉ truy cập dữ liệu có sẵn công khai trong khi tuân thủ nghiêm ngặt các luật, quy định và chính sách bảo mật trang web hiện hành. Nội dung trong blog này chỉ nhằm mục đích trình diễn và không liên quan đến bất kỳ hoạt động bất hợp pháp hoặc vi phạm nào. Chúng tôi không đảm bảo và từ chối mọi trách nhiệm đối với việc sử dụng thông tin từ blog này hoặc các liên kết của bên thứ ba. Trước khi tham gia vào bất kỳ hoạt động cạo nào, hãy tham khảo ý kiến cố vấn pháp lý của bạn và xem xét các điều khoản dịch vụ của trang web mục tiêu hoặc có được các quyền cần thiết.



