Integrations

Crawlee + CaptchaAI:现代抓取框架集成

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 处理静态页面验证码

静态页面场景最简单,整个流程可以拆成四步:

  1. CheerioCrawler 请求页面 HTML
  2. 检测到 [data-sitekey] 时调用 CaptchaAI 的 in.php 提交任务
  3. 轮询 res.php,拿到 token
  4. 把 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,处理步骤也多了两步:

  1. 等待页面渲染完成,检测 [data-sitekey]
  2. 识别到 sitekey 后调用 solveCaptcha
  3. 把 token 写回页面 DOM
  4. 手动触发 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,减少重复识别

高频抓取同一站点时,没必要每个请求都重新识别一次验证码,思路是:

  1. 检测到 .captcha-container 时识别验证码
  2. 把 token 写入 session.userData
  3. 后续请求先复用 session 里的 token
  4. 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

该文章已禁用评论。