Un acteur Apify qui tombe sur un reCAPTCHA v2 en pleine navigation s'arrête net : la page reste bloquée, l'extraction renvoie du vide et le run se termine en erreur. La correction tient en une étape ciblée : insérez un appel à CaptchaAI dans le requestHandler, récupérez le token, injectez-le dans la page, puis laissez Crawlee reprendre son cours normal. Vous n'avez ni à réécrire l'acteur ni à sortir la logique de scraping de la plateforme.
Du point de vue de votre code, CaptchaAI se comporte comme un simple appel HTTP : vous envoyez le sitekey et l'URL de la page, vous interrogez le résultat quelques secondes, vous récupérez une valeur g-recaptcha-response. Ce guide montre où brancher cet appel dans un acteur Playwright, comment exposer la clé API proprement dans l'input, et comment articuler les proxys Apify avec la résolution pour ne pas payer deux fois le même trafic.
Où la résolution s'insère dans le cycle de l'acteur
Un acteur Apify basé sur Crawlee traite chaque URL dans un requestHandler. C'est exactement le bon endroit pour intercepter un défi CAPTCHA : la page est déjà chargée, Playwright est disponible, et vous avez le contrôle avant de lancer l'extraction. Le flux se résume à quatre temps : détecter le sitekey dans le DOM, déléguer la résolution à CaptchaAI, réinjecter le token retourné, puis poursuivre l'extraction comme si rien ne s'était passé.
L'intérêt de garder ce découpage, c'est qu'un acteur sans CAPTCHA sur sa route ne paie rien : la branche de résolution ne s'exécute que si un sitekey est présent. Vous ajoutez de la robustesse sans alourdir le cas nominal.
Configuration de l'acteur
L'input schema a un seul objectif : rendre l'acteur réutilisable depuis l'interface Apify sans toucher au code à chaque exécution. On y expose les URL de départ, la clé API (marquée secrète) et le niveau de concurrence.
Schéma d'entrée
{
"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"]
}
Le champ isSecret: true évite que la clé apparaisse en clair dans les logs ou l'historique de run. Le maxConcurrency par défaut à 3 est un point de départ prudent : chaque requête concurrente qui déclenche une résolution consomme un thread côté CaptchaAI, on y revient plus bas.
Code de l'acteur
Le handler ci-dessous cherche un attribut data-sitekey, appelle le solveur seulement s'il en trouve un, injecte le token dans le champ g-recaptcha-response, déclenche le callback éventuel, puis soumet le formulaire avant de reprendre l'extraction.
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');
}
}
La classe CaptchaAISolver reste volontairement minimale : in.php pour soumettre la tâche, une pause initiale de 15 secondes, puis une boucle d'interrogation de res.php toutes les 5 secondes tant que la réponse est CAPCHA_NOT_READY. Le method=userrecaptcha correspond à reCAPTCHA v2 ; pour un autre type pris en charge (reCAPTCHA v3, Cloudflare Turnstile), il suffit d'adapter la méthode et les paramètres, la mécanique de polling ne change pas.
Variables d'environnement sur Apify
Si vous ne voulez pas saisir la clé API à chaque run, stockez-la dans les variables d'environnement de l'acteur. C'est aussi la voie recommandée pour un acteur partagé ou planifié.
- Ouvrez les paramètres de l'acteur puis Environment variables.
- Ajoutez
CAPTCHAAI_API_KEYet marquez-la comme secrète. - Récupérez-la ensuite dans le code via
process.env.CAPTCHAAI_API_KEY.
// Alternative: use env var instead of input
const apiKey = input.captchaaiApiKey || process.env.CAPTCHAAI_API_KEY;
Ce repli sur l'input reste pratique en développement : vous testez avec une clé passée à la main, et la production lit la variable d'environnement.
Proxys Apify et CaptchaAI
Pour la grande majorité des cas, laissez Apify gérer les proxys de scraping et réservez CaptchaAI à la seule résolution. Cela sépare clairement les deux responsabilités : Apify route vos requêtes de navigation, CaptchaAI renvoie un token. Multiplier les proxys des deux côtés complique l'architecture et gonfle la facture sans bénéfice réel.
const crawler = new PlaywrightCrawler({
proxyConfiguration: await Actor.createProxyConfiguration({
groups: ['RESIDENTIAL'],
}),
// ... rest of config
});
Les proxys résidentiels d'Apify conviennent aux sites sensibles à la réputation d'IP ; pour des cibles plus tolérantes, un proxy datacenter suffit et coûte moins cher. La résolution du reCAPTCHA, elle, ne dépend pas du proxy que vous utilisez pour naviguer.
Timeouts et robustesse
Une résolution reCAPTCHA v2 s'étale sur plusieurs secondes entre la soumission et le token final. Ajoutez à cela le chargement de la page et la navigation post-soumission, et un requestHandlerTimeoutSecs trop court fait échouer l'acteur alors que la résolution était en bonne voie. Le seuil de 180 secondes du code ci-dessus laisse une marge confortable même sur un site lent.
Côté données personnelles, gardez le réflexe RGPD : ne collectez que les champs dont vous avez réellement besoin, documentez la base légale de votre scraping et évitez de journaliser des identifiants sensibles dans le dataset Apify. La résolution du CAPTCHA ne change rien à vos obligations sur les données extraites derrière.
FAQ
Quel type de CAPTCHA cet acteur résout-il ?
L'exemple cible reCAPTCHA v2 via method=userrecaptcha. CaptchaAI prend aussi en charge reCAPTCHA v3, Cloudflare Turnstile et Challenge, GeeTest v3, ainsi que les CAPTCHA image et grille : réutilisez la même boucle de polling en changeant la méthode et les paramètres soumis.
Combien coûte la résolution dans un acteur à forte concurrence ?
La facturation CaptchaAI est basée sur les threads, avec des résolutions illimitées par thread. Un maxConcurrency de 3 tient largement dans l'offre BASIC ($15/mois, 5 threads) ; montez en gamme uniquement si vous lancez plus de résolutions simultanées, pas en fonction du nombre total de pages.
Que faire si le token est injecté mais que le formulaire ne se soumet pas ?
Vérifiez que le callback du widget est bien déclenché : certains sites attendent l'appel à la fonction data-callback plutôt qu'un simple remplissage du champ g-recaptcha-response. Le code gère ce cas, mais confirmez le nom du sélecteur .g-recaptcha sur votre cible.
Faut-il un proxy résidentiel pour que la résolution fonctionne ?
Non. Le token reCAPTCHA v2 se résout indépendamment du proxy de navigation. Choisissez le type de proxy Apify (résidentiel ou datacenter) selon la tolérance du site cible, pas selon la résolution.
Comment rester conforme au RGPD en scrapant depuis Apify ?
Minimisez les données personnelles collectées, limitez la rétention dans le dataset et vérifiez vos obligations avant de lancer un run à grande échelle. Ce guide couvre la mécanique technique, pas le cadre juridique de votre projet.
Guides connexes
Si Apify orchestre déjà votre pipeline, obtenez une clé CaptchaAI et ajoutez la résolution CAPTCHA comme une étape normale de l'acteur, pas comme un traitement d'exception.