Tutorials

Détecter les CAPTCHA en chargement différé (lazy loading)

Votre script récupère le code source de la page, cherche le sitekey... et ne trouve rien. Pourtant, quand vous ouvrez le même site à la main, le CAPTCHA est bien là.

La cause est presque toujours la même : le défi n'est injecté dans le DOM qu'après une action précise — un clic, un focus, un défilement ou un délai. La bonne réponse n'est donc pas de lire la page plus tôt, mais d'attendre le bon déclencheur avant d'aller chercher le sitekey.

Ce guide montre comment repérer et attendre un CAPTCHA chargé dynamiquement, en Puppeteer comme en Selenium, puis comment le résoudre via l'API CaptchaAI.


Quels déclencheurs retardent l'apparition du CAPTCHA

Avant d'écrire du code, identifiez ce qui provoque l'injection.

Dans la grande majorité des cas, il s'agit de l'un des cinq déclencheurs suivants.

Déclencheur Exemple Comment l'activer
Clic sur le bouton « Envoyer » ajoute reCAPTCHA au formulaire Cliquez d'abord sur le bouton
Focus sur le formulaire CAPTCHA apparaît lorsque la saisie est ciblée Focus sur le champ email/password
Position de défilement CAPTCHA se charge lorsque la section est visible Faites défiler jusqu'au formulaire
Minuterie CAPTCHA se charge après 3 secondes Attendez le retard
Condition JavaScript CAPTCHA se charge après la réponse AJAX Déclencher la demande de prérequis

Un cas concret côté francophone : vous automatisez le parcours de connexion d'un portail interne hébergé chez OVHcloud, où le Turnstile n'apparaît qu'au moment où le champ mot de passe reçoit le focus.

Tant que votre script ne simule pas ce focus, la page ne contient aucun .cf-turnstile à extraire — d'où l'échec silencieux de l'extraction.

Pour repérer le bon déclencheur, ouvrez le site à la main et surveillez :

  • l'onglet Réseau, pour voir quand la bibliothèque CAPTCHA est chargée ;
  • l'inspecteur d'éléments, pour voir à quel moment le <div> apparaît ;
  • l'action précise (clic, focus, défilement) qui précède immédiatement l'injection.

À noter : reproduisez ces tests uniquement sur des sites que vous êtes autorisé à automatiser, et minimisez les données personnelles collectées conformément à vos obligations RGPD.


Méthode 1 : surveiller le DOM avec MutationObserver

La méthode la plus fiable consiste à observer le DOM et à réagir dès qu'un élément CAPTCHA y est ajouté.

Point clé : le MutationObserver doit être posé avant de déclencher l'action, sinon vous risquez de rater l'injection.

En Puppeteer

const puppeteer = require('puppeteer');

const browser = await puppeteer.launch({ headless: false });
const page = await browser.newPage();
await page.goto('https://example.com/login');

// Set up MutationObserver before triggering the CAPTCHA
const captchaInfo = await page.evaluate(() => {
  return new Promise((resolve) => {
    // Check if already present
    const existing = document.querySelector('.g-recaptcha, .cf-turnstile, .h-captcha');
    if (existing) {
      resolve({
        type: existing.className,
        sitekey: existing.getAttribute('data-sitekey'),
      });
      return;
    }

    // Watch for new elements
    const observer = new MutationObserver((mutations) => {
      for (const mutation of mutations) {
        for (const node of mutation.addedNodes) {
          if (node.nodeType !== 1) continue;

          const captcha = node.matches?.('.g-recaptcha, .cf-turnstile, .h-captcha')
            ? node
            : node.querySelector?.('.g-recaptcha, .cf-turnstile, .h-captcha');

          if (captcha) {
            observer.disconnect();
            resolve({
              type: captcha.className,
              sitekey: captcha.getAttribute('data-sitekey'),
            });
            return;
          }
        }
      }
    });

    observer.observe(document.body, { childList: true, subtree: true });

    // Timeout after 30 seconds
    setTimeout(() => {
      observer.disconnect();
      resolve(null);
    }, 30000);
  });
});

console.log('Detected CAPTCHA:', captchaInfo);

Notez le test initial : si l'élément est déjà présent, on résout immédiatement. C'est ce contrôle qui évite le piège classique de l'observateur qui « rate » un CAPTCHA parce qu'il était là dès le départ.

Déclencher l'apparition du CAPTCHA

Une fois l'observateur en place, provoquez l'injection avec l'action qui correspond à votre déclencheur.

// Click the submit button to trigger CAPTCHA
await page.click('#submit-btn');

// Or focus the input
await page.focus('#email');

// Or scroll to the form
await page.evaluate(() => {
  document.querySelector('#signup-form').scrollIntoView();
});

Méthode 2 : attendre le chargement du script CAPTCHA

