Use Cases

使用 Node.js 进行验证码抓取:完整教程

用 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);

有些站点要求先建立会话再提交表单。普通 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,用于维持后续会话。

相关阅读

该文章已禁用评论。