Playwright 自动登录实战:CDP 与 Camoufox 两条路线对比

Playwright 自动登录实战:CDP 与 Camoufox 两条路线对比

同样是用 Playwright 自动登录一个网站,走 CDP 连 Chrome 和走 Camoufox 连 Firefox,从代码到效果完全是两个世界。

一、两条路线概览

维度 CDP 路线 Camoufox 路线
浏览器 Chromium / Chrome Camoufox(Firefox 魔改)
协议 Chrome DevTools Protocol Juggler(Playwright 原生 Firefox 协议)
反检测 无,需要自己加 stealth 补丁 C++ 级内置,开箱即用
指纹伪造 JavaScript 注入,可被检测 引擎层修改,JS 看到的就是"真实"值
适用场景 内部系统、无反爬的网站 Google、Cloudflare、有反机器人检测的网站

二、路线 A:CDP 连接 Chrome

这是最常见的方案。启动一个 Chrome 实例,通过 CDP 端口连接。

基本流程

from playwright.async_api import async_playwright
import asyncio

async def login_via_cdp():
    async with async_playwright() as p:
        # 连接到已运行的 Chrome(通过 CDP)
        browser = await p.chromium.connect_over_cdp("http://localhost:9222")
        context = browser.contexts[0]
        page = context.pages[0] if context.pages else await context.new_page()

        await page.goto("https://example.com/login")
        await page.fill('input[name="username"]', '<username>')
        await page.fill('input[name="password"]', '<password>')
        await page.click('button[type="submit"]')
        await page.wait_for_url("**/dashboard**")

        # 保存登录状态
        storage = await context.storage_state(path="state.json")
        print("登录成功,状态已保存")
        await browser.close()

asyncio.run(login_via_cdp())

启动 Chrome 的方式

# 方式一:直接启动带 CDP 的 Chrome
google-chrome \
  --remote-debugging-port=9222 \
  --user-data-dir=/tmp/chrome-profile \
  --no-first-run

# 方式二:Docker(BrowserStation 场景)
docker run -d -p 9222:9222 zenika/alpine-chrome:100 \
  --no-sandbox \
  --remote-debugging-address=0.0.0.0 \
  --remote-debugging-port=9222

或者让 Playwright 自己启动

async def login_local_chrome():
    async with async_playwright() as p:
        browser = await p.chromium.launch(headless=False)
        context = await browser.new_context()
        page = await context.new_page()

        await page.goto("https://example.com/login")
        await page.fill('input[name="username"]', '<username>')
        await page.fill('input[name="password"]', '<password>')
        await page.click('button[type="submit"]')
        await page.wait_for_url("**/dashboard**")

        await context.storage_state(path="state.json")
        await browser.close()

CDP 路线的问题

在没有反爬的内部系统上,上面的代码跑得很好。但一旦目标网站有反机器人检测,你会遇到:

1. navigator.webdriver 泄露

// 反爬脚本检测
if (navigator.webdriver === true) {
    // 你是机器人
}

Playwright 启动的 Chromium 默认 navigator.webdriver = true

2. CDP 协议本身可被检测

反爬系统可以通过多种方式检测 CDP 的存在:

3. 指纹不一致

即使你用 stealth 插件伪造了 User-Agent,但 Canvas、WebGL、AudioContext 等深层指纹没变,反爬系统一做交叉验证就露馅了。

加 Stealth 补丁(治标不治本)

# pip install playwright-stealth
from playwright_stealth import stealth_async

async def login_with_stealth():
    async with async_playwright() as p:
        browser = await p.chromium.launch(headless=False)
        context = await browser.new_context()
        page = await context.new_page()

        # 注入 stealth 脚本
        await stealth_async(page)

        await page.goto("https://example.com/login")
        # ... 登录逻辑同上

Stealth 插件做的事情:

但这些都是 JavaScript 层面的伪造,反爬系统可以用更深层的检测手段识破:

// 反爬系统的反制
// 检查属性描述符是否被篡改
const desc = Object.getOwnPropertyDescriptor(navigator, 'webdriver');
if (desc && desc.get) {
    // 原生属性不应该有自定义 getter → 你在伪造
}

// 检查 Worker 线程里的值(stealth 通常只改主线程)
const worker = new Worker(URL.createObjectURL(
    new Blob([`postMessage(navigator.webdriver)`])
));
worker.onmessage = (e) => {
    if (e.data === true) {
        // Worker 里没被改 → 你在伪造
    }
};