Un <div> vide ne suffit pas : le CAPTCHA n'est exploitable qu'une fois sa bibliothèque JavaScript chargée.

Vous pouvez donc attendre que l'objet global soit disponible plutôt que de guetter un élément du DOM. Chaque type expose le sien :

  • reCAPTCHA charge window.grecaptcha (avec sa méthode render) ;
  • Cloudflare Turnstile charge window.turnstile.
// Wait for reCAPTCHA script to load
await page.waitForFunction(() => {
  return typeof window.grecaptcha !== 'undefined' 
    && typeof window.grecaptcha.render === 'function';
}, { timeout: 30000 });

// Now extract parameters
const sitekey = await page.evaluate(() => {
  const el = document.querySelector('.g-recaptcha');
  return el?.getAttribute('data-sitekey');
});

Cas de Cloudflare Turnstile

Le principe est identique pour Turnstile : on attend l'objet window.turnstile avant de lire le sitekey.

await page.waitForFunction(() => {
  return typeof window.turnstile !== 'undefined';
}, { timeout: 30000 });

const sitekey = await page.evaluate(() => {
  const el = document.querySelector('.cf-turnstile');
  return el?.getAttribute('data-sitekey');
});

Méthode 3 : intercepter l'appel à render()

Pour les intégrations les plus tardives, le plus sûr est de se brancher sur la bibliothèque CAPTCHA elle-même.

En redéfinissant grecaptcha.render avant l'exécution des scripts de la page, vous capturez les paramètres exacts passés au rendu — sitekey, callback, conteneur — sans avoir à deviner les sélecteurs.

// Inject before page scripts run
await page.evaluateOnNewDocument(() => {
  window.__captchaDetected = null;

  // Hook grecaptcha.render
  let _grecaptcha;
  Object.defineProperty(window, 'grecaptcha', {
    set(val) {
      _grecaptcha = val;
      const origRender = val.render;
      val.render = function(container, params) {
        window.__captchaDetected = {
          type: 'recaptcha',
          sitekey: params.sitekey,
          callback: params.callback?.name,
          container: typeof container === 'string' ? container : container.id,
        };
        return origRender.apply(this, arguments);
      };
    },
    get() { return _grecaptcha; },
  });
});

await page.goto('https://example.com/signup');

// Trigger the CAPTCHA (click, scroll, etc.)
await page.click('#show-form');

// Wait for detection
await page.waitForFunction(() => window.__captchaDetected !== null, {
  timeout: 30000,
});

const detected = await page.evaluate(() => window.__captchaDetected);
console.log('Detected:', detected);
// { type: 'recaptcha', sitekey: '6Le-wvkS...', callback: 'onCaptcha', container: 'recaptcha-box' }

En Python avec Selenium : attendre un CAPTCHA différé

Côté Selenium, WebDriverWait remplit le même rôle que le MutationObserver : il déclenche l'action, puis attend l'apparition de l'élément avant d'extraire le sitekey et de déterminer le type.

from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC

driver = webdriver.Chrome()
driver.get("https://example.com/login")

# Trigger the CAPTCHA loading
submit = driver.find_element(By.ID, "submit-btn")
submit.click()

# Wait for CAPTCHA to appear
try:
    captcha_el = WebDriverWait(driver, 30).until(
        EC.presence_of_element_located((
            By.CSS_SELECTOR,
            ".g-recaptcha, .cf-turnstile, .h-captcha"
        ))
    )
    sitekey = captcha_el.get_attribute("data-sitekey")
    captcha_class = captcha_el.get_attribute("class")

    if "g-recaptcha" in captcha_class:
        captcha_type = "recaptcha"
    elif "cf-turnstile" in captcha_class:
        captcha_type = "turnstile"
    else:
        captcha_type = "hcaptcha"

    print(f"Type: {captcha_type}, Sitekey: {sitekey}")
except Exception:
    print("No CAPTCHA appeared within 30 seconds")

Attendre l'iframe reCAPTCHA

Piège fréquent : le <div> existe déjà mais le script n'a pas fini de s'exécuter, si bien que le sitekey reste vide. Attendez alors l'iframe plutôt que le conteneur.

# reCAPTCHA loads an iframe even when the div exists but the script is still loading
WebDriverWait(driver, 30).until(
    EC.presence_of_element_located((
        By.CSS_SELECTOR,
        "iframe[src*='recaptcha'], iframe[src*='challenges.cloudflare.com']"
    ))
)
print("CAPTCHA iframe loaded")

Détection et résolution de bout en bout

Une fois la détection fiable, la fonction ci-dessous enchaîne cinq étapes :

  1. déclencher l'action qui injecte le CAPTCHA ;
  2. attendre son apparition dans le DOM ;
  3. identifier le type et choisir la méthode d'envoi ;
  4. résoudre avec CaptchaAI et interroger le résultat ;
  5. injecter le token dans le champ attendu.

