在 Playwright 和 Puppeteer 中识别 PopularCaptcha(Node.js)
一旦出现 PopularCaptcha,浏览器自动化就会卡住。从脚本里点击图片网格既慢又脆弱。从识别 API 获取 token 并把它放入页面,只需一次请求,脚本的其余部分就可以照常继续。
本指南涵盖使用 Node.js 的 Playwright 和 Puppeteer。两者的流程相同,示例使用无代理识别,由 Scarnox 在自己的网络上完成。PopularCaptcha 是 Scarnox 目前识别的类型。其他验证码类型即将支持,因此下面的代码仅适用于 PopularCaptcha。
流程
- 从页面中读取 sitekey。
- 通过一次阻塞的 POST 请求向 Scarnox 获取 token。
- 把 token 填入响应字段,或交给网站的 JavaScript 回调,然后提交表单。
设置密钥
注册账户,免费试用额度,无需信用卡。请把密钥保存在环境变量中,绝不要写在测试代码里:
export SCARNOX_KEY="scarnox_YOURAPIKEY"
Node 18 及更高版本内置了 fetch,因此调用 API 无需额外的包。
Playwright
import { chromium } from "playwright";
const API_KEY = process.env.SCARNOX_KEY;
async function solve(sitekey, url) {
const r = await fetch("https://api.scarnox.com/api/tasks/create", {
method: "POST",
headers: { Authorization: `Bearer ${API_KEY}`, "Content-Type": "application/json" },
body: JSON.stringify({ captcha_type: "PopularCaptchaTaskProxyless", site_key: sitekey, site_url: url }),
});
const data = await r.json();
if (data.errorId !== 0) throw new Error(`${data.errorCode}: ${data.errorDescription}`);
return data.solution.token;
}
const browser = await chromium.launch();
const page = await browser.newPage();
await page.goto("https://example.com/signup");
const sitekey = await page.getAttribute("[data-sitekey]", "data-sitekey");
const token = await solve(sitekey, page.url());
// 把 token 写入网站可能读取的两个响应字段。
await page.evaluate((t) => {
for (const name of ["h-captcha-response", "g-recaptcha-response"]) {
document.querySelectorAll(`[name="${name}"]`).forEach((el) => { el.value = t; });
}
}, token);
await page.fill("#email", "you@example.com");
await page.click("button[type=submit]");
注意 site_url 来自 page.url()。那是控件所在的页面,也是识别器需要的 URL。识别是在页面加载完成之后进行的,所以 sitekey 已经在 DOM 中了。
Puppeteer
Puppeteer 与 Playwright 的区别仅在于选择器 API。复用上面的 solve 函数,并替换页面调用:
const sitekey = await page.$eval("[data-sitekey]", (el) => el.dataset.sitekey);
const token = await solve(sitekey, page.url());
await page.evaluate((t) => {
document.querySelectorAll('[name="h-captcha-response"],[name="g-recaptcha-response"]')
.forEach((el) => { el.value = t; });
}, token);
当网站使用回调时
有些网站不读取隐藏字段,而是等待控件的 JavaScript 回调。找到控件 data-callback 属性中指定的函数,并用 token 调用它:
await page.evaluate((t) => {
const name = document.querySelector("[data-sitekey]")?.dataset.callback;
if (name && typeof window[name] === "function") window[name](t);
}, token);
如果网站两种方式都用,那就同时设置字段并调用回调。两者都做没有坏处。
隐形控件
隐形控件没有复选框。网站从自己的代码中启动它,通常是在访客点击提交时。发送同样的请求,并将可选字段 is_invisible 设为 true:
body: JSON.stringify({
captcha_type: "PopularCaptchaTaskProxyless",
site_key: sitekey,
site_url: url,
is_invisible: true,
}),
隐形验证码指南说明了如何识别这类控件,以及如何把 token 回传给页面。
时机技巧
- 在提交之前立即获取 token。Token 在大约两分钟内过期。
- 为识别请求设置较长的超时时间,约 185 秒。它会阻塞,直到 token 准备好。
- 识别失败不收费,因此重试只花费时间。对临时性失败使用带退避的重试,参见错误处理与退避。
- 同时运行多个浏览器?请限制并发识别的数量,避免一批缓慢的任务拖慢其余的运行。
- 在
finally块中关闭浏览器。如果崩溃的测试留下了 Chromium 在运行,同一台机器上之后的每次识别都会变慢。
测试你自己的流程
在对更大的目标运行示例之前,先在你自己的表单上运行。检查三件事:token 被接受、表单成功提交,并且下一个页面出现。当流程端到端跑通之后,可以在每次运行中只做一次真实识别,并让 CI 保持通过。
免费试用
注册账户,免费试用额度,无需信用卡。这足以搭建并验证整个流程。大规模识别的费用,请查看当前定价。
调试被拒绝的 token
当网站拒绝 token 时,请只用一次请求和一次页面加载来复现流程,然后再做其他改动。请按以下顺序检查:
- 你发送的 sitekey 与你所加载页面上的控件一致。
site_url是控件所在的页面,而不是重定向后的目标页面。- Token 被写入了页面读取的字段,或者回调已经被调用。
- 提交发生在识别之后约两分钟之内。
在失败的时刻,用 await page.content() 保存页面 HTML。将它与一次成功的运行做比较,通常能一眼看出差异:sitekey 不同、字段缺失,或者控件以另一种模式渲染。