Integrations

Apify + CaptchaAI:云抓取平台集成

在 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

主流程按顺序做四件事:

  1. 打开目标页面;
  2. 检测页面里 reCAPTCHA 的 sitekey;
  3. 调用 CaptchaAI 识别,把 token 注入表单后提交;
  4. 抓取数据,写入 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 自带的环境变量功能可以把它当密文存放。

三步接入

  1. 打开 Actor 设置 → Environment variables
  2. 新增变量:CAPTCHAAI_API_KEY = 你的密钥(勾选 Secret)
  3. 代码里读取: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

该文章已禁用评论。