三、路线 B:Camoufox

Camoufox 是一个修改了 C++ 源码的 Firefox 分支。它的 Python 库直接返回 Playwright 的 Browser 对象,你现有的 Playwright 代码几乎不用改。

安装

# 安装 Camoufox(含 GeoIP 支持)
pip install -U "camoufox[geoip]"

# 首次运行会自动下载 Camoufox 浏览器二进制文件
python -c "from camoufox.sync_api import Camoufox; Camoufox()"

基本登录

from camoufox.async_api import AsyncCamoufox
import asyncio

async def login_via_camoufox():
    async with AsyncCamoufox(
        headless=False,
        humanize=True,       # 贝塞尔曲线鼠标移动
        os="windows",        # 伪装成 Windows 用户
    ) as browser:
        page = await browser.new_page()
        await page.goto("https://example.com/login")
        await page.fill('input[name="username"]', '<username>')
        await page.fill('input[name="password"]', '<password>')
        await page.click('button[type="submit"]')
        await page.wait_for_url("**/dashboard**")

        # 保存状态(跟 Playwright 完全一样的 API)
        context = browser.contexts[0]
        await context.storage_state(path="state.json")
        print("登录成功")

asyncio.run(login_via_camoufox())

注意看——登录逻辑的代码跟 CDP 路线几乎一模一样。区别只在浏览器初始化部分。

带代理 + GeoIP 自动匹配

async def login_with_proxy():
    async with AsyncCamoufox(
        headless=True,
        humanize=True,
        geoip=True,  # 自动根据代理 IP 设置时区、语言、地理位置
        proxy={
            "server": "http://proxy.example.com:8080",
            "username": "<proxy_user>",
            "password": "<proxy_pass>",
        },
    ) as browser:
        page = await browser.new_page()
        await page.goto("https://example.com/login")
        # ... 登录逻辑

geoip=True 做了什么:

这意味着你用日本代理,浏览器的时区就是 Asia/Tokyo,语言可能是 ja-JP,WebRTC 不会泄露真实 IP。全部自动完成,不需要手动配置。

过 Cloudflare Turnstile

async def login_behind_cloudflare():
    async with AsyncCamoufox(
        headless=False,
        humanize=True,
        disable_coop=True,  # 允许点击 cross-origin iframe 里的 Turnstile
    ) as browser:
        page = await browser.new_page()
        await page.goto("https://example.com/login")

        # Cloudflare Turnstile 验证
        await page.wait_for_load_state("networkidle")
        await page.wait_for_timeout(3000)

        # Turnstile 通常在 iframe 里,Camoufox 的人性化鼠标可以直接点
        # 具体坐标需要根据页面布局调整
        turnstile_frame = page.frame_locator("iframe[src*='challenges.cloudflare.com']")
        await turnstile_frame.locator("input[type='checkbox']").click()

        await page.wait_for_timeout(3000)

        # 继续登录
        await page.fill('input[name="username"]', '<username>')
        await page.fill('input[name="password"]', '<password>')
        await page.click('button[type="submit"]')

复用登录状态

import os

STATE_FILE = "login_state.json"

async def login_once_reuse_forever():
    if os.path.exists(STATE_FILE):
        # 复用已有状态
        async with AsyncCamoufox(headless=True) as browser:
            context = await browser.new_context(storage_state=STATE_FILE)
            page = await context.new_page()
            await page.goto("https://example.com/dashboard")

            if "/login" in page.url:
                print("状态过期,重新登录")
                os.remove(STATE_FILE)
                return await login_once_reuse_forever()

            print("复用登录状态成功")
            return page
    else:
        # 首次登录
        async with AsyncCamoufox(headless=False, humanize=True) as browser:
            page = await browser.new_page()
            await page.goto("https://example.com/login")
            await page.fill('input[name="username"]', '<username>')
            await page.fill('input[name="password"]', '<password>')
            await page.click('button[type="submit"]')
            await page.wait_for_url("**/dashboard**")

            context = browser.contexts[0]
            await context.storage_state(path=STATE_FILE)
            print("首次登录完成,状态已保存")

四、深层对比

反检测能力

