Comparisons

用于验证码自动化的 WebDriver 与 Chrome DevTools 协议

跨浏览器跑同一份用例,选 WebDriver;只面向 Chromium,还要拦请求、改响应、读写 httpOnly cookie,就选 CDP。验证码这一环两边一样——取 sitekey、交给 CaptchaAI、把 token 写回表单,走 HTTP 接口,与浏览器协议无关。

两条链路:多一层中间件还是直连

WebDriver 是 W3C 标准,Selenium 用 HTTP 把命令发给 ChromeDriver,再由它翻译成 DevTools 命令;CDP 没有中间层,一条 WebSocket 直连浏览器。

WebDriver (Selenium):
  Script ──HTTP──▶ ChromeDriver ──DevTools──▶ Chrome
  (JSON Wire)        (bridge)      (internal)

CDP (Puppeteer/Playwright):
  Script ──WebSocket──▶ Chrome
  (direct)               (no middle layer)

这一跳解释了几乎所有差异:每条命令一次 HTTP 往返;ChromeDriver 会留下内部变量;能力受限于 W3C 规范。

能力对照:WebDriver 与 CDP 逐项比较

对比项 WebDriver CDP
传输方式 HTTP + JSON WebSocket
网络拦截 不支持(需 Selenium Wire) 内置(FetchNetwork
跨浏览器 Chrome、Firefox、Safari、Edge 仅 Chromium

站点如何识别 Selenium 环境

风控系统不只看一个标志位,而是把几个自动化环境特有的属性一起取出来判断:

// What sites check for Selenium
navigator.userAgent                    // true (set by ChromeDriver)
document.$cdc_asdjflasutopfhvcZLmcfl  // ChromeDriver internal variable
window.callSelenium                    // Older Selenium versions
window._selenium                      // Selenium internals
document.__webdriver_evaluate          // WebDriver namespace

启动参数能关掉一部分提示,但 $cdc_ 前缀的变量由 ChromeDriver 注入 document:

# Even with flag removal, ChromeDriver leaves traces
from selenium import webdriver

options = webdriver.ChromeOptions()
options.add_argument("--no-sandbox")
options.add_experimental_option("excludeSwitches", ["enable-automation"])
options.add_experimental_option("useAutomationExtension", False)

driver = webdriver.Chrome(options=options)

# Still detectable via $cdc variables in document

CDP 没有这套专有变量,还允许在文档创建前执行脚本:

// Puppeteer sets webdriver flag too, but it's fully patchable
await page.evaluateOnNewDocument(() => {
  Object.defineProperty(navigator, 'webdriver', {
    get: () => undefined,
  });
  // No $cdc variables to worry about
  // No Selenium-specific traces
});

用 Selenium + CaptchaAI 识别 reCAPTCHA v2

取出 sitekey 提交给 in.php,每 5 秒轮询 res.php,拿到 token 写回 #g-recaptcha-response 再提交表单。

# Selenium (WebDriver) + CaptchaAI
import requests
import time
from selenium import webdriver
from selenium.webdriver.common.by import By

API_KEY = "YOUR_API_KEY"
API_URL = "https://ocr.captchaai.com"

options = webdriver.ChromeOptions()
options.add_argument("--no-sandbox")
driver = webdriver.Chrome(options=options)

driver.get("https://example.com/form")
time.sleep(2)

# Extract sitekey
sitekey = driver.execute_script(
    "return document.querySelector('[data-sitekey]')?.getAttribute('data-sitekey')"
)

# Solve via CaptchaAI
resp = requests.post(f"{API_URL}/in.php", data={
    "key": API_KEY, "method": "userrecaptcha",
    "googlekey": sitekey, "pageurl": driver.current_url, "json": 1,
})
task_id = resp.json()["request"]

for _ in range(60):
    time.sleep(5)
    resp = requests.get(f"{API_URL}/res.php", params={
        "key": API_KEY, "action": "get", "id": task_id, "json": 1,
    })
    data = resp.json()
    if data["request"] != "CAPCHA_NOT_READY":
        break

token = data["request"]

# Inject token
driver.execute_script(f"""
    document.querySelector('#g-recaptcha-response').value = '{token}';
    document.querySelectorAll('[name="g-recaptcha-response"]')
        .forEach(el => {{ el.value = '{token}'; }});
""")

driver.find_element(By.CSS_SELECTOR, "form").submit()
driver.quit()

页面常有多个同名字段,示例用 querySelectorAll 一起赋值,避免只填了隐藏的那个。

用 Puppeteer + CaptchaAI 走同一套接口

接口调用完全一样:同一个 API Key、同一组参数、同样的 in.phpres.php,区别只在改用 page.evaluateRuntime.evaluate

// Puppeteer (CDP) + CaptchaAI
const puppeteer = require("puppeteer-extra");
const StealthPlugin = require("puppeteer-extra-plugin-stealth");
const https = require("https");

puppeteer.use(StealthPlugin());

const API_KEY = "YOUR_API_KEY";
const API_URL = "https://ocr.captchaai.com";

function solveCaptcha(siteUrl, sitekey) {
  // Submit + poll (simplified)
  return new Promise(async (resolve, reject) => {
    const submitResp = await httpPost(`${API_URL}/in.php`, {
      key: API_KEY, method: "userrecaptcha",
      googlekey: sitekey, pageurl: siteUrl, json: "1",
    });

    const taskId = submitResp.request;

    for (let i = 0; i < 60; i++) {
      await new Promise((r) => setTimeout(r, 5000));
      const result = await httpGet(
        `${API_URL}/res.php?key=${API_KEY}&action=get&id=${taskId}&json=1`
      );
      if (result.request !== "CAPCHA_NOT_READY") {
        return resolve(result.request);
      }
    }
    reject(new Error("Timeout"));
  });
}

(async () => {
  const browser = await puppeteer.launch({
    headless: "new",
    args: ["--no-sandbox", "--window-size=1920,1080"],
  });

  const page = await browser.newPage();

  // CDP: Enable network interception
  const cdp = await page.createCDPSession();
  await cdp.send("Network.enable");

  // Monitor CAPTCHA-related requests
  cdp.on("Network.requestWillBeSent", (params) => {
    if (params.request.url.includes("recaptcha")) {
      console.log(`CAPTCHA request: ${params.request.url}`);
    }
  });

  await page.goto("https://example.com/form", { waitUntil: "networkidle0" });

  const sitekey = await page.evaluate(() =>
    document.querySelector("[data-sitekey]")?.getAttribute("data-sitekey")
  );

  if (sitekey) {
    const token = await solveCaptcha(page.url(), sitekey);

    // CDP: Inject via Runtime.evaluate (lower-level than page.evaluate)
    await cdp.send("Runtime.evaluate", {
      expression: `
        document.querySelector('#g-recaptcha-response').value = '${token}';
        document.querySelector('form').submit();
      `,
    });
  }

  await browser.close();
})();

页面迟迟不出现验证码框时,这段网络日志比翻控制台快。

CDP 才有的三件事

网络拦截。 WebDriver 规范里没有这个接口,Selenium 侧要靠第三方的 Selenium Wire:

# WebDriver: Cannot intercept requests natively
# Requires selenium-wire (third-party)
from seleniumwire import webdriver

driver = webdriver.Chrome()
# selenium-wire proxies all traffic through a local proxy

CDP 在协议层提供 Fetch 域,按 URL 模式挂钩子即可:

// CDP: Built-in request interception
await cdp.send("Fetch.enable", {
  patterns: [{ urlPattern: "*recaptcha*" }],
});

cdp.on("Fetch.requestPaused", async ({ requestId, request }) => {
  console.log(`Intercepted: ${request.url}`);
  await cdp.send("Fetch.continueRequest", { requestId });
});

改写响应。 调试自家页面在不同验证码配置下的表现,比改后端快:

// CDP: Can modify responses (WebDriver cannot)
cdp.on("Fetch.requestPaused", async ({ requestId, request }) => {
  if (request.url.includes("captcha-config")) {
    // Return modified response
    await cdp.send("Fetch.fulfillRequest", {
      requestId,
      responseCode: 200,
      body: btoa(JSON.stringify({ enabled: false })),
    });
  } else {
    await cdp.send("Fetch.continueRequest", { requestId });
  }
});

cookie 读写。 WebDriver 只能操作脚本可见的那部分:

# WebDriver: Standard cookie API
driver.add_cookie({"name": "session", "value": "abc123"})
cookies = driver.get_cookies()

CDP 则能读写全部 cookie,包括 httpOnly:

// CDP: Full cookie control including httpOnly
await cdp.send("Network.setCookie", {
  name: "session",
  value: "abc123",
  domain: "example.com",
  httpOnly: true,
  secure: true,
});

// Get all cookies including httpOnly (WebDriver can't access these)
const { cookies } = await cdp.send("Network.getAllCookies");

混合写法:Selenium 4 直接发 CDP 命令

不必二选一。Selenium 4 提供了 execute_cdp_cmd():导航和定位继续用 WebDriver,需要 CDP 能力时再切过去。

from selenium import webdriver

driver = webdriver.Chrome()

# Use WebDriver for navigation
driver.get("https://example.com")

# Use CDP for QA patches
driver.execute_cdp_cmd("Page.addScriptToEvaluateOnNewDocument", {
    "source": """
        Object.defineProperty(navigator, 'webdriver', {
            get: () => undefined
        });
    """
})

# Use CDP for network events
driver.execute_cdp_cmd("Network.enable", {})

对已有大量 Selenium 用例的团队,这是成本最低的路径。

性能参考数据

以下数字基于公开资料与内部观测样本,仅供参考。实际结果会因运行环境、任务量和时间而不同,请在自有环境中测量。

指标 WebDriver(Selenium) CDP(Puppeteer)
会话启动 ~2.5 秒 ~1.5 秒
reCAPTCHA v2 识别(CaptchaAI) <60 秒 <60 秒

识别耗时两边一致:那是一次跑在服务端的调用。

国内开发环境的两个变量

  • 验证码类型不同。 国内站点大多用 GeeTest(极验)、网易易盾,reCAPTCHA 和 Turnstile 更多出现在出海业务上。CaptchaAI 覆盖 reCAPTCHA v2/v3、Turnstile 与 GeeTest v3,GeeTest v4 目前只是即将支持。
  • reCAPTCHA 依赖 Google 域名下的脚本,从内地网络访问并不稳定。waitUntil: "networkidle0" 容易卡到超时,改成 domcontentloaded 再等验证码容器更稳。

另外,只在自己拥有或已获授权的站点上跑自动化,并遵守 robots 协议。

常见问题

换成 CDP 之后,页面弹验证码的次数会变少吗?

有可能变少,但不要指望归零。站点策略和请求频率同样起作用,稳妥做法是把识别接好,让脚本不中断。

token 已经写回表单,还是提示验证失败,怎么排查?

先确认 pageurl 与地址栏一致、googlekey 是当前页面真实的 sitekey、token 没过期。都对还失败,多半是字段在 iframe 内。

同时跑几十个浏览器实例,识别会成为瓶颈吗?

取决于线程数,不是协议。CaptchaAI 按并发线程计费,BASIC($15/月,5 线程)起步,线程内识别次数不限。

延伸阅读

协议选定之后,验证码交给接口更省事——获取 CaptchaAI API Key,两套脚本共用一份逻辑。

该文章已禁用评论。