在 Apify 上跑的 Crawlee Actor 抓国际站点时,reCAPTCHA v2 是最常见的拦路虎——页面刚加载完,表单准备提交,验证码先弹了出来。思路很直接:Actor 检测到 sitekey 后调用 CaptchaAI 的 API 完成识别,reCAPTCHA v2 通常在 60 秒以内出结果,成功率也比较高;拿到 token 写回页面再提交表单,整个流程可以完全自动化。下面从 Input Schema、Actor 代码,到环境变量管理、代理配置和线程数选型,把这套集成方案讲清楚。
Actor 输入设计与验证码识别逻辑
Actor 的 Input Schema 要提前留出 CaptchaAI 的 API Key 字段,方便在 Apify 控制台里以密文形式保存,不用写进代码仓库:
{
"title": "CAPTCHA Scraper Input",
"type": "object",
"properties": {
"startUrls": {
"title": "Start URLs",
"type": "array",
"editor": "requestListSources"
},
"captchaaiApiKey": {
"title": "CaptchaAI API Key",
"type": "string",
"isSecret": true
},
"maxConcurrency": {
"title": "Max Concurrency",
"type": "integer",
"default": 3
}
},
"required": ["startUrls", "captchaaiApiKey"]
}
Actor 代码:检测 sitekey、识别验证码、提交 token
主流程按顺序做四件事:
- 打开目标页面;
- 检测页面里 reCAPTCHA 的 sitekey;
- 调用 CaptchaAI 识别,把 token 注入表单后提交;
- 抓取数据,写入 Apify 数据集。
下面的 CaptchaAISolver 类封装了提交任务和轮询结果的逻辑——提交后先等 15 秒,再每 5 秒轮询一次 res.php,直到拿到 token 或者超时退出:
const { Actor } = require('apify');
const { PlaywrightCrawler } = require('crawlee');
Actor.main(async () => {
const input = await Actor.getInput();
const { startUrls, captchaaiApiKey, maxConcurrency = 3 } = input;
const solver = new CaptchaAISolver(captchaaiApiKey);
const crawler = new PlaywrightCrawler({
maxConcurrency,
requestHandlerTimeoutSecs: 180,
async requestHandler({ request, page, log }) {
await page.goto(request.url, { waitUntil: 'networkidle' });
// Check for CAPTCHA
const sitekey = await page.evaluate(() => {
const el = document.querySelector('[data-sitekey]');
return el ? el.getAttribute('data-sitekey') : null;
});
if (sitekey) {
log.info(`Solving CAPTCHA on ${request.url}`);
const token = await solver.solve(sitekey, request.url);
// Inject and submit
await page.evaluate((t) => {
document.querySelector('[name="g-recaptcha-response"]').value = t;
const cb = document.querySelector('.g-recaptcha')?.getAttribute('data-callback');
if (cb && window[cb]) window[cb](t);
}, token);
await page.click('button[type="submit"]');
await page.waitForNavigation({ timeout: 15000 });
}
// Extract data
const title = await page.title();
const items = await page.$$eval('.item', els =>
els.map(el => ({
name: el.querySelector('.name')?.textContent?.trim(),
price: el.querySelector('.price')?.textContent?.trim(),
url: el.querySelector('a')?.href,
}))
);
// Push to Apify dataset
await Actor.pushData({
url: request.url,
title,
items,
scrapedAt: new Date().toISOString(),
});
log.info(`Scraped ${items.length} items from ${request.url}`);
},
});
await crawler.run(startUrls);
});
class CaptchaAISolver {
constructor(apiKey) {
this.apiKey = apiKey;
}
async solve(sitekey, pageurl) {
const params = new URLSearchParams({
key: this.apiKey,
method: 'userrecaptcha',
googlekey: sitekey,
pageurl: pageurl,
json: '1',
});
const submitResp = await fetch('https://ocr.captchaai.com/in.php', {
method: 'POST',
body: params,
});
const submitResult = await submitResp.json();
if (submitResult.status !== 1) {
throw new Error(`Submit: ${submitResult.request}`);
}
const taskId = submitResult.request;
await new Promise(r => setTimeout(r, 15000));
for (let i = 0; i < 24; i++) {
const pollResp = await fetch(
`https://ocr.captchaai.com/res.php?key=${this.apiKey}&action=get&id=${taskId}&json=1`
);
const result = await pollResp.json();
if (result.status === 1) return result.request;
if (result.request !== 'CAPCHA_NOT_READY') {
throw new Error(`Solve: ${result.request}`);
}
await new Promise(r => setTimeout(r, 5000));
}
throw new Error('Timeout');
}
}
举个例子:如果你的 Actor 用来监控海外电商站点的价格和库存——跨境选品团队很常见的用法——maxConcurrency 设成 3 意味着最多 3 个页面并发抓取,对应也就最多同时有 3 个验证码在等待识别。CaptchaAI 按线程数计费,不按每次识别收费,线程数得跟并发量对得上:
| maxConcurrency | 建议套餐 | 线程数 | 价格 |
|---|---|---|---|
| ≤ 5 | BASIC | 5 线程 | $15/月 |
| ≤ 15 | STANDARD | 15 线程 | $30/月 |
| > 15 | ADVANCE | 50 线程 | $90/月 |
线程数不够,会成为整个 Actor 的瓶颈。
在 Apify 上安全存放 API Key
CaptchaAI 的 API Key 不建议直接写死在 Actor 代码里,Apify 自带的环境变量功能可以把它当密文存放。
三步接入
- 打开 Actor 设置 → Environment variables
- 新增变量:
CAPTCHAAI_API_KEY= 你的密钥(勾选 Secret) - 代码里读取:
process.env.CAPTCHAAI_API_KEY
// Alternative: use env var instead of input
const apiKey = input.captchaaiApiKey || process.env.CAPTCHAAI_API_KEY;
这样即使 Actor 的运行日志被别人看到,密钥也不会以明文形式暴露出来。
Apify Proxy 要不要跟 CaptchaAI 一起用
两者分工不一样:Apify Proxy 负责抓取请求本身(换 IP、绕开访问频率限制),CaptchaAI 负责识别验证码,二者并不冲突。
建议:抓取请求走 Apify Proxy,CaptchaAI 识别请求直连,不用额外套代理——多数场景下这是最省钱的组合。
const crawler = new PlaywrightCrawler({
proxyConfiguration: await Actor.createProxyConfiguration({
groups: ['RESIDENTIAL'],
}),
// ... rest of config
});
常见问题
Apify 免费套餐能调用 CaptchaAI 吗?大概要多少钱?
可以。CaptchaAI 是标准的外部 API 调用,跟 Apify 用的是哪个套餐无关。你的成本 = CaptchaAI 的线程费用(BASIC 低至 $15/月,5 线程)加上 Apify 本身的计算用量费用,两笔账分开算,互不影响。
抓海外电商站点时遇到的是 reCAPTCHA v3 而不是 v2,代码要改吗?
要改一小部分。reCAPTCHA v3 没有点击框,检测环节要变——不用再找 data-sitekey 元素等按钮点击,而是从页面的 grecaptcha.execute() 调用里取 sitekey 和 action 参数;提交任务时额外带上 version: 'v3' 和 action 字段,提交、轮询逻辑不用动,拿到 token 后按目标页面自己的方式回传给后端验证即可。
CaptchaAI 还不支持 GeeTest v4,国内站点能用吗?
GeeTest v4 目前是“即将支持”状态,还不能用;已经支持的是 GeeTest(极验)v3。如果你的 Actor 抓的是用 GeeTest 做验证码的国内站点,先确认对方用的是 v3 还是 v4,再决定要不要接入这一段逻辑。
requestHandlerTimeoutSecs 要留多少才够验证码识别?
至少 180 秒。CaptchaAI 识别 reCAPTCHA v2 通常在 60 秒以内完成,但加上排队、重试和页面本身的加载时间,180 秒是比较稳的下限;如果超时次数明显偏多,再往上调整即可。
多个 Actor 并发跑,CaptchaAI 的线程数会不会不够用?
够不够用,看总并发量而不是 Actor 数量。线程数就是同时能处理的验证码数量:3 个 Actor 各开 5 并发,等于总共最多 15 个验证码同时在识别,这时候 STANDARD($30/月,15 线程)刚好够用;并发量继续往上加,就得看 ADVANCE($90/月,50 线程)。
相关指南
部署你自己的验证码识别 Actor —— 立即使用 CaptchaAI。