Résoudre un PopularCaptcha avec Playwright et Puppeteer (Node.js)
L'automatisation de navigateur se bloque dès qu'un PopularCaptcha apparaît. Cliquer à travers une grille d'images depuis un script est lent et fragile. Obtenir un jeton auprès d'une API de résolution et le placer dans la page demande une seule requête, et le reste de votre script continue normalement.
Ce guide couvre Playwright et Puppeteer avec Node.js. Le flux est le même dans les deux, et les exemples utilisent des résolutions sans proxy, que Scarnox exécute sur son propre réseau. PopularCaptcha est le type résolu aujourd'hui par Scarnox. Les autres types de captcha arrivent bientôt, donc le code ci-dessous ne concerne que PopularCaptcha.
Le flux
- Lire la sitekey dans la page.
- Demander un jeton à Scarnox avec une seule requête POST bloquante.
- Placer le jeton dans le champ de réponse, ou le transmettre au callback du site, puis envoyer le formulaire.
Configurer la clé
Créez un compte et commencez avec un crédit d'essai gratuit, sans carte bancaire. Gardez la clé dans une variable d'environnement, jamais dans le code de test :
export SCARNOX_KEY="scarnox_YOURAPIKEY"
Node 18 et versions ultérieures incluent fetch, l'appel API ne nécessite donc aucun paquet supplémentaire.
Playwright
import { chromium } from "playwright";
const API_KEY = process.env.SCARNOX_KEY;
async function solve(sitekey, url) {
const r = await fetch("https://api.scarnox.com/api/tasks/create", {
method: "POST",
headers: { Authorization: `Bearer ${API_KEY}`, "Content-Type": "application/json" },
body: JSON.stringify({ captcha_type: "PopularCaptchaTaskProxyless", site_key: sitekey, site_url: url }),
});
const data = await r.json();
if (data.errorId !== 0) throw new Error(`${data.errorCode}: ${data.errorDescription}`);
return data.solution.token;
}
const browser = await chromium.launch();
const page = await browser.newPage();
await page.goto("https://example.com/signup");
const sitekey = await page.getAttribute("[data-sitekey]", "data-sitekey");
const token = await solve(sitekey, page.url());
// Placer le jeton dans les deux champs de réponse que le site pourrait lire.
await page.evaluate((t) => {
for (const name of ["h-captcha-response", "g-recaptcha-response"]) {
document.querySelectorAll(`[name="${name}"]`).forEach((el) => { el.value = t; });
}
}, token);
await page.fill("#email", "you@example.com");
await page.click("button[type=submit]");
Remarquez que site_url provient de page.url(). C'est la page sur laquelle se trouve le widget, ce dont le solveur a besoin. La résolution se fait après le chargement de la page, la sitekey est donc déjà présente dans le DOM.
Puppeteer
Puppeteer ne diffère que par l'API de sélecteurs. Réutilisez la fonction solve ci-dessus et remplacez les appels de page :
const sitekey = await page.$eval("[data-sitekey]", (el) => el.dataset.sitekey);
const token = await solve(sitekey, page.url());
await page.evaluate((t) => {
document.querySelectorAll('[name="h-captcha-response"],[name="g-recaptcha-response"]')
.forEach((el) => { el.value = t; });
}, token);
Lorsque le site utilise un callback
Certains sites ne lisent pas le champ caché. Ils attendent le callback JavaScript du widget. Repérez la fonction nommée dans l'attribut data-callback du widget, et appelez-la avec le jeton :
await page.evaluate((t) => {
const name = document.querySelector("[data-sitekey]")?.dataset.callback;
if (name && typeof window[name] === "function") window[name](t);
}, token);
Si le site utilise les deux mécanismes, remplissez le champ et appelez le callback. Faire les deux est sans danger.
Widgets invisibles
Un widget invisible n'a pas de case à cocher. Le site le démarre depuis son propre code, généralement au moment où le visiteur appuie sur envoyer. Envoyez la même requête avec le champ facultatif is_invisible à true :
body: JSON.stringify({
captcha_type: "PopularCaptchaTaskProxyless",
site_key: sitekey,
site_url: url,
is_invisible: true,
}),
Le guide sur le captcha invisible explique comment repérer ces widgets et comment renvoyer le jeton.
Conseils de timing
- Demandez le jeton juste avant d'envoyer. Les jetons expirent au bout d'environ deux minutes.
- Donnez à la requête de résolution un délai long, d'environ 185 secondes. Elle bloque jusqu'à ce que le jeton soit prêt.
- Les résolutions échouées sont gratuites, donc une nouvelle tentative ne coûte que du temps. Réessayez les échecs passagers avec un recul, comme décrit dans gestion des erreurs et recul.
- Vous lancez beaucoup de navigateurs en parallèle ? Limitez le nombre de résolutions simultanées, afin qu'un lot lent ne bloque pas le reste de l'exécution.
- Fermez les navigateurs dans un bloc
finally. Un test planté qui laisse Chromium en marche ralentira toutes les résolutions suivantes sur la même machine.
Tester votre propre flux
Exécutez l'exemple sur un formulaire qui vous appartient avant de le lancer sur quelque chose de plus gros. Vérifiez trois choses : le jeton est accepté, le formulaire est envoyé, et la page suivante apparaît. Une fois le flux fonctionnel de bout en bout, gardez-le vert en CI avec une seule résolution réelle par exécution.
Essayer gratuitement
Créez un compte et commencez avec un crédit d'essai gratuit, sans carte bancaire. Cela suffit pour câbler et vérifier l'ensemble du flux. Pour le coût de la résolution à grande échelle, consultez les tarifs actuels.
Déboguer un jeton rejeté
Lorsque le site rejette un jeton, reproduisez le flux avec une requête et un chargement de page avant de modifier quoi que ce soit d'autre. Vérifiez ces points dans l'ordre :
- La sitekey envoyée correspond au widget de la page chargée.
site_urlest la page où se trouve le widget, et non la cible d'une redirection.- Le jeton a été placé dans le champ que la page lit, ou le callback a été appelé.
- L'envoi a eu lieu dans les deux minutes environ suivant la résolution.
Sauvegardez le HTML de la page au moment de l'échec avec await page.content(). Comparer ce HTML avec une exécution qui fonctionne révèle généralement la différence d'un coup d'œil : une autre sitekey, un champ manquant, ou un widget rendu dans un autre mode.