用 Node.js 抓取页面时,最常见的中断点就是目标站点突然弹出验证码,脚本直接卡死。思路并不复杂:用 axios 拿到页面内容,把 sitekey 交给 CaptchaAI API 识别,拿到 token 后提交回去,抓取流程继续跑。本文用一套完整的 Node.js 示例演示这个过程,覆盖 axios + cheerio 这套最常见的抓取组合。
环境准备与依赖
开始之前先确认好这几样,其中 CaptchaAI API Key 需要单独申请:
| 要求 | 细节 |
|---|---|
| Node.js 16+ | 建议搭配 npm 使用 |
| axios | npm install axios |
| cheerio | npm install cheerio |
| CaptchaAI API Key | 在 CaptchaAI 官网 注册后获取 |
如果 npm install 很慢,可以换成国内镜像:npm install --registry=https://registry.npmmirror.com。
封装 CaptchaAI 验证码识别模块
把提交任务和轮询结果封装成一个模块,后面的抓取逻辑直接复用即可。CaptchaSolver 支持 reCAPTCHA v2、reCAPTCHA v3 和 Cloudflare Turnstile:提交后每 5 秒轮询一次 res.php,最长等待 5 分钟,超时或出错都会抛出异常。
// captcha-solver.js
const axios = require("axios");
class CaptchaSolver {
constructor(apiKey) {
this.apiKey = apiKey;
this.baseUrl = "https://ocr.captchaai.com";
}
async _submit(params) {
params.key = this.apiKey;
const resp = await axios.get(`${this.baseUrl}/in.php`, { params });
if (!resp.data.startsWith("OK|")) {
throw new Error(`Submit error: ${resp.data}`);
}
return resp.data.split("|")[1];
}
async _poll(taskId, timeout = 300000) {
const deadline = Date.now() + timeout;
while (Date.now() < deadline) {
await new Promise((r) => setTimeout(r, 5000));
const resp = await axios.get(`${this.baseUrl}/res.php`, {
params: { key: this.apiKey, action: "get", id: taskId },
});
if (resp.data === "CAPCHA_NOT_READY") continue;
if (resp.data.startsWith("OK|")) return resp.data.split("|")[1];
throw new Error(`Solve error: ${resp.data}`);
}
throw new Error("Solve timed out");
}
async solveRecaptchaV2(siteKey, pageUrl) {
const taskId = await this._submit({
method: "userrecaptcha",
googlekey: siteKey,
pageurl: pageUrl,
});
return this._poll(taskId);
}
async solveRecaptchaV3(siteKey, pageUrl, action = "verify") {
const taskId = await this._submit({
method: "userrecaptcha",
googlekey: siteKey,
pageurl: pageUrl,
version: "v3",
action,
});
return this._poll(taskId);
}
async solveTurnstile(siteKey, pageUrl) {
const taskId = await this._submit({
method: "turnstile",
sitekey: siteKey,
pageurl: pageUrl,
});
return this._poll(taskId);
}
}
module.exports = CaptchaSolver;
实战:抓取带 reCAPTCHA 验证的页面
整体流程分四步:加载页面、提取 sitekey、拿 token、带着 token 重新提交表单。下面以一个搜索页为例,页面在提交关键词前会先校验 reCAPTCHA v2:
const axios = require("axios");
const cheerio = require("cheerio");
const CaptchaSolver = require("./captcha-solver");
const solver = new CaptchaSolver("YOUR_API_KEY");
async function scrapeProtectedPage(url) {
// Step 1: Load the page
const { data: html } = await axios.get(url, {
headers: {
"User-Agent":
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
},
});
const $ = cheerio.load(html);
// Step 2: Extract site key
const siteKey = $(".g-recaptcha").attr("data-sitekey");
if (!siteKey) {
console.log("No CAPTCHA found, page loaded directly");
return html;
}
console.log("Site key found:", siteKey);
// Step 3: Solve the CAPTCHA
const token = await solver.solveRecaptchaV2(siteKey, url);
console.log("Token received:", token.substring(0, 50));
// Step 4: Submit with the token
const result = await axios.post(
url,
new URLSearchParams({
"g-recaptcha-response": token,
q: "search query",
}),
{
headers: {
"Content-Type": "application/x-www-form-urlencoded",
"User-Agent":
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
},
}
);
return result.data;
}
并发抓取多个页面,同时控制节奏
批量抓取时,用任务队列配合固定数量的 worker,把并发收敛在目标站点能接受的范围内。下面 3 个 worker 轮流取 URL、各自提交任务并等待结果,某个 URL 慢不会拖慢其他 worker。并发数建议从 3–5 开始测试,再按响应情况调整:
async function scrapePages(urls, siteKey, concurrency = 3) {
const results = [];
const queue = [...urls];
const worker = async () => {
while (queue.length > 0) {
const url = queue.shift();
try {
const token = await solver.solveRecaptchaV2(siteKey, url);
const { data } = await axios.post(
url,
new URLSearchParams({ "g-recaptcha-response": token }),
{
headers: {
"User-Agent":
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
},
}
);
results.push({ url, data, success: true });
console.log(`Scraped: ${url}`);
} catch (err) {
results.push({ url, error: err.message, success: false });
console.error(`Failed: ${url} - ${err.message}`);
}
}
};
// Run workers concurrently
const workers = Array(concurrency)
.fill(null)
.map(() => worker());
await Promise.all(workers);
return results;
}
// Usage
const urls = [
"https://example.com/page/1",
"https://example.com/page/2",
"https://example.com/page/3",
];
const results = await scrapePages(urls, "6Le-wvkS...", 3);
处理会话 Cookie
有些站点要求先建立会话再提交表单。普通 axios 实例不会自动保留 cookie,需要配合 cookie jar:
const { wrapper } = require("axios-cookiejar-support");
const { CookieJar } = require("tough-cookie");
const jar = new CookieJar();
const client = wrapper(
axios.create({
jar,
headers: {
"User-Agent":
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
},
})
);
async function scrapeWithSession(url, siteKey) {
// Initial page load sets cookies
await client.get(url);
// Solve CAPTCHA
const token = await solver.solveRecaptchaV2(siteKey, url);
// Submit with maintained cookies
const result = await client.post(
url,
new URLSearchParams({ "g-recaptcha-response": token })
);
return result.data;
}
用 Cheerio 提取结构化数据
用 Cheerio 的 jQuery 风格选择器提取数据,比正则表达式稳定得多,页面小改动也不容易整段失效:
function parseResults(html) {
const $ = cheerio.load(html);
const items = [];
$(".result-item").each((_, el) => {
items.push({
title: $(el).find(".title").text().trim(),
url: $(el).find("a").attr("href"),
description: $(el).find(".description").text().trim(),
});
});
return items;
}
一个真实场景:出海电商的价格监控脚本
某出海电商团队用 Node.js 定时抓取海外商品页做价格监控,其中几个站点在高频访问后会弹出 Turnstile。接入 CaptchaSolver 后,脚本识别通过即可继续请求,不需要人工过验证码。抓取范围只限于页面公开展示的商品名称和价格,并遵循目标站点的 robots 协议——这也是国内团队做数据采集时对照《网络安全法》《数据安全法》精神通常会把控的基本边界。
常见报错与排查
几个最容易踩的坑:
| 问题 | 原因 | 处理方式 |
|---|---|---|
CAPTCHA_NOT_READY 一直循环 |
site key 错误,或识别耗时较长 | 检查 site key 是否正确;适当调大超时时间 |
POST 请求返回 403 Forbidden |
缺少必要的 cookie 或请求头 | 补上会话 cookie;添加 Referer 请求头 |
| Cheerio 提取不到目标元素 | 页面内容由 JS 动态渲染 | 改用 Puppeteer 处理需要 JS 渲染的站点 |
请求返回 ECONNREFUSED |
目标站点触发了限流 | 增大请求间隔;用 QA 测试会话控制并发节奏 |
常见问题
Node.js 爬虫什么时候该用 Puppeteer,而不是 axios + cheerio?
目标页面是标准 HTML、表单提交也是普通 POST 时,axios + cheerio 更轻量、更快。只有页面依赖 JS 渲染或复杂交互时才需要 Puppeteer,资源开销明显更高。
CaptchaAI 支持哪些验证码类型?起步套餐够用吗?
支持 reCAPTCHA v2/v3、Cloudflare Turnstile、GeeTest v3 等类型,按并发线程计费而非按次数:BASIC $15/月、5 线程,线程内识别次数不限,小规模抓取通常够用,量大了再升级到 STANDARD($30/月,15 线程)。
并发抓取时怎么避免被目标站点限流?
并发从 3–5 个 worker 起步,出现 ECONNREFUSED 或 403 就调低;同时给请求加上合理间隔,不要把并发和频率都拉满。
遇到 Cloudflare Turnstile 和完整挑战页面,要怎么区分处理?
只嵌入 Turnstile 组件时,用 solver.solveTurnstile() 就够了;遇到完整的 Cloudflare 挑战验证解决方案对应的挑战页面,识别通过后返回 qa_session_cookie,用于维持后续会话。