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.