Integrations

Crawlee + CaptchaAI : intégration du framework de scraping moderne

Crawlee gère les sessions, les proxys et les nouvelles tentatives à votre place, mais il ne résout pas les CAPTCHA. Dès qu'un reCAPTCHA v2 s'affiche, le crawler reste bloqué sur la page. La solution : déléguez la résolution à CaptchaAI via un appel API, récupérez le token, puis réinjectez-le dans le formulaire avant de reprendre l'extraction. Voici ce branchement dans un projet Crawlee (Node.js), de CheerioCrawler aux crawlers qui pilotent un navigateur.


Pourquoi associer CaptchaAI à Crawlee

Crawlee fournit l'ossature du crawler ; CaptchaAI comble la brique qui lui manque face aux pages protégées :

Capacité de Crawlee Ce que CaptchaAI y ajoute
Gestion de session intégrée Réutilisez des empreintes cohérentes une fois le CAPTCHA résolu
Nouvelles tentatives automatiques Relancez les requêtes échouées après obtention du token
Rotation des proxys Faites tourner vos proxys résidentiels pendant la résolution
File d'attente des requêtes Résolvez les CAPTCHA en parallèle du crawl

Le principe reste le même quel que soit le crawler que vous choisissez : détecter la présence d'un data-sitekey sur la page, envoyer ce sitekey à CaptchaAI, attendre le token, puis le remettre dans le flux normal de Crawlee. La détection et la résolution vivent dans le requestHandler ; le reste de votre logique d'extraction ne change pas. C'est ce qui rend l'intégration peu intrusive : vous ajoutez une étape conditionnelle, sans réécrire votre crawler.


Intégration de base avec CheerioCrawler

CheerioCrawler parse le HTML sans navigateur : c'est l'option la plus rapide et la plus économe en ressources pour les pages statiques. La fonction solveCaptcha envoie le sitekey à l'endpoint in.php avec method=userrecaptcha, attend une première fois, puis interroge res.php en boucle jusqu'à recevoir le token ou atteindre le timeout. Tant que la réponse vaut CAPCHA_NOT_READY, la résolution est encore en cours ; toute autre valeur d'erreur interrompt la boucle. Une fois le token obtenu, le requestHandler renvoie le formulaire avec la valeur g-recaptcha-response. Le maxConcurrency: 5 aligne le crawl sur cinq résolutions simultanées — retenez ce chiffre, il détermine le nombre de threads à prévoir côté CaptchaAI.

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',
    ]);
})();

PlaywrightCrawler pour les pages rendues en JavaScript

Beaucoup de sites n'exposent le widget reCAPTCHA qu'après exécution du JavaScript : CheerioCrawler, qui ne rend pas la page, ne le verra jamais. Passez alors à PlaywrightCrawler. Comme il pilote un navigateur complet, vous attendez que la page soit stable (networkidle), lisez le sitekey dans le DOM, puis — une fois le token obtenu — vous l'écrivez dans le champ caché g-recaptcha-response et déclenchez le data-callback du widget. Cette étape de callback est souvent oubliée : sans elle, le token est bien présent mais le site ne considère pas le défi comme résolu, et la soumission échoue.

const { PlaywrightCrawler } = require('crawlee');

const crawler = new PlaywrightCrawler({
    maxConcurrency: 3,
    requestHandlerTimeoutSecs: 180,
    launchContext: {
        launchOptions: {
            headless: true,
            args: ['--disable-blink-features=AutomationControlled'],
        },
    },

    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}`);
    },
});

Résolution CAPTCHA au niveau de la session

Le pool de sessions de Crawlee garde des identités stables (cookies, empreinte, proxy) entre les requêtes, avec maxPoolSize sessions réutilisables jusqu'à maxUsageCount fois. En stockant le token et son horodatage dans session.userData, vous suivez l'état de la résolution au niveau de la session : utile pour savoir quelles sessions ont franchi un défi et pour éviter de relancer un solve inutile. Gardez toutefois à l'esprit qu'un token reCAPTCHA v2 est à usage unique et de courte durée ; l'horodatage sert surtout à décider quand une session doit repasser par une résolution, pas à rejouer un ancien 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`);
    },
});

Dépannage rapide

Problème Cause probable Correctif
Le token est refusé à la soumission Le TTL du token reCAPTCHA v2 (environ 2 min) a expiré Résolvez juste avant d'envoyer le formulaire, pas en avance
CAPCHA_NOT_READY en boucle jusqu'au timeout La résolution dépasse la fenêtre de polling Augmentez le nombre d'itérations ou l'intervalle
Le sitekey ressort null sous Playwright Le widget est dans une iframe ou chargé tardivement Attendez le sélecteur avant page.evaluate
Le crawl sature ou ralentit maxConcurrency dépasse votre nombre de threads Alignez maxConcurrency sur le nombre de threads du plan

Déploiement et conformité

Un crawler Crawlee se déploie aussi bien sur un acteur Apify que sur une instance OVHcloud ou Scaleway proche de vos cibles ; une région comme eu-west-3 (Paris) réduit la latence sur des sites européens. Définissez CAPTCHAAI_API_KEY comme variable d'environnement plutôt qu'en dur dans le code, et journalisez les résolutions pour suivre votre consommation de threads.

Côté données, restez sobre : limitez la collecte aux informations réellement nécessaires, respectez le robots.txt et les conditions d'utilisation des sites visés, et vérifiez vos obligations RGPD avant de scraper des données personnelles. La minimisation des données n'est pas qu'une exigence légale pour les lecteurs en France, en Belgique ou au Québec — c'est aussi ce qui rend un pipeline de scraping plus simple à maintenir.


FAQ

Comment injecter le token reCAPTCHA v2 dans une page pilotée par Playwright ?

Écrivez le token dans le champ caché g-recaptcha-response, puis déclenchez le data-callback du widget si la page en définit un. Sans ce callback, le formulaire ignore souvent le token même lorsqu'il est présent.

Combien de threads CaptchaAI prévoir pour un crawl parallèle ?

Chaque CAPTCHA en cours occupe un thread. Avec maxConcurrency: 5 et un CAPTCHA par page, prévoyez au moins 5 threads. Le plan BASIC ($15/mois, 5 threads) couvre ce cas ; STANDARD ($30/mois, 15 threads) convient à un crawl plus large. La facturation est au thread, avec résolutions illimitées.

Que faire si la résolution dépasse le timeout du requestHandler ?

Accordez de la marge : requestHandlerTimeoutSecs: 180 laisse le temps à un reCAPTCHA v2 de se résoudre, là où une valeur trop basse ferait échouer la requête avant le retour du token. Si vous voyez souvent des timeouts, augmentez cette valeur ou réduisez maxConcurrency pour éviter de saturer vos threads.

CaptchaAI gère-t-il autre chose que reCAPTCHA v2 pour mes crawlers ?

Oui. Le même schéma envoyer puis interroger fonctionne pour reCAPTCHA v3, Cloudflare Turnstile, Cloudflare Challenge et GeeTest v3 ; seuls le method et les paramètres changent. hCaptcha et FunCaptcha ne sont pas pris en charge.


Guides connexes


Ajoutez la résolution CAPTCHA à vos crawlers Crawlee — récupérez votre clé CaptchaAI.

Les commentaires sont désactivés pour cet article.