Pour scraper une page qui affiche un CAPTCHA en Node.js, vous n'avez pas besoin de piloter un navigateur complet : envoyez la clé du site à l'API CaptchaAI, récupérez le token, puis rejouez la requête HTTP avec ce token. Votre script continue de gérer les requêtes en asynchrone pendant que la résolution se fait côté service. Ce tutoriel construit ce workflow de bout en bout avec axios pour le réseau et cheerio pour parcourir le HTML, sur des pages protégées par reCAPTCHA v2, reCAPTCHA v3 ou Cloudflare Turnstile.
C'est l'approche la plus légère quand la cible renvoie du HTML statique et des formulaires classiques. Si la page ne s'affiche qu'après exécution de JavaScript, reportez-vous à la fin de ce guide : c'est le moment de passer à un navigateur headless.
Le workflow tient en quatre étapes, que l'on retrouvera dans chaque exemple :
- charger la page cible avec axios ;
- repérer le type et la clé du site (sitekey) dans le HTML ;
- résoudre le CAPTCHA via l'API CaptchaAI et récupérer le token ;
- rejouer la requête finale en y injectant ce token.
Prérequis
| Prérequis | Détails |
|---|---|
| Node.js 16+ | Avec npm |
| axios | npm install axios |
| cheerio | npm install cheerio |
| Clé API CaptchaAI | Depuis captchaai.com |
CaptchaAI facture par thread simultané, pas par résolution. Un thread correspond à un CAPTCHA en cours de traitement : le niveau de parallélisme de votre scraper (la variable concurrency plus bas) ne doit donc pas dépasser le nombre de threads de votre offre. Le plan BASIC ($15/mois, 5 threads) suffit pour trois à cinq pages en parallèle ; montez en gamme uniquement quand vous saturez réellement vos threads.
Le module de résolution CaptchaAI
Isolez la logique d'appel à l'API dans un module réutilisable. Il soumet la tâche à in.php, interroge res.php toutes les cinq secondes jusqu'à obtenir le token, et expose une méthode par type de CAPTCHA. Vous l'importerez ensuite dans chaque script de scraping.
// captcha-solver.js
const axios = require("axios");
class CaptchaSolver {
constructor(apiKey) {
this.apiKey = apiKey;
this.baseUrl = "https://ocr.captchaai.com";
}
async _submit(params) {
params.key = this.apiKey;
const resp = await axios.get(`${this.baseUrl}/in.php`, { params });
if (!resp.data.startsWith("OK|")) {
throw new Error(`Submit error: ${resp.data}`);
}
return resp.data.split("|")[1];
}
async _poll(taskId, timeout = 300000) {
const deadline = Date.now() + timeout;
while (Date.now() < deadline) {
await new Promise((r) => setTimeout(r, 5000));
const resp = await axios.get(`${this.baseUrl}/res.php`, {
params: { key: this.apiKey, action: "get", id: taskId },
});
if (resp.data === "CAPCHA_NOT_READY") continue;
if (resp.data.startsWith("OK|")) return resp.data.split("|")[1];
throw new Error(`Solve error: ${resp.data}`);
}
throw new Error("Solve timed out");
}
async solveRecaptchaV2(siteKey, pageUrl) {
const taskId = await this._submit({
method: "userrecaptcha",
googlekey: siteKey,
pageurl: pageUrl,
});
return this._poll(taskId);
}
async solveRecaptchaV3(siteKey, pageUrl, action = "verify") {
const taskId = await this._submit({
method: "userrecaptcha",
googlekey: siteKey,
pageurl: pageUrl,
version: "v3",
action,
});
return this._poll(taskId);
}
async solveTurnstile(siteKey, pageUrl) {
const taskId = await this._submit({
method: "turnstile",
sitekey: siteKey,
pageurl: pageUrl,
});
return this._poll(taskId);
}
}
module.exports = CaptchaSolver;
La boucle _poll s'arrête au bout de cinq minutes par défaut. CAPCHA_NOT_READY n'est pas une erreur : c'est le signal que la résolution est encore en cours, on continue simplement d'interroger le résultat.
Extraire une page protégée par reCAPTCHA
Le parcours est toujours le même : charger la page, lire le data-sitekey de l'élément .g-recaptcha, résoudre le CAPTCHA, puis renvoyer la requête avec le champ g-recaptcha-response. Si aucun sitekey n'est présent, la page est accessible directement et l'appel à l'API devient inutile.
const axios = require("axios");
const cheerio = require("cheerio");
const CaptchaSolver = require("./captcha-solver");
const solver = new CaptchaSolver("YOUR_API_KEY");
async function scrapeProtectedPage(url) {
// Step 1: Load the page
const { data: html } = await axios.get(url, {
headers: {
"User-Agent":
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
},
});
const $ = cheerio.load(html);
// Step 2: Extract site key
const siteKey = $(".g-recaptcha").attr("data-sitekey");
if (!siteKey) {
console.log("No CAPTCHA found, page loaded directly");
return html;
}
console.log("Site key found:", siteKey);
// Step 3: Solve the CAPTCHA
const token = await solver.solveRecaptchaV2(siteKey, url);
console.log("Token received:", token.substring(0, 50));
// Step 4: Submit with the token
const result = await axios.post(
url,
new URLSearchParams({
"g-recaptcha-response": token,
q: "search query",
}),
{
headers: {
"Content-Type": "application/x-www-form-urlencoded",
"User-Agent":
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
},
}
);
return result.data;
}
Scraper plusieurs pages en parallèle
Node.js brille sur les charges I/O : pendant qu'un CAPTCHA se résout, d'autres requêtes peuvent avancer. Le modèle ci-dessous répartit une file d'URL entre plusieurs workers. Gardez concurrency aligné sur vos threads CaptchaAI et sur le débit toléré par la cible : trois workers sont un bon point de départ.
async function scrapePages(urls, siteKey, concurrency = 3) {
const results = [];
const queue = [...urls];
const worker = async () => {
while (queue.length > 0) {
const url = queue.shift();
try {
const token = await solver.solveRecaptchaV2(siteKey, url);
const { data } = await axios.post(
url,
new URLSearchParams({ "g-recaptcha-response": token }),
{
headers: {
"User-Agent":
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
},
}
);
results.push({ url, data, success: true });
console.log(`Scraped: ${url}`);
} catch (err) {
results.push({ url, error: err.message, success: false });
console.error(`Failed: ${url} - ${err.message}`);
}
}
};
// Run workers concurrently
const workers = Array(concurrency)
.fill(null)
.map(() => worker());
await Promise.all(workers);
return results;
}
// Usage
const urls = [
"https://example.com/page/1",
"https://example.com/page/2",
"https://example.com/page/3",
];
const results = await scrapePages(urls, "6Le-wvkS...", 3);
Chaque résultat porte un indicateur success, ce qui permet de rejouer uniquement les URL en échec sans relancer tout le lot.
Gérer les cookies et les sessions
Certains sites lient le CAPTCHA à une session : le token n'est validé que si la requête finale réutilise les cookies posés au premier chargement. Enveloppez axios avec un CookieJar pour conserver l'état entre le GET initial et le POST de soumission.
const { wrapper } = require("axios-cookiejar-support");
const { CookieJar } = require("tough-cookie");
const jar = new CookieJar();
const client = wrapper(
axios.create({
jar,
headers: {
"User-Agent":
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
},
})
);
async function scrapeWithSession(url, siteKey) {
// Initial page load sets cookies
await client.get(url);
// Solve CAPTCHA
const token = await solver.solveRecaptchaV2(siteKey, url);
// Submit with maintained cookies
const result = await client.post(
url,
new URLSearchParams({ "g-recaptcha-response": token })
);
return result.data;
}
Analyser les résultats avec cheerio
Une fois la page débloquée, cheerio offre une API façon jQuery pour extraire les données. Sélectionnez vos éléments et projetez-les dans des objets propres, prêts à être enregistrés en JSON ou en base.
function parseResults(html) {
const $ = cheerio.load(html);
const items = [];
$(".result-item").each((_, el) => {
items.push({
title: $(el).find(".title").text().trim(),
url: $(el).find("a").attr("href"),
description: $(el).find(".description").text().trim(),
});
});
return items;
}
Si vous collectez des données personnelles, pensez RGPD : minimisez ce que vous récupérez et vérifiez vos obligations avant de stocker quoi que ce soit.
Bonnes pratiques pour un scraper stable
Quelques réflexes gardent un scraper Node.js fiable dans la durée, que vous le déployiez sur une VM OVHcloud ou dans une fonction serverless :
- résolvez le CAPTCHA au dernier moment, juste avant la requête finale, pour éviter qu'un token n'expire ;
- rejouez sélectivement les URL en échec grâce à l'indicateur
success, plutôt que de relancer tout le lot ; - espacez les requêtes et faites tourner vos proxys pour ne pas saturer la cible ;
- gardez votre clé API hors du code source, dans une variable d'environnement.
Dépannage
| Problème | Cause | Correctif |
|---|---|---|
CAPCHA_NOT_READY en boucle sans fin |
Mauvais sitekey ou résolution lente | Vérifiez le sitekey ; augmentez le timeout de _poll |
403 Forbidden sur le POST |
Cookies ou en-têtes manquants | Passez par une session ; ajoutez l'en-tête Referer |
| cheerio ne trouve aucun élément | Contenu injecté en JavaScript | Passez à Puppeteer pour les pages rendues côté client |
ECONNREFUSED |
Débit bridé par le site cible | Espacez les requêtes ; faites tourner vos proxys |
FAQ
axios et cheerio suffisent-ils, ou faut-il un navigateur headless ?
axios plus cheerio conviennent quand la cible renvoie du HTML complet et des formulaires standard. Dès que le contenu n'apparaît qu'après exécution de JavaScript, passez à Puppeteer ou Playwright, qui pilotent un vrai moteur de rendu.
Comment répartir la charge sans dépasser mon offre ?
Alignez le paramètre concurrency sur le nombre de threads de votre plan. Un thread traite un CAPTCHA à la fois ; lancer plus de résolutions simultanées que de threads ne les accélère pas, cela les met en file d'attente.
Le token expire-t-il avant que je puisse l'utiliser ?
Oui, un token reCAPTCHA a une durée de vie courte, de l'ordre de deux minutes. Résolvez le CAPTCHA juste avant d'envoyer la requête finale, et non en avance : ne constituez pas de réserve de tokens.
Que faire pour une page protégée par Cloudflare ?
Si le site utilise Turnstile, appelez solver.solveTurnstile(). Pour une page de défi Cloudflare complet, passez par la méthode dédiée, qui renvoie les cookies cf_clearance à réinjecter dans vos requêtes suivantes.
Guides connexes
- Résolution de CAPTCHA avec Puppeteer en Node.js
- Scraping de CAPTCHA en Python
- Rotation de proxys pour le scraping de CAPTCHA