用 Node.js + Playwright 跑自动化时,最容易卡住的一步往往是验证码。
本教程给出可复用的做法:提取页面 sitekey,交给 CaptchaAI API 识别,再把 token 写回页面提交。
覆盖三类最常见的场景:
- reCAPTCHA v2 识别与回调触发
- Cloudflare Turnstile sitekey 提取与写回
- 图片验证码识别
所有示例仅用于你自有或已授权的 QA、staging 环境,请勿用于未授权的第三方站点。
环境准备与安装
安装 Playwright 与浏览器内核,国内可搭配 npm 镜像加速:
npm install playwright
npx playwright install chromium
浏览器初始化配置
标准初始化:指定 User-Agent、视口和 locale,保证不同机器上行为一致。
const { chromium } = require("playwright");
async function createBrowser() {
const browser = await chromium.launch({
headless: false,
args: ["--no-sandbox"],
});
const context = await browser.newContext({
userAgent:
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " +
"(KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
viewport: { width: 1920, height: 1080 },
locale: "en-US",
});
// Remove Playwright detection
await context.addInitScript(() => {
Object.defineProperty(navigator, "webdriver", { get: () => undefined });
delete navigator.__proto__.webdriver;
});
const page = await context.newPage();
return { browser, context, page };
}
封装 CaptchaAI 识别接口
CaptchaAI 调用分两步,后面各类验证码都复用它:
- 向
in.php提交任务,拿到 ID - 每 5 秒轮询
res.php,直到结果就绪
const API_KEY = "YOUR_API_KEY";
async function solveCaptcha(method, params) {
// Submit
const submitResp = await fetch("https://ocr.captchaai.com/in.php", {
method: "POST",
body: new URLSearchParams({ key: API_KEY, method, json: "1", ...params }),
});
const submitData = await submitResp.json();
if (submitData.status !== 1) throw new Error(`Submit: ${submitData.request}`);
const taskId = submitData.request;
// Poll
for (let i = 0; i < 30; i++) {
await new Promise((r) => setTimeout(r, 5000));
const pollResp = await fetch(
`https://ocr.captchaai.com/res.php?${new URLSearchParams({
key: API_KEY,
action: "get",
id: taskId,
json: "1",
})}`
);
const data = await pollResp.json();
if (data.status === 1) return data.request;
if (data.request === "ERROR_CAPTCHA_UNSOLVABLE") throw new Error("Unsolvable");
}
throw new Error("Timed out");
}
用 Playwright 识别 reCAPTCHA v2
流程固定为三步:
- 从 DOM 读出
data-sitekey - 用
userrecaptcha方法提交给 API - token 写回
g-recaptcha-response并触发回调
async function solveRecaptchaV2(page) {
// Extract sitekey
const sitekey = await page.evaluate(() => {
const el = document.querySelector("[data-sitekey]");
return el ? el.getAttribute("data-sitekey") : null;
});
if (!sitekey) throw new Error("Sitekey not found");
// Solve
const token = await solveCaptcha("userrecaptcha", {
googlekey: sitekey,
pageurl: page.url(),
});
// Inject
await page.evaluate((t) => {
const textarea = document.getElementById("g-recaptcha-response");
if (textarea) {
textarea.value = t;
textarea.style.display = "block";
}
// Trigger callback
if (typeof ___grecaptcha_cfg !== "undefined") {
const clients = ___grecaptcha_cfg.clients;
for (const key in clients) {
for (const prop in clients[key]) {
try {
const cb = clients[key][prop];
if (cb && typeof cb.callback === "function") cb.callback(t);
} catch {}
}
}
}
}, token);
return token;
}
用 Playwright 识别 Cloudflare Turnstile
Turnstile 的 sitekey 以 0x 开头,常在页面加载后由 JS 注入。
提取与写回顺序:
- 先找
.cf-turnstile元素 - 回退到遍历
0x开头的data-sitekey - token 写入隐藏字段
cf-turnstile-response
async function solveTurnstile(page) {
// Extract sitekey
const sitekey = await page.evaluate(() => {
const el = document.querySelector(".cf-turnstile[data-sitekey]");
if (el) return el.getAttribute("data-sitekey");
// Fallback: any data-sitekey starting with 0x
const all = document.querySelectorAll("[data-sitekey]");
for (const item of all) {
const key = item.getAttribute("data-sitekey");
if (key && key.startsWith("0x")) return key;
}
return null;
});
if (!sitekey) throw new Error("Turnstile sitekey not found");
// Solve
const token = await solveCaptcha("turnstile", {
sitekey,
pageurl: page.url(),
});
// Inject
await page.evaluate((t) => {
document
.querySelectorAll('[name="cf-turnstile-response"]')
.forEach((el) => (el.value = t));
}, token);
return token;
}
自动检测验证码类型并识别
真实任务常遇到不同类型的验证码:国内站点多用极验(GeeTest)滑块,海外站更常见 reCAPTCHA 与 Turnstile。
函数按顺序判断类型:
- 先判断 reCAPTCHA
- 再判断 Turnstile
- 最后回退图片验证码
async function detectAndSolve(page) {
const captchaInfo = await page.evaluate(() => {
// Check reCAPTCHA
const recaptcha = document.querySelector("[data-sitekey]");
if (
recaptcha &&
(document.querySelector(".g-recaptcha") ||
document.querySelector('script[src*="recaptcha"]'))
) {
return { type: "recaptcha", sitekey: recaptcha.getAttribute("data-sitekey") };
}
// Check Turnstile
const turnstile = document.querySelector(".cf-turnstile[data-sitekey]");
if (turnstile) {
return { type: "turnstile", sitekey: turnstile.getAttribute("data-sitekey") };
}
// Check image CAPTCHA
const captchaImg = document.querySelector(
'img.captcha, img[alt*="captcha"], img[src*="captcha"]'
);
if (captchaImg) {
return { type: "image" };
}
return { type: null };
});
if (!captchaInfo.type) return null;
console.log(`Detected: ${captchaInfo.type}`);
switch (captchaInfo.type) {
case "recaptcha":
return await solveCaptcha("userrecaptcha", {
googlekey: captchaInfo.sitekey,
pageurl: page.url(),
});
case "turnstile":
return await solveCaptcha("turnstile", {
sitekey: captchaInfo.sitekey,
pageurl: page.url(),
});
case "image":
return await solveImageCaptcha(page);
default:
return null;
}
}
用 Playwright 处理图片验证码
对传统图片验证码:截图元素、转成 base64、用 base64 方法提交,再把结果填进输入框。
async function solveImageCaptcha(page) {
const captchaImg = page.locator(
'img.captcha, img[alt*="captcha"], img[src*="captcha"]'
).first();
// Screenshot the CAPTCHA element
const imgBuffer = await captchaImg.screenshot();
const imgBase64 = imgBuffer.toString("base64");
// Solve via CaptchaAI
const answer = await solveCaptcha("base64", { body: imgBase64 });
// Type the answer
const input = page.locator(
'input[name="captcha"], input[name="code"], input.captcha-input'
).first();
await input.fill(answer);
return answer;
}
通过路由拦截提取验证码参数
有些参数(如 GeeTest 的 gt、challenge)不在 DOM 里,而由接口返回。
这时监听网络响应来提取:
- 监听
response事件 - 匹配含
geetest/gt=的响应,读出gt与challenge
async function interceptCaptchaRoutes(page, url) {
const captchaParams = {};
// Intercept responses
page.on("response", async (response) => {
const respUrl = response.url();
// GeeTest parameters
if (respUrl.includes("geetest") || respUrl.includes("gt=")) {
try {
const data = await response.json();
if (data.gt) {
captchaParams.type = "geetest";
captchaParams.gt = data.gt;
captchaParams.challenge = data.challenge;
}
} catch {}
}
});
await page.goto(url, { waitUntil: "networkidle" });
return captchaParams;
}
完整的自动化封装类
把上面的能力整合成一个类,几行代码就能跑通登录流程。
loginWithCaptcha 是可复用的入口,它会依次:
- 打开页面并填写表单
- 自动检测并识别验证码
- 写回 token 后提交
const { chromium } = require("playwright");
class PlaywrightAutomation {
#apiKey;
#browser;
#context;
#page;
constructor(apiKey) {
this.#apiKey = apiKey;
}
async start(headless = false) {
this.#browser = await chromium.launch({
headless,
args: ["--no-sandbox"],
});
this.#context = await this.#browser.newContext({
userAgent:
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/120.0.0.0 Safari/537.36",
viewport: { width: 1920, height: 1080 },
});
await this.#context.addInitScript(() => {
Object.defineProperty(navigator, "webdriver", { get: () => undefined });
});
this.#page = await this.#context.newPage();
}
async stop() {
await this.#browser?.close();
}
async navigate(url) {
await this.#page.goto(url, { waitUntil: "networkidle" });
}
async fillForm(fields) {
for (const [selector, value] of Object.entries(fields)) {
await this.#page.fill(selector, value);
}
}
async solveCaptcha() {
return await detectAndSolve(this.#page);
}
async submit(selector = 'button[type="submit"]') {
await this.#page.click(selector);
await this.#page.waitForLoadState("networkidle");
return this.#page.url();
}
async loginWithCaptcha(url, fields, submitSelector) {
await this.navigate(url);
await this.fillForm(fields);
const token = await this.solveCaptcha();
if (token) {
// Inject token
await this.#page.evaluate((t) => {
const re = document.getElementById("g-recaptcha-response");
if (re) re.value = t;
document
.querySelectorAll('[name="cf-turnstile-response"]')
.forEach((el) => (el.value = t));
}, token);
}
return await this.submit(submitSelector);
}
get page() {
return this.#page;
}
}
// Usage
const bot = new PlaywrightAutomation("YOUR_API_KEY");
await bot.start();
try {
const result = await bot.loginWithCaptcha(
"https://staging.example.com/qa-login",
{
"#email": "user@example.com",
"#password": "pass123",
},
"#login-btn"
);
console.log(`Redirected to: ${result}`);
} finally {
await bot.stop();
}
Playwright 与 Puppeteer 对比
选型参考:Playwright 默认配置更省心,原生支持 TypeScript 与多浏览器。
| 特性 | Playwright | Puppeteer |
|---|---|---|
| 多浏览器支持 | Chromium、Firefox、WebKit | 仅 Chromium |
| API 风格 | 基于 Locator | 基于选择器 |
| 自动等待 | 内置 | 需手动等待 |
| 网络拦截 | 基于路由 | 基于请求 |
| 默认配置 | 合理的开箱默认 | 需额外插件 |
| TypeScript | 原生支持 | 社区类型定义 |
故障排查
常见问题速查:
| 症状 | 原因 | 处理方式 |
|---|---|---|
page.evaluate 返回 null |
元素尚未加载 | 先调用 waitForSelector 等待 |
| 检测不到 Turnstile | 页面加载后才由 JS 注入 | 等待 .cf-turnstile 出现 |
| 写回 token 后表单不提交 | 没有触发回调 | 显式调用 reCAPTCHA 回调 |
| 浏览器被识别 | 缺少初始化脚本 | 补上 webdriver 覆盖脚本 |
networkidle 超时 |
页面有长轮询请求 | 改用 domcontentloaded |
常见问题
CaptchaAI 支持哪些 GeeTest 版本?
目前支持 GeeTest(极验)v3,通过 geetest 方法调用。
GeeTest v4 官方标注为即将支持,暂不可用;hCaptcha 与 FunCaptcha 也不支持。
国内测试 reCAPTCHA 加载不出来怎么办?
reCAPTCHA 依赖 Google 托管的脚本,在部分网络下页面可能渲染不全。
好在识别不依赖页面渲染:拿到 sitekey 和 pageurl 提交给 API,token 返回后写回页面即可。
Playwright 并发跑多个任务,应该选哪个套餐?
CaptchaAI 按并发线程计费,每个同时进行的识别任务占用一个线程。
轻量脚本用 BASIC($15/月,5 线程)够用;并发更高可升级到 ADVANCE($90/月,50 线程),按实际并发数选择。
写回 token 后表单还是没提交,是什么原因?
reCAPTCHA 需显式触发回调,仅设 g-recaptcha-response 值不够;Turnstile 要确认 cf-turnstile-response 已赋值。
先检查回调是否被调用,再看提交按钮是否被脚本禁用。
headless 模式会影响识别成功率吗?
不会。识别由 CaptchaAI 在服务端完成,与浏览器是否有界面无关。
把 launch() 的 headless 设为 true 即可在 CI 等无界面环境运行。
小结
Node.js + Playwright + CaptchaAI 组成一套现代自动化方案:自动检测类型、路由拦截取参、多类验证码统一识别。PlaywrightAutomation 类把「登录 + 验证码」流程封装到一起,复制即可跑通。