CaptchaAI prend en charge reCAPTCHA v2/v3, Cloudflare Turnstile et Challenge, GeeTest v3 ainsi que les CAPTCHA image ; adaptez la branche au type réellement présent sur la page.

import requests
import time

def detect_and_solve(driver, api_key, trigger_action=None):
    """Detect a lazy-loaded CAPTCHA, solve it, and inject the token."""

    # 1. Trigger the CAPTCHA
    if trigger_action:
        trigger_action(driver)

    # 2. Wait for it to appear
    captcha_el = WebDriverWait(driver, 30).until(
        EC.presence_of_element_located((
            By.CSS_SELECTOR,
            ".g-recaptcha, .cf-turnstile, .h-captcha"
        ))
    )

    sitekey = captcha_el.get_attribute("data-sitekey")
    page_url = driver.current_url
    captcha_class = captcha_el.get_attribute("class")

    # 3. Determine type and method
    if "g-recaptcha" in captcha_class:
        method, key_param, token_field = "userrecaptcha", "googlekey", "g-recaptcha-response"
    elif "cf-turnstile" in captcha_class:
        method, key_param, token_field = "turnstile", "sitekey", "cf-turnstile-response"
    else:
        method, key_param, token_field = "hcaptcha", "sitekey", "h-captcha-response"

    # 4. Solve with CaptchaAI
    resp = requests.post("https://ocr.captchaai.com/in.php", data={
        "key": api_key, "method": method,
        key_param: sitekey, "pageurl": page_url, "json": "1",
    }).json()

    task_id = resp["request"]
    for _ in range(24):
        time.sleep(5)
        result = requests.get("https://ocr.captchaai.com/res.php", params={
            "key": api_key, "action": "get", "id": task_id, "json": "1"
        }).json()
        if result["status"] == 1:
            token = result["request"]
            break

    # 5. Inject
    driver.execute_script(f"""
        const el = document.querySelector('textarea[name="{token_field}"], input[name="{token_field}"]');
        if (el) el.value = arguments[0];
    """, token)

    return token

La boucle interroge le résultat toutes les 5 secondes, jusqu'à 24 fois (deux minutes).

CaptchaAI facture au thread concurrent, pas au CAPTCHA résolu : le plan BASIC ($15/mois, 5 threads) autorise cinq résolutions en parallèle, ce qui suffit à un parcours de connexion automatisé.


Dépannage

Problème Cause probable Correctif
Le CAPTCHA n'apparaît jamais Mauvaise action de déclenchement Inspectez la page pour trouver l'événement (clic, focus, défilement) qui l'injecte
Le sitekey est vide Le conteneur existe mais le script n'a pas fini de s'exécuter Attendez l'iframe CAPTCHA, pas seulement le <div>
L'observateur rate l'élément Le CAPTCHA était déjà présent au chargement Vérifiez les éléments existants avant de poser l'observateur
Le délai d'expiration est atteint Le site n'injecte le CAPTCHA que pour de vraies sessions Utilisez un navigateur complet avec une empreinte de navigateur réaliste

FAQ

Pourquoi mon sitekey est-il vide alors que le CAPTCHA s'affiche ?

Parce que vous lisez l'attribut data-sitekey trop tôt : le <div> est déjà dans le DOM, mais le script CAPTCHA ne l'a pas encore renseigné. Attendez l'iframe (iframe[src*='recaptcha']) ou l'objet global (window.grecaptcha) avant d'extraire la clé.

MutationObserver ou waitForFunction : quelle méthode choisir ?

Utilisez MutationObserver quand vous ne savez pas quand ni où l'élément apparaîtra, car il réagit à toute modification du DOM. Préférez waitForFunction quand vous savez que c'est le script (grecaptcha, turnstile) qui conditionne le rendu : le test est plus simple et plus lisible.

Un CAPTCHA en chargement différé fonctionne-t-il en mode headless ?

Oui, mais certains sites n'injectent le CAPTCHA que pour des navigateurs pilotés de façon réaliste. Si rien n'apparaît en headless, passez au mode headless: 'new' de Puppeteer ou lancez un navigateur complet avec une fenêtre visible et une empreinte cohérente.

Combien de temps attendre avant de conclure qu'aucun CAPTCHA n'apparaîtra ?

Un délai de 30 secondes couvre la plupart des cas, minuteries et réponses AJAX lentes comprises. Au-delà, l'absence de CAPTCHA trahit presque toujours une action de déclenchement incorrecte, pas une lenteur réseau.


Résolvez vos CAPTCHA avec CaptchaAI

Récupérez votre clé API sur le site CaptchaAI et branchez la détection ci-dessus sur votre flux de résolution.


Guides associés

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