跨浏览器跑同一份用例,选 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) | 内置(Fetch、Network) |
| 跨浏览器 | 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.php 与 res.php,区别只在改用 page.evaluate 和 Runtime.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,两套脚本共用一份逻辑。