什么时候必须上浏览器
requests 拿到的是服务端返回的初始 HTML。如果页面内容由前端 JS 渲染,或者接口带签名参数需要浏览器环境才能生成,就必须用真实浏览器。Playwright 比 Selenium 更适合采集场景:启动快、原生支持请求拦截、等待机制更可靠。
基础配置
代理在 launch 或 new_context 阶段传入:
from playwright.sync_api import sync_playwright
PROXY = {
"server": "http://gateway.example.com:8080",
"username": "用户名",
"password": "密码",
}
with sync_playwright() as pw:
browser = pw.chromium.launch(proxy=PROXY, headless=True)
context = browser.new_context(
locale="zh-CN",
timezone_id="Asia/Shanghai",
viewport={"width": 1440, "height": 900},
)
page = context.new_page()
page.goto("https://shop.example.com/item/123", wait_until="domcontentloaded")
print(page.title())
browser.close()
要点:locale 和 timezone_id 一定要和目标业务地区一致。用国内时区去访问美国站点的价格页,是很常见的暴露点。
等待:不要用固定 sleep
固定 time.sleep(3) 要么浪费三秒,要么在慢的时候不够。用显式等待:
# 等某个元素出现,而不是等固定时间
page.goto(url, wait_until="domcontentloaded")
# 方式一:等选择器
page.wait_for_selector(".price-value", timeout=15000)
# 方式二:等接口返回(更适合数据在 XHR 里的页面)
with page.expect_response(lambda r: "/api/product/" in r.url) as info:
page.wait_for_selector("#load-more")
payload = info.value.json()
方式二往往更高效:数据本来就在接口响应里,不必等 DOM 渲染完再解析。
资源拦截省流量
住宅代理按流量计费,浏览器默认会加载图片、字体、媒体和大量第三方脚本,这些几乎都是纯消耗:
BLOCK = {"image", "font", "media"}
BLOCK_HOSTS = ("google-analytics.com", "doubleclick.net", "hotjar.com")
def route_handler(route):
req = route.request
if req.resource_type in BLOCK:
return route.abort()
if any(h in req.url for h in BLOCK_HOSTS):
return route.abort()
return route.continue_()
context.route("**/*", route_handler)
注意不要拦 stylesheet。很多站点的关键内容依赖 CSS 选择器判断可见性,拦掉样式表可能导致元素被判定为不可见而拿不到数据。
用真实的浏览器上下文
new_context 默认是一个干净环境,缺少真实浏览器的很多细节。用持久化上下文能保留 Cookie 和本地存储,对需要登录态的任务很重要:
# 持久化上下文:目录里保留 cookie、localStorage、缓存
context = pw.chromium.launch_persistent_context(
user_data_dir="./profile_shop",
proxy=PROXY,
locale="zh-CN",
viewport={"width": 1440, "height": 900},
)
配合静态住宅 IP 使用时,一个账号对应一个 user_data_dir 加一个固定出口 IP,环境一致性最好。
被风控时,先分清是哪一层的问题
Playwright 场景下”抓不到数据”有三个完全不同的原因,处理方式差别很大:
| 现象 | 大概率原因 | 处理方向 |
|---|---|---|
| 页面返回验证码 / 挑战页 | 出口 IP 被风控 | 换住宅代理,降低单 IP 频率 |
| 页面正常但关键元素不存在 | 浏览器指纹被识别 | 处理自动化特征,改用持久化上下文 |
| 元素选择器超时,但手动浏览器正常 | 等待策略不对 | 改显式等待,等接口而非 DOM |
| 请求直接 403 | 代理未生效或 IP 被封 | 先用 httpbin 确认出口 IP |
第三行是纯工程问题,跟代理无关,但经常被误判成”代理质量差”。
关于自动化特征
Playwright 默认会暴露一些自动化痕迹,比如 navigator.webdriver。部分站点会据此拦截。处理方式是启动时关掉自动化开关:
browser = pw.chromium.launch(
proxy=PROXY,
args=[
"--disable-blink-features=AutomationControlled",
"--no-sandbox",
],
)
需要注意的是,这类处理只能解决基础检测。如果目标站点有成熟的设备指纹体系,仅靠参数调整不够,通常需要更完整的浏览器环境方案。先确认瓶颈是否真的在这一层——用同一个代理配 requests 请求同一页面,如果 requests 能拿到内容而 Playwright 不行,那才是指纹问题;如果两者都拿不到,问题在 IP。
一个稳定的采集骨架
把上面的要点合起来,一个可用的骨架大概是这样:
from playwright.sync_api import sync_playwright
import random
def scrape(urls, proxy):
results = []
with sync_playwright() as pw:
browser = pw.chromium.launch(proxy=proxy, headless=True,
args=["--disable-blink-features=AutomationControlled"])
context = browser.new_context(locale="zh-CN", timezone_id="Asia/Shanghai")
context.route("**/*", lambda r: r.abort()
if r.request.resource_type in {"image", "font", "media"}
else r.continue_())
page = context.new_page()
for url in urls:
try:
page.goto(url, wait_until="domcontentloaded", timeout=45000)
page.wait_for_selector(".price-value", timeout=15000)
results.append({"url": url, "price": page.inner_text(".price-value")})
except Exception as e:
results.append({"url": url, "error": str(e)})
# 请求之间加随机间隔,避免形成规律流量
page.wait_for_timeout(random.randint(800, 2500))
browser.close()
return results
小结
Playwright 和代理配合的关键是三点:时区与语言对齐目标地区、显式等待替代固定 sleep、拦截静态资源控制流量。被拦截时先做一次 requests 对照测试,用结果区分是 IP 层的问题还是浏览器层的问题,能省掉大量无效调参。
取舍结论
| 维度 | 结论 |
|---|---|
| 前提条件 | 已用 requests 对同一页面做过对照测试,确认瓶颈在浏览器层而不是 IP 层。 |
| 核心取舍 | 拦截静态资源能省一半以上流量,但拦掉样式表可能让依赖可见性判断的元素取不到值。 |
| 不适用场景 | 目标站点有成熟设备指纹体系时,仅靠启动参数处理不够,需要更完整的浏览器环境方案。 |
一句话:先做 requests 对照测试再调参,避免把 IP 问题误判成指纹问题。
在你的目标站点上验证这套方案
文中的做法都可以用试用额度直接跑通。用你自己的目标站点测一轮,成功率数据比任何评测都可靠。