检测维度 CDP + Chrome CDP + Stealth Camoufox
navigator.webdriver ❌ 暴露 ⚠️ JS 伪造,可被反制 ✅ 引擎层返回 undefined
CDP 协议痕迹 ❌ 可检测 ⚠️ 部分隐藏 ✅ 不使用 CDP,用 Juggler
Playwright 注入变量 ❌ 可检测 ⚠️ 部分隐藏 ✅ 沙箱隔离,页面看不到
Canvas 指纹 ❌ 真实值暴露 ⚠️ 噪声注入,可检测注入行为 ✅ C++ 层伪造
WebGL 指纹 ❌ 真实值暴露 ⚠️ 部分伪造 ✅ C++ 层伪造
AudioContext ❌ 真实值暴露 ❌ 通常不处理 ✅ C++ 层伪造
字体枚举 ❌ 真实值暴露 ❌ 通常不处理 ✅ 伪造 + 匹配目标 OS
WebRTC IP 泄露 ❌ 泄露真实 IP ⚠️ 可禁用但行为异常 ✅ 协议层伪造
TLS 指纹 (JA3/JA4) ⚠️ Chromium 特征 ⚠️ 同左 ✅ Firefox 特征,更难识别
Worker 线程一致性 ❌ stealth 不覆盖 Worker ❌ 同左 ✅ 引擎层修改,全局一致
toString() 检测 ❌ 暴露 JS 覆写 ⚠️ 部分处理 ✅ 原生返回 [native code]

为什么 Juggler 比 CDP 更隐蔽

CDP 的问题在于它是 Chromium 的一部分,反爬系统对它研究得非常透彻。常见的检测方式:

// 检测 CDP 的 Runtime.evaluate 痕迹
// CDP 执行 JS 时会在调用栈里留下特征
try {
    null[0]();
} catch (e) {
    if (e.stack.includes('Runtime.evaluate')) {
        // CDP 在控制这个浏览器
    }
}

Juggler 是 Playwright 团队为 Firefox 开发的独立协议,不是 Firefox 核心的一部分。Camoufox 进一步修改了 Juggler:

指纹一致性

CDP + Stealth 最大的问题不是单个参数伪造不了,而是参数之间不自洽

❌ 典型的 Stealth 指纹矛盾:
   User-Agent: Windows 10 / Chrome 120
   WebGL Renderer: Apple M1 GPU        ← 不可能出现在 Windows 上
   屏幕分辨率: 2560x1600               ← MacBook 分辨率
   字体列表: 包含 SF Pro               ← macOS 独有字体

Camoufox 使用 BrowserForge 生成指纹,确保所有参数匹配真实设备的统计分布:

✅ Camoufox 生成的一致指纹:
   User-Agent: Windows 10 / Firefox 135
   WebGL Renderer: NVIDIA GeForce RTX 3060
   屏幕分辨率: 1920x1080
   字体列表: Segoe UI, Calibri, Consolas  ← 全是 Windows 字体
   时区: America/Chicago                   ← 匹配代理 IP 地理位置
   语言: en-US

五、性能与资源对比

指标 CDP + Chrome Camoufox
启动时间 ~1-2s ~2-3s(首次需下载二进制 ~300MB)
内存占用 ~150-300MB ~200-400MB
页面加载速度 快(V8 引擎优化好) 略慢(Firefox 引擎)
Chromium 专属网站兼容性 ✅ 完美 ⚠️ 极少数 Chrome-only 功能不支持
并发能力 中等(指纹生成有开销)

六、怎么选

用 CDP + Chrome 的场景:

用 Camoufox 的场景:

混合使用(推荐):

async def smart_login(url: str, has_antibot: bool = False):
    if has_antibot:
        async with AsyncCamoufox(humanize=True, geoip=True, proxy=PROXY) as browser:
            page = await browser.new_page()
            await page.goto(url)
            return page
    else:
        async with async_playwright() as p:
            browser = await p.chromium.connect_over_cdp("http://browserstation:9222")
            page = browser.contexts[0].pages[0]
            await page.goto(url)
            return page

七、总结

CDP 和 Camoufox 不是非此即彼的关系。CDP 是基础设施级的浏览器控制协议,成熟、快速、生态完善;Camoufox 是专门解决"被检测"这个问题的工具,在反爬对抗上做到了目前开源方案的天花板。

核心区别就一句话:CDP + Stealth 是在 JavaScript 层面"演戏",Camoufox 是在 C++ 层面"整容"。 演戏可以被拆穿,整容后连 X 光都看不出来。

选择哪条路线,取决于你的对手是谁。