Crawlee 本身不做验证码识别——遇到 reCAPTCHA v2、Turnstile 或 GeeTest v3 时,爬虫会直接卡在挑战页面上,除非接入外部识别服务。CaptchaAI 提供的正是这一层:把 sitekey 提交给接口,几秒到几十秒后拿到 token,再交给 CheerioCrawler 或 PlaywrightCrawler。下面按静态页面、JS 渲染页面、会话池缓存三种场景,给出可用的 Node.js 代码。
Crawlee 遇到验证码时缺的那一环
Crawlee 由 Apify 团队维护,专注抓取层面的工程问题——请求队列、会话池、自动重试、代理轮换,这些都是开箱即用的。但验证码识别不在它的能力范围内,需要单独接入。CaptchaAI 补上的正是这一块:
| 能力 | Crawlee 自带 | 接入 CaptchaAI 后 |
|---|---|---|
| 会话管理 | 内置 session pool | 识别出的 token 可写入 session,跨请求复用 |
| 失败重试 | 自动重试失败请求 | 验证码识别失败可单独重试,不拖累整个任务 |
| 代理轮换 | 支持代理池 | 可与 CaptchaAI 的代理支持配合使用 |
| 请求队列 | 内置队列调度 | 验证码识别任务可与抓取请求一起排队处理 |
国内环境的两个实际问题
在国内网络环境跑 Crawlee 项目,有两点值得留意:
- 装依赖可配置国内镜像加速,例如
npm config set registry https://registry.npmmirror.com - reCAPTCHA 挑战脚本由 Google 提供,国内网络访问不稳定是常见现象,偶尔会看到页面元素加载超时——这属于网络连通性问题,与 CaptchaAI 的识别逻辑无关
用 CheerioCrawler 处理静态页面验证码
静态页面场景最简单,整个流程可以拆成四步:
- CheerioCrawler 请求页面 HTML
- 检测到
[data-sitekey]时调用 CaptchaAI 的in.php提交任务 - 轮询
res.php,拿到 token - 把 token 塞进表单字段,提交页面
完整示例:
const { CheerioCrawler } = require('crawlee');
const https = require('https');
const API_KEY = process.env.CAPTCHAAI_API_KEY;
async function solveCaptcha(sitekey, pageurl) {
// Submit task
const submitData = new URLSearchParams({
key: API_KEY,
method: 'userrecaptcha',
googlekey: sitekey,
pageurl: pageurl,
json: '1',
});
const submitResp = await fetch('https://ocr.captchaai.com/in.php', {
method: 'POST',
body: submitData,
});
const submitResult = await submitResp.json();
if (submitResult.status !== 1) {
throw new Error(`Submit error: ${submitResult.request}`);
}
const taskId = submitResult.request;
// Poll for result
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=${API_KEY}&action=get&id=${taskId}&json=1`
);
const pollResult = await pollResp.json();
if (pollResult.status === 1) return pollResult.request;
if (pollResult.request !== 'CAPCHA_NOT_READY') {
throw new Error(`Solve error: ${pollResult.request}`);
}
await new Promise(r => setTimeout(r, 5000));
}
throw new Error('Solve timeout');
}
// Crawlee spider with CAPTCHA handling
const crawler = new CheerioCrawler({
maxConcurrency: 5,
requestHandlerTimeoutSecs: 180,
async requestHandler({ request, $, log }) {
// Check if page has CAPTCHA
const captchaDiv = $('[data-sitekey]');
if (captchaDiv.length > 0) {
const sitekey = captchaDiv.attr('data-sitekey');
log.info(`CAPTCHA found on ${request.url}, solving...`);
const token = await solveCaptcha(sitekey, request.url);
log.info('CAPTCHA solved, submitting form');
// Submit form with token
const formData = new URLSearchParams({
'g-recaptcha-response': token,
});
const resp = await fetch(request.url, {
method: 'POST',
body: formData,
});
const html = await resp.text();
// Parse the result page...
}
// Extract data
const title = $('title').text();
const data = $('table tr').map((i, row) => ({
col1: $(row).find('td:eq(0)').text().trim(),
col2: $(row).find('td:eq(1)').text().trim(),
})).get();
log.info(`Scraped ${data.length} rows from ${request.url}`);
},
failedRequestHandler({ request, log }) {
log.error(`Failed: ${request.url}`);
},
});
// Run
(async () => {
await crawler.run([
'https://example.com/page1',
'https://example.com/page2',
]);
})();
提示:轮询间隔是 5 秒,超时上限 24 次(约 2 分钟),对 reCAPTCHA v2 来说足够宽裕——多数任务在 15–30 秒内出结果。生产环境建议把
API_KEY放进环境变量,不要写死在代码里。
JS 渲染页面:PlaywrightCrawler 的验证码处理
有些页面要等浏览器渲染完成才会触发验证码,这种情况换成 PlaywrightCrawler,处理步骤也多了两步:
- 等待页面渲染完成,检测
[data-sitekey] - 识别到 sitekey 后调用
solveCaptcha - 把 token 写回页面 DOM
- 手动触发 reCAPTCHA 的回调函数,再提交表单
完整代码:
const { PlaywrightCrawler } = require('crawlee');
const crawler = new PlaywrightCrawler({
maxConcurrency: 3,
requestHandlerTimeoutSecs: 180,
launchContext: {
launchOptions: {
headless: true,
args: ['--no-sandbox'],
},
},
async requestHandler({ request, page, log }) {
await page.goto(request.url, { waitUntil: 'networkidle' });
// Check for reCAPTCHA
const sitekey = await page.evaluate(() => {
const el = document.querySelector('[data-sitekey]');
return el ? el.getAttribute('data-sitekey') : null;
});
if (sitekey) {
log.info(`CAPTCHA detected, solving for ${request.url}`);
const token = await solveCaptcha(sitekey, request.url);
// Inject token
await page.evaluate((t) => {
const ta = document.querySelector('[name="g-recaptcha-response"]');
if (ta) {
ta.style.display = 'block';
ta.value = t;
}
// Trigger callback
const widget = document.querySelector('.g-recaptcha');
if (widget) {
const cb = widget.getAttribute('data-callback');
if (cb && typeof window[cb] === 'function') {
window[cb](t);
}
}
}, token);
await page.click('button[type="submit"]');
await page.waitForNavigation({ waitUntil: 'networkidle' });
}
// Extract data
const title = await page.title();
const content = await page.textContent('body');
log.info(`Page: ${title}, length: ${content.length}`);
},
});
提示:如果把 token 写入页面之后表单没有反应,多数情况是页面用了自定义
data-callback。示例代码里已经处理了这种情况——先取出.g-recaptcha上声明的回调函数名,再手动调用一次,通常就能触发提交逻辑。
会话池缓存 token,减少重复识别
高频抓取同一站点时,没必要每个请求都重新识别一次验证码,思路是:
- 检测到
.captcha-container时识别验证码 - 把 token 写入
session.userData - 后续请求先复用 session 里的 token
- token 失效、验证码再次出现时才重新识别
示例:
const { CheerioCrawler, Session } = require('crawlee');
const crawler = new CheerioCrawler({
useSessionPool: true,
sessionPoolOptions: {
maxPoolSize: 10,
sessionOptions: {
maxUsageCount: 50,
},
},
async requestHandler({ request, $, session, log }) {
// If blocked, solve CAPTCHA and mark session as usable
if ($('.captcha-container').length > 0) {
const sitekey = $('[data-sitekey]').attr('data-sitekey');
const token = await solveCaptcha(sitekey, request.url);
// Store token in session for subsequent requests
session.userData = session.userData || {};
session.userData.captchaToken = token;
session.userData.tokenTime = Date.now();
log.info('CAPTCHA solved, session updated');
}
// Normal scraping
const items = $('div.item').map((i, el) => ({
name: $(el).find('.name').text().trim(),
price: $(el).find('.price').text().trim(),
})).get();
log.info(`Found ${items.length} items`);
},
});
提示:token 有效期通常较短,多数站点几分钟内就会失效,所以
userData里最好带上tokenTime用来判断要不要重新识别。示例代码已经记录了这个时间戳,接下来只需要在请求前加一层过期检查。
常见问题
Crawlee 自己能识别验证码吗?
不能。Crawlee 负责会话管理、代理、请求队列和自动重试,但验证码识别需要接入 CaptchaAI 这类识别服务。
PlaywrightCrawler 把 token 写入页面后表单没反应,是什么原因?
最常见的原因是页面用了自定义 data-callback,只写入隐藏字段不会自动触发提交。参考上面的示例:取出 .g-recaptcha 声明的回调函数名,再手动调用一次即可。
除了 reCAPTCHA v2,Crawlee 项目里 CaptchaAI 还能识别哪些验证码?
CaptchaAI 还支持 Cloudflare Turnstile 和 GeeTest v3,接入思路类似,只是提交任务的参数字段不同,具体参考 API 文档。
在 Apify 上部署 Crawlee actor,API Key 应该怎么管理?
把 API Key 设置成 Apify 环境变量,不要写进代码或提交到仓库。actor 运行时用 process.env 读取,和本地用 .env 文件的做法一致。
相关指南
给 Crawlee 项目接入验证码识别 – 获取你的 CaptchaAI API Key。