scrapfly-browser
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseScrapfly Cloud Browser
Scrapfly Cloud Browser
Use the Scrapfly Cloud Browser API with Python Playwright to automate remote cloud browsers with built-in proxy rotation, anti-bot fingerprinting, and geo-targeting.
使用 Scrapfly Cloud Browser API 与 Python Playwright 自动化远程云端浏览器,提供代理轮换、浏览器指纹管理和地理定向功能,以应对反机器人防护。
When to use
适用场景
- Automating browser interactions on remote cloud browsers (login flows, form filling, navigation)
- Scraping JavaScript-heavy sites that require full browser automation
- Bypassing anti-bot protections with managed browser fingerprinting
- Running Playwright scripts through geo-targeted proxies
- Maintaining persistent browser sessions across multiple connections
- Downloading files through browser interactions
- 在远程云端浏览器上执行交互自动化(登录流程、表单填写、页面导航)
- 抓取需要完整浏览器自动化的 JavaScript 密集型站点
- 借助托管的浏览器指纹模拟绕过反机器人防护
- 通过地理定向代理运行 Playwright 脚本
- 在多次连接之间维持持久浏览器会话
- 通过浏览器交互下载文件
Setup
初始化
bash
pip install playwrightThe API key must be provided via environment variable or passed directly in the connection URL.
SCRAPFLY_API_KEYbash
pip install playwrightAPI 密钥必须通过环境变量 提供,或直接传入连接 URL。
SCRAPFLY_API_KEYAPI Reference
API 参考
Connection: WebSocket CDP (Chrome DevTools Protocol)
wss://browser.scrapfly.io?api_key=YOUR_API_KEY连接: WebSocket CDP(Chrome DevTools Protocol)
wss://browser.scrapfly.io?api_key=YOUR_API_KEYConnection Parameters
连接参数
| Parameter | Type | Default | Description |
|---|---|---|---|
| str | required | Scrapfly API key |
| str | | Proxy type: |
| str | random | OS fingerprint: |
| str | None | Proxy country (ISO 3166-1 alpha-2, e.g. |
| str | None | Session ID for browser state persistence |
| str | | Close session on disconnect. Set |
| int | 900 | Max session duration in seconds (900-1800) |
| str | | Enable session video recording |
| str | None | |
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| str | 必填 | Scrapfly API 密钥 |
| str | | 代理类型: |
| str | 随机 | 操作系统指纹: |
| str | None | 代理所在国家/地区(ISO 3166-1 alpha-2,例如 |
| str | None | 用于持久保存浏览器状态的会话 ID |
| str | | 断开连接时关闭会话。设置为 |
| int | 900 | 会话最长持续时间(秒)(900-1800) |
| str | | 启用会话视频录制 |
| str | None | |
Building the Connection URL
构建连接 URL
python
import os
from urllib.parse import urlencode
params = {
"api_key": os.environ["SCRAPFLY_API_KEY"],
"proxy_pool": "datacenter",
"os": "linux",
}
BROWSER_WS = f"wss://browser.scrapfly.io?{urlencode(params)}"python
import os
from urllib.parse import urlencode
params = {
"api_key": os.environ["SCRAPFLY_API_KEY"],
"proxy_pool": "datacenter",
"os": "linux",
}
BROWSER_WS = f"wss://browser.scrapfly.io?{urlencode(params)}"Examples
示例
Basic page navigation
基础页面导航
python
from playwright.sync_api import sync_playwright
import os
API_KEY = os.environ["SCRAPFLY_API_KEY"]
BROWSER_WS = f"wss://browser.scrapfly.io?api_key={API_KEY}&proxy_pool=datacenter&os=linux"
with sync_playwright() as p:
browser = p.chromium.connect_over_cdp(BROWSER_WS)
try:
context = browser.contexts[0]
page = context.pages[0] if context.pages else context.new_page()
page.goto("https://web-scraping.dev")
print("Title:", page.title())
print("Content:", page.content()[:500])
finally:
browser.close()python
from playwright.sync_api import sync_playwright
import os
API_KEY = os.environ["SCRAPFLY_API_KEY"]
BROWSER_WS = f"wss://browser.scrapfly.io?api_key={API_KEY}&proxy_pool=datacenter&os=linux"
with sync_playwright() as p:
browser = p.chromium.connect_over_cdp(BROWSER_WS)
try:
context = browser.contexts[0]
page = context.pages[0] if context.pages else context.new_page()
page.goto("https://web-scraping.dev")
print("Title:", page.title())
print("Content:", page.content()[:500])
finally:
browser.close()Async Playwright
异步 Playwright
python
from playwright.async_api import async_playwright
import asyncio
import os
API_KEY = os.environ["SCRAPFLY_API_KEY"]
BROWSER_WS = f"wss://browser.scrapfly.io?api_key={API_KEY}&proxy_pool=datacenter&os=linux"
async def main():
async with async_playwright() as p:
browser = await p.chromium.connect_over_cdp(BROWSER_WS)
try:
context = browser.contexts[0]
page = context.pages[0] if context.pages else await context.new_page()
await page.goto("https://web-scraping.dev")
print("Title:", await page.title())
finally:
await browser.close()
asyncio.run(main())python
from playwright.async_api import async_playwright
import asyncio
import os
API_KEY = os.environ["SCRAPFLY_API_KEY"]
BROWSER_WS = f"wss://browser.scrapfly.io?api_key={API_KEY}&proxy_pool=datacenter&os=linux"
async def main():
async with async_playwright() as p:
browser = await p.chromium.connect_over_cdp(BROWSER_WS)
try:
context = browser.contexts[0]
page = context.pages[0] if context.pages else await context.new_page()
await page.goto("https://web-scraping.dev")
print("Title:", await page.title())
finally:
await browser.close()
asyncio.run(main())Login flow with form filling
登录流程与表单填写
python
with sync_playwright() as p:
browser = p.chromium.connect_over_cdp(BROWSER_WS)
try:
context = browser.contexts[0]
page = context.pages[0] if context.pages else context.new_page()
page.goto("https://web-scraping.dev/login")
page.fill("input[name=username]", "user123")
page.fill("input[name=password]", "password")
page.click("button[type=submit]")
page.wait_for_selector("div#secret-message")
print("Logged in:", page.url)
finally:
browser.close()python
with sync_playwright() as p:
browser = p.chromium.connect_over_cdp(BROWSER_WS)
try:
context = browser.contexts[0]
page = context.pages[0] if context.pages else context.new_page()
page.goto("https://web-scraping.dev/login")
page.fill("input[name=username]", "user123")
page.fill("input[name=password]", "password")
page.click("button[type=submit]")
page.wait_for_selector("div#secret-message")
print("Logged in:", page.url)
finally:
browser.close()Take a screenshot
截取屏幕截图
python
with sync_playwright() as p:
browser = p.chromium.connect_over_cdp(BROWSER_WS)
try:
context = browser.contexts[0]
page = context.pages[0] if context.pages else context.new_page()
page.goto("https://web-scraping.dev")
page.screenshot(path="screenshot.png", full_page=True)
finally:
browser.close()python
with sync_playwright() as p:
browser = p.chromium.connect_over_cdp(BROWSER_WS)
try:
context = browser.contexts[0]
page = context.pages[0] if context.pages else context.new_page()
page.goto("https://web-scraping.dev")
page.screenshot(path="screenshot.png", full_page=True)
finally:
browser.close()Scrape data with selectors
使用选择器抓取数据
python
with sync_playwright() as p:
browser = p.chromium.connect_over_cdp(BROWSER_WS)
try:
context = browser.contexts[0]
page = context.pages[0] if context.pages else context.new_page()
page.goto("https://web-scraping.dev/products")
page.wait_for_selector("div.product")
products = page.query_selector_all("div.product")
for product in products:
name = product.query_selector("a").inner_text()
price = product.query_selector(".price").inner_text()
print(f"{name}: {price}")
finally:
browser.close()python
with sync_playwright() as p:
browser = p.chromium.connect_over_cdp(BROWSER_WS)
try:
context = browser.contexts[0]
page = context.pages[0] if context.pages else context.new_page()
page.goto("https://web-scraping.dev/products")
page.wait_for_selector("div.product")
products = page.query_selector_all("div.product")
for product in products:
name = product.query_selector("a").inner_text()
price = product.query_selector(".price").inner_text()
print(f"{name}: {price}")
finally:
browser.close()Geo-targeted browsing with residential proxy
使用住宅代理进行地理定向浏览
python
BROWSER_WS = (
f"wss://browser.scrapfly.io?"
f"api_key={API_KEY}"
f"&proxy_pool=residential"
f"&country=de"
f"&os=windows"
)
with sync_playwright() as p:
browser = p.chromium.connect_over_cdp(BROWSER_WS)
try:
context = browser.contexts[0]
page = context.pages[0] if context.pages else context.new_page()
page.goto("https://web-scraping.dev")
print("Page content from DE proxy:", page.title())
finally:
browser.close()python
BROWSER_WS = (
f"wss://browser.scrapfly.io?"
f"api_key={API_KEY}"
f"&proxy_pool=residential"
f"&country=de"
f"&os=windows"
)
with sync_playwright() as p:
browser = p.chromium.connect_over_cdp(BROWSER_WS)
try:
context = browser.contexts[0]
page = context.pages[0] if context.pages else context.new_page()
page.goto("https://web-scraping.dev")
print("Page content from DE proxy:", page.title())
finally:
browser.close()Persistent sessions (multi-step workflows)
持久会话(多步骤工作流)
Uses the same login credentials and selectors as the Login flow with form filling example.
python
LOGIN_URL = "https://web-scraping.dev/login"
USERNAME = "user123"
PASSWORD = "password"使用与登录流程与表单填写示例相同的登录凭据和选择器。
python
LOGIN_URL = "https://web-scraping.dev/login"
USERNAME = "user123"
PASSWORD = "password"Step 1: Login and keep session alive
Step 1: Login and keep session alive
BROWSER_WS = (
f"wss://browser.scrapfly.io?"
f"api_key={API_KEY}"
f"&session=my-session"
f"&auto_close=false"
f"&proxy_pool=datacenter"
)
with sync_playwright() as p:
browser = p.chromium.connect_over_cdp(BROWSER_WS)
try:
context = browser.contexts[0]
page = context.pages[0] if context.pages else context.new_page()
page.goto(LOGIN_URL)
page.fill("input[name=username]", USERNAME)
page.fill("input[name=password]", PASSWORD)
page.click("button[type=submit]")
page.wait_for_selector("div#secret-message")
print("Login complete, session preserved")
finally:
browser.close() # Disconnects but session stays aliveBROWSER_WS = (
f"wss://browser.scrapfly.io?"
f"api_key={API_KEY}"
f"&session=my-session"
f"&auto_close=false"
f"&proxy_pool=datacenter"
)
with sync_playwright() as p:
browser = p.chromium.connect_over_cdp(BROWSER_WS)
try:
context = browser.contexts[0]
page = context.pages[0] if context.pages else context.new_page()
page.goto(LOGIN_URL)
page.fill("input[name=username]", USERNAME)
page.fill("input[name=password]", PASSWORD)
page.click("button[type=submit]")
page.wait_for_selector("div#secret-message")
print("Login complete, session preserved")
finally:
browser.close() # Disconnects but session stays aliveStep 2: Reconnect to same session (still authenticated with same credentials)
Step 2: Reconnect to same session (still authenticated with same credentials)
BROWSER_WS = (
f"wss://browser.scrapfly.io?"
f"api_key={API_KEY}"
f"&session=my-session"
f"&proxy_pool=datacenter"
)
with sync_playwright() as p:
browser = p.chromium.connect_over_cdp(BROWSER_WS)
try:
context = browser.contexts[0]
page = context.pages[0] if context.pages else context.new_page()
page.goto(LOGIN_URL)
page.wait_for_selector("div#secret-message")
print("Accessed protected page (still logged in):", page.title())
finally:
browser.close()undefinedBROWSER_WS = (
f"wss://browser.scrapfly.io?"
f"api_key={API_KEY}"
f"&session=my-session"
f"&proxy_pool=datacenter"
)
with sync_playwright() as p:
browser = p.chromium.connect_over_cdp(BROWSER_WS)
try:
context = browser.contexts[0]
page = context.pages[0] if context.pages else context.new_page()
page.goto(LOGIN_URL)
page.wait_for_selector("div#secret-message")
print("Accessed protected page (still logged in):", page.title())
finally:
browser.close()undefinedInfinite scroll scraping
无限滚动抓取
python
with sync_playwright() as p:
browser = p.chromium.connect_over_cdp(BROWSER_WS)
try:
context = browser.contexts[0]
page = context.pages[0] if context.pages else context.new_page()
page.goto("https://web-scraping.dev/testimonials")
page.wait_for_selector("span.rating")
all_items = []
for _ in range(10): # Scroll 10 times
items = page.query_selector_all("span.rating")
all_items = items
page.evaluate("window.scrollTo(0, document.body.scrollHeight)")
page.wait_for_timeout(2000)
print(f"Collected {len(all_items)} reviews")
finally:
browser.close()python
with sync_playwright() as p:
browser = p.chromium.connect_over_cdp(BROWSER_WS)
try:
context = browser.contexts[0]
page = context.pages[0] if context.pages else context.new_page()
page.goto("https://web-scraping.dev/testimonials")
page.wait_for_selector("span.rating")
all_items = []
for _ in range(10): # Scroll 10 times
items = page.query_selector_all("span.rating")
all_items = items
page.evaluate("window.scrollTo(0, document.body.scrollHeight)")
page.wait_for_timeout(2000)
print(f"Collected {len(all_items)} reviews")
finally:
browser.close()Execute JavaScript in page
在页面中执行 JavaScript
python
with sync_playwright() as p:
browser = p.chromium.connect_over_cdp(BROWSER_WS)
try:
context = browser.contexts[0]
page = context.pages[0] if context.pages else context.new_page()
page.goto("https://web-scraping.dev")
# Extract data using JavaScript
data = page.evaluate("""
() => {
return {
title: document.title,
links: Array.from(document.querySelectorAll('a')).map(a => ({
text: a.innerText,
href: a.href,
})),
};
}
""")
print(f"Found {len(data['links'])} links")
finally:
browser.close()python
with sync_playwright() as p:
browser = p.chromium.connect_over_cdp(BROWSER_WS)
try:
context = browser.contexts[0]
page = context.pages[0] if context.pages else context.new_page()
page.goto("https://web-scraping.dev")
# Extract data using JavaScript
data = page.evaluate("""
() => {
return {
title: document.title,
links: Array.from(document.querySelectorAll('a')).map(a => ({
text: a.innerText,
href: a.href,
})),
};
}
""")
print(f"Found {len(data['links'])} links")
finally:
browser.close()Multi-page navigation
多页面导航
python
with sync_playwright() as p:
browser = p.chromium.connect_over_cdp(BROWSER_WS)
try:
context = browser.contexts[0]
page = context.pages[0] if context.pages else context.new_page()
urls = [
"https://web-scraping.dev/products",
"https://web-scraping.dev/products?page=2",
"https://web-scraping.dev/products?page=3",
]
results = []
for url in urls:
page.goto(url)
results.append({"url": url, "content": content})
print(f"Scraped {len(results)} pages")
finally:
browser.close()python
with sync_playwright() as p:
browser = p.chromium.connect_over_cdp(BROWSER_WS)
try:
context = browser.contexts[0]
page = context.pages[0] if context.pages else context.new_page()
urls = [
"https://web-scraping.dev/products",
"https://web-scraping.dev/products?page=2",
"https://web-scraping.dev/products?page=3",
]
results = []
for url in urls:
page.goto(url)
results.append({"url": url, "content": content})
print(f"Scraped {len(results)} pages")
finally:
browser.close()File Downloads
文件下载
A download triggered in the page (a click behind a login, an anti-bot flow) is
captured by the session and read back over CDP with the
commands. They are browser-level: send them on a CDP session.
ScrapiumBrowser.*| Command | Params | Returns |
|---|---|---|
| none | |
| none | |
| | |
| | |
| | every file's content, carried twice: |
页面中触发的下载(例如登录后的点击、反机器人验证流程)会被会话捕获,并通过 CDP 使用 命令读回。这些命令属于浏览器级别:应在 CDP session 上发送。
ScrapiumBrowser.*| 命令 | 参数 | 返回值 |
|---|---|---|
| 无 | |
| 无 | |
| | 单个文件的 |
| | |
| | 所有文件的内容,内容会传输两次:分别位于 |
Wait before you read
读取下载内容前请等待
Nothing queues the read behind the download. Listen for
with , or poll until the file is
there and its size has stopped moving. Calling straight after the
click returns and your lookup raises .
Browser.downloadProgressstate == "completed"getDownloadsMetadatasgetDownloads{}KeyErrorpython
import base64
import time
def wait_for_download(cdp, filename, timeout=60):
"""Poll until the file exists and its size is stable across two reads."""
deadline = time.monotonic() + timeout
last = -1
while time.monotonic() < deadline:
size = cdp.send("ScrapiumBrowser.getDownloadsMetadatas")["metadata"].get(filename)
if size is not None and size == last:
return size
last = size if size is not None else -1
time.sleep(0.5)
raise TimeoutError(f"{filename} did not finish downloading")
with sync_playwright() as p:
browser = p.chromium.connect_over_cdp(BROWSER_WS)
try:
context = browser.contexts[0]
page = context.pages[0] if context.pages else context.new_page()
cdp = context.new_cdp_session(page)
page.goto("https://web-scraping.dev/file-download")
page.click("#download-btn")
size = wait_for_download(cdp, "download-sample.pdf")
# One file by name: content is carried once, so the reply is ~1.34x the file.
result = cdp.send("ScrapiumBrowser.getDownload", {"filename": "download-sample.pdf"})
open("download-sample.pdf", "wb").write(base64.b64decode(result["data"]))
finally:
browser.close()Downloaded files are cleaned up when the session ends. Pass to
/ to reclaim disk during a long-running session.
delete: TruegetDownloadgetDownloads读取操作不会自动排在下载任务之后。请监听 且其 ,或者轮询 ,直到文件出现且大小不再变化。点击后立即调用 会返回 ,随后执行查找将引发 。
Browser.downloadProgressstate == "completed"getDownloadsMetadatasgetDownloads{}KeyErrorpython
import base64
import time
def wait_for_download(cdp, filename, timeout=60):
"""Poll until the file exists and its size is stable across two reads."""
deadline = time.monotonic() + timeout
last = -1
while time.monotonic() < deadline:
size = cdp.send("ScrapiumBrowser.getDownloadsMetadatas")["metadata"].get(filename)
if size is not None and size == last:
return size
last = size if size is not None else -1
time.sleep(0.5)
raise TimeoutError(f"{filename} did not finish downloading")
with sync_playwright() as p:
browser = p.chromium.connect_over_cdp(BROWSER_WS)
try:
context = browser.contexts[0]
page = context.pages[0] if context.pages else context.new_page()
cdp = context.new_cdp_session(page)
page.goto("https://web-scraping.dev/file-download")
page.click("#download-btn")
size = wait_for_download(cdp, "download-sample.pdf")
# One file by name: content is carried once, so the reply is ~1.34x the file.
result = cdp.send("ScrapiumBrowser.getDownload", {"filename": "download-sample.pdf"})
open("download-sample.pdf", "wb").write(base64.b64decode(result["data"]))
finally:
browser.close()会话结束时,已下载文件会被清理。对于长时间运行的会话,向 / 传入 可回收磁盘空间。
getDownloadgetDownloadsdelete: TrueLarge files: size the reply before you ask for it
大文件:请求前先评估响应大小
getDownloads1009Target closedSession closed- reply ≈ 2.7x the file — base64 costs a third and the content is carried under both
getDownloadsandfiles.files_by_guid - reply ≈ 1.34x the file. Prefer it when you know the name.
getDownload - Server-side cap: 500 MB per file. An oversized file is never encoded, and
the call it appears in fails: answers with
getDownloadsand returns nothing, including the small files that were fine.download too large: "export.csv" (620000000 bytes) exceeds the 524288000 byte limit per filestill lists the oversized file, andgetDownloadsMetadatasstill serves the others by name — that pair is the recovery path.getDownload - Call first and branch on the real size. Past a few tens of megabytes, stream instead of inlining.
getDownloadsMetadatas
getDownloads1009Target closedSession closed- 响应约为文件大小的 2.7 倍——base64 会增加约三分之一的开销,并且内容会同时通过
getDownloads和files传输。files_by_guid - 响应约为文件大小的 1.34 倍。已知文件名时应优先使用。
getDownload - 服务端上限:每个文件 500 MB。 超出上限的文件不会被编码,且包含该文件的调用会失败:会返回
getDownloads,并且不返回任何内容,包括原本正常的小文件。download too large: "export.csv" (620000000 bytes) exceeds the 524288000 byte limit per file仍会列出超大文件,而getDownloadsMetadatas仍可按名称返回其他文件——将二者结合使用即可恢复。getDownload - 应先调用 ,再根据实际大小选择处理方式。超过几十 MB 后,应改用流式传输而不是内联返回。
getDownloadsMetadatas
Stream large downloads with downloads_stream=1
downloads_stream=1使用 downloads_stream=1
流式传输大文件
downloads_stream=1Add to the Cloud Browser WebSocket URL. It is a connection
option: nothing later in the session can turn it on. then answers
with a smaller shape — each file's content once under , with
mapping GUID to filename, and anything above ~8 MB of base64 handed
back under as an IO handle instead of inline content:
downloads_stream=1getDownloadsfiles_by_guidguid_namesstreamsjson
{
"files": {},
"files_by_guid": {"9F2C1A4E-...": "JVBERi0xLjQKJe..."},
"guid_names": {"9F2C1A4E-...": "invoice.pdf", "B71D0E88-...": "export.csv"},
"streams": {
"B71D0E88-...": {"stream": "scrapfly-download:1", "size": 91250688, "name": "export.csv"}
}
}A value is an ordinary IO handle, drained with / :
streamIO.readIO.closepython
import base64
from urllib.parse import urlencode
from playwright.sync_api import sync_playwright
params = {"api_key": API_KEY, "proxy_pool": "datacenter", "downloads_stream": "1"}
BROWSER_WS = f"wss://browser.scrapfly.io?{urlencode(params)}"
CHUNK = 8 * 1024 * 1024 # base64 characters, not raw bytes (32 MB is the cap)
def drain(cdp, handle, destination):
"""Read a scrapfly-download: handle to eof. Sequential only, no offset."""
encoded = []
try:
while True:
read = cdp.send("IO.read", {"handle": handle, "size": CHUNK})
if read.get("data"):
encoded.append(read["data"])
if read.get("eof"):
break
finally:
try:
cdp.send("IO.close", {"handle": handle})
except Exception:
pass
# Chunks are cut on base64 group boundaries, so they concatenate as text
# and decode once.
with open(destination, "wb") as out:
out.write(base64.b64decode("".join(encoded)))
with sync_playwright() as p:
browser = p.chromium.connect_over_cdp(BROWSER_WS)
try:
context = browser.contexts[0]
page = context.pages[0] if context.pages else context.new_page()
cdp = context.new_cdp_session(page)
page.goto("https://web-scraping.dev/file-download")
page.click("#download-btn")
wait_for_download(cdp, "download-sample.pdf")
result = cdp.send("ScrapiumBrowser.getDownloads")
# Small files still arrive inline, large ones as handles. Handle both.
for guid, b64 in result.get("files_by_guid", {}).items():
open(result["guid_names"][guid], "wb").write(base64.b64decode(b64))
for guid, stream in result.get("streams", {}).items():
drain(cdp, stream["stream"], stream["name"])
finally:
browser.close()Rules for download handles (they differ from Chromium's own IO streams):
- Reads are sequential. Passing is an error: bytes are released as they are read.
offset - counts base64 characters here, not raw bytes. Chunks are capped at 32 MB and default to 4 MB when no
sizeis sent.size - Loop until is true. An empty
eofwithoutdatais not the end.eof - Always , including on the error path. An abandoned handle is reclaimed after 120 seconds, and reaching
IO.closereleases it, but closing is the contract.eof - Only handles are served by the session.
scrapfly-download:<n>handles,blob:streams and every other IO handle reach Chromium untouched.Page.printToPDF
在 Cloud Browser WebSocket URL 中添加 。这是一个连接选项:会话开始后无法再启用。启用后, 会返回更精简的结构——每个文件的内容在 下仅出现一次, 将 GUID 映射到文件名,而超过约 8 MB 的 base64 内容会通过 以 IO 句柄形式返回,而不是作为内联内容返回:
downloads_stream=1getDownloadsfiles_by_guidguid_namesstreamsjson
{
"files": {},
"files_by_guid": {"9F2C1A4E-...": "JVBERi0xLjQKJe..."},
"guid_names": {"9F2C1A4E-...": "invoice.pdf", "B71D0E88-...": "export.csv"},
"streams": {
"B71D0E88-...": {"stream": "scrapfly-download:1", "size": 91250688, "name": "export.csv"}
}
}streamIO.readIO.closepython
import base64
from urllib.parse import urlencode
from playwright.sync_api import sync_playwright
params = {"api_key": API_KEY, "proxy_pool": "datacenter", "downloads_stream": "1"}
BROWSER_WS = f"wss://browser.scrapfly.io?{urlencode(params)}"
CHUNK = 8 * 1024 * 1024 # base64 characters, not raw bytes (32 MB is the cap)
def drain(cdp, handle, destination):
"""Read a scrapfly-download: handle to eof. Sequential only, no offset."""
encoded = []
try:
while True:
read = cdp.send("IO.read", {"handle": handle, "size": CHUNK})
if read.get("data"):
encoded.append(read["data"])
if read.get("eof"):
break
finally:
try:
cdp.send("IO.close", {"handle": handle})
except Exception:
pass
# Chunks are cut on base64 group boundaries, so they concatenate as text
# and decode once.
with open(destination, "wb") as out:
out.write(base64.b64decode("".join(encoded)))
with sync_playwright() as p:
browser = p.chromium.connect_over_cdp(BROWSER_WS)
try:
context = browser.contexts[0]
page = context.pages[0] if context.pages else context.new_page()
cdp = context.new_cdp_session(page)
page.goto("https://web-scraping.dev/file-download")
page.click("#download-btn")
wait_for_download(cdp, "download-sample.pdf")
result = cdp.send("ScrapiumBrowser.getDownloads")
# Small files still arrive inline, large ones as handles. Handle both.
for guid, b64 in result.get("files_by_guid", {}).items():
open(result["guid_names"][guid], "wb").write(base64.b64decode(b64))
for guid, stream in result.get("streams", {}).items():
drain(cdp, stream["stream"], stream["name"])
finally:
browser.close()下载句柄的规则(它们与 Chromium 自带的 IO 流不同):
- 读取必须按顺序进行。 传入 会报错:字节会随读取被释放。
offset - 在这里统计的是 base64 字符数,而不是原始字节数。分块最大为 32 MB;未发送
size时默认为 4 MB。size - 持续循环,直到 为 true。
eof为空但data不为 true 并不表示结束。eof - 务必调用 ,错误路径中也不例外。放弃的句柄会在 120 秒后被回收;读取到
IO.close也会释放句柄,但显式关闭仍是必须遵循的约定。eof - 会话只会提供 句柄。
scrapfly-download:<n>句柄、blob:流及其他所有 IO 句柄不会被处理,而会原样传递给 Chromium。Page.printToPDF
No download at all: stream a URL out of the page
完全不触发下载:从页面流式读取 URL
When the URL is known and the browser download machinery is not needed, fetch it
in the page and stream the Blob. Nothing large ever crosses the wire, and the
500 MB cap does not apply on this path.
python
CHUNK = 10 * 1024 * 1024 # raw bytes here; the reply inflates ~1.34x
evaluated = cdp.send("Runtime.evaluate", {
"expression": f"fetch({url!r}).then(r => r.blob())",
"awaitPromise": True,
})
resolved = cdp.send("IO.resolveBlob", {"objectId": evaluated["result"]["objectId"]})
handle = "blob:" + resolved["uuid"]当 URL 已知且不需要浏览器的下载机制时,可以在页面中 fetch 该 URL 并流式传输 Blob。这样不会有大数据通过传输链路,且此路径不受 500 MB 上限约束。
python
CHUNK = 10 * 1024 * 1024 # raw bytes here; the reply inflates ~1.34x
evaluated = cdp.send("Runtime.evaluate", {
"expression": f"fetch({url!r}).then(r => r.blob())",
"awaitPromise": True,
})
resolved = cdp.send("IO.resolveBlob", {"objectId": evaluated["result"]["objectId"]})
handle = "blob:" + resolved["uuid"]Drain with IO.read / IO.close, decoding each chunk when base64Encoded is true.
Drain with IO.read / IO.close, decoding each chunk when base64Encoded is true.
`Page.printToPDF` takes `transferMode: "ReturnAsStream"` and returns a handle the
same way.
**Do not use `Network.getResponseBody` for a large body.** Chromium evicts big
response bodies from the inspector cache before you can ask
(`Request content was evicted from inspector cache`), and for a text content type
it returns the body already decoded, which silently corrupts binary payloads.
`Page.printToPDF` 使用 `transferMode: "ReturnAsStream"` 时,也会以同样的方式返回一个句柄。
**不要对大型响应正文使用 `Network.getResponseBody`。** Chromium 会在你请求之前将大型响应正文从 Inspector 缓存中淘汰(`Request content was evicted from inspector cache`);当内容类型为文本时,返回的正文已经被解码,从而悄悄破坏二进制负载。Anti-Bot Features
反机器人防护功能
The Cloud Browser automatically manages browser fingerprinting:
- TLS/JA3 fingerprints matching real browsers
- User-Agent, device metrics, timezone, locale (based on )
country - WebGL and Canvas fingerprint emulation
- HTTP/2 fingerprints
Cloud Browser 会自动管理浏览器指纹:
- 与真实浏览器匹配的 TLS/JA3 指纹
- User-Agent、device metrics、timezone、locale(基于 )
country - WebGL 和 Canvas 指纹模拟
- HTTP/2 指纹
Important Notes
重要说明
- Always use (not
connect_over_cdp()) to connect to the Cloud Browserconnect() - Always use to get the pre-configured context. Do NOT create new contexts - the Cloud Browser provides a context with proper fingerprinting
browser.contexts[0] - Always close the browser in a block to stop billing. Forgetting to close means continuous charges
try/finally - Do NOT set fingerprint overrides (user agent, viewport, timezone, etc.) - they are managed automatically
- Sessions expire after 1 hour of inactivity
- Maximum session duration is 30 minutes (1800 seconds)
- Maximum file download size is 500 MB per file, and one oversized file fails the
whole call. Past a few tens of megabytes use
getDownloadsand drain the IO handle, see File Downloadsdownloads_stream=1 - Start with , upgrade to
proxy_pool=datacenteronly when needed for anti-bot bypassresidential - Use for session video recording when troubleshooting
debug=true
- 务必使用 (而不是
connect_over_cdp())连接 Cloud Browserconnect() - 务必使用 获取预配置的 context。不要创建新 context——Cloud Browser 已提供一个具备正确指纹模拟的 context
browser.contexts[0] - 务必在 块中关闭 browser,以停止计费。忘记关闭将持续产生费用
try/finally - 不要设置指纹覆盖项(User-Agent、viewport、timezone 等)——这些均由系统自动管理
- 会话在 1 小时无活动后过期
- 会话最长持续时间为 30 分钟(1800 秒)
- 每个文件的最大下载大小为 500 MB;一个超大文件会导致整个 调用失败。超过几十 MB 时,请使用
getDownloads并读取 IO 句柄,参见文件下载downloads_stream=1 - 初始使用 ,仅在需要绕过反机器人防护时升级为
proxy_pool=datacenterresidential - 排查问题时,使用 录制会话视频
debug=true