Use Cases

Vérification publicitaire sur des sites protégés par CAPTCHA

Un pipeline de vérification publicitaire s'arrête net dès qu'un éditeur affiche un CAPTCHA : la page ne se charge plus, la mesure de campagne saute et le rapport se retrouve troué au moment où vos annonceurs attendent des preuves de diffusion. CaptchaAI résout ce défi automatiquement, pour que le contrôle des placements, de la brand safety et de la visibilité continue de tourner sur des milliers de pages. L'enjeu n'est pas de « visiter des sites » : c'est de garder une collecte fiable quand chaque éditeur ajoute sa propre couche de protection anti-bot.

Ce que vous contrôlez, et pourquoi un CAPTCHA surgit

La vérification publicitaire consiste à visiter en continu des pages d'éditeurs pour vérifier où, à côté de quoi et pour qui une annonce s'affiche. Le tableau ci-dessous relie chaque contrôle métier à la raison technique pour laquelle il déclenche un défi.

Contrôle Description Pourquoi un CAPTCHA peut apparaître
Placement publicitaire L'annonce est-elle visible au bon endroit sur la page ? Les visites automatisées en série ressemblent à un comportement bot
Brand safety L'annonce apparaît-elle à côté d'un contenu sensible ou problématique ? Le balayage massif d'URL ressemble à du scraping intensif
Visibilité L'annonce a-t-elle réellement été chargée et affichée ? Les navigateurs headless et sessions uniformes sont vite détectés
Ciblage géographique La bonne création est-elle servie dans la bonne région ? Les proxys et changements de localisation déclenchent plus souvent des contrôles
Veille concurrentielle Quelles annonces diffusent les concurrents sur quels supports ? Le volume de consultations augmente le risque de challenge

Plus l'opération grandit, plus le vrai sujet devient la fiabilité du pipeline. Un contrôle qui échoue sans laisser de trace vaut moins qu'un échec explicite : votre équipe ad ops doit pouvoir distinguer « annonce absente » de « page bloquée par un CAPTCHA ». Pour chaque page, journalisez au minimum :

  • l'URL et l'horodatage du contrôle ;
  • si un défi a été rencontré et résolu, et de quel type ;
  • les réseaux publicitaires détectés dans le rendu ;
  • le proxy et la région utilisés, pour rejouer le contrôle à l'identique.

Détecter, résoudre et journaliser chaque page

Le cœur du pipeline tient en une boucle : récupérer la page, repérer une clé de site si un défi est présent, envoyer la tâche à CaptchaAI, puis rejouer la requête avec le token obtenu. Le résultat n'est pas un simple succès ou échec : on enregistre si un CAPTCHA a été résolu, quels réseaux publicitaires ont été détectés et si un signal de risque a été relevé.

import requests
import time
import re
import json
import os
from datetime import datetime

API_KEY = os.environ["CAPTCHAAI_API_KEY"]


def solve_captcha(method, params):
    params["key"] = API_KEY
    params["method"] = method

    resp = requests.get("https://ocr.captchaai.com/in.php", params=params)
    if not resp.text.startswith("OK|"):
        raise Exception(resp.text)

    task_id = resp.text.split("|")[1]
    for _ in range(60):
        time.sleep(5)
        result = requests.get("https://ocr.captchaai.com/res.php", params={
            "key": API_KEY, "action": "get", "id": task_id,
        })
        if result.text == "CAPCHA_NOT_READY":
            continue
        if result.text.startswith("OK|"):
            return result.text.split("|", 1)[1]
        raise Exception(result.text)
    raise TimeoutError()


def verify_ad_placement(url, session):
    """Verify ad placement on a publisher page."""
    resp = session.get(url)

    # Solve CAPTCHA if present
    match = re.search(r'data-sitekey=["\']([A-Za-z0-9_-]+)["\']', resp.text)
    if match:
        token = solve_captcha("userrecaptcha", {
            "googlekey": match.group(1),
            "pageurl": url,
        })
        resp = session.post(url, data={"g-recaptcha-response": token})

    html = resp.text

    # Check for ad elements
    result = {
        "url": url,
        "timestamp": datetime.utcnow().isoformat(),
        "ads_found": [],
        "brand_safety": True,
        "captcha_solved": match is not None,
    }

    # Detect ad tags
    ad_patterns = [
        (r'googletag\.pubads', "Google Ad Manager"),
        (r'doubleclick\.net', "DFP/DoubleClick"),
        (r'ad\.doubleclick', "DoubleClick"),
        (r'amazon-adsystem', "Amazon Ads"),
        (r'criteo\.com/.*\.js', "Criteo"),
    ]

    for pattern, name in ad_patterns:
        if re.search(pattern, html):
            result["ads_found"].append(name)

    # Brand safety check - flag problematic content
    safety_keywords = [
        "violence", "hate speech", "explicit",
        "gambling", "illegal",
    ]
    page_text = re.sub(r'<[^>]+>', '', html).lower()
    for keyword in safety_keywords:
        if keyword in page_text:
            result["brand_safety"] = False
            break

    return result


def run_verification(urls, output_file="verification_report.json"):
    """Run ad verification across multiple publisher URLs."""
    session = requests.Session()
    session.headers["User-Agent"] = (
        "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
        "AppleWebKit/537.36 Chrome/120.0.0.0"
    )

    results = []
    for i, url in enumerate(urls):
        try:
            result = verify_ad_placement(url, session)
            results.append(result)
            ads = ", ".join(result["ads_found"]) or "None"
            safe = "SAFE" if result["brand_safety"] else "UNSAFE"
            print(f"  [{i+1}/{len(urls)}] {url}: {ads} [{safe}]")
        except Exception as e:
            results.append({
                "url": url,
                "error": str(e),
                "timestamp": datetime.utcnow().isoformat(),
            })
            print(f"  [{i+1}/{len(urls)}] {url}: ERROR - {e}")

        time.sleep(2)

    with open(output_file, "w") as f:
        json.dump(results, f, indent=2)

    # Summary
    total = len(results)
    safe = sum(1 for r in results if r.get("brand_safety"))
    captchas = sum(1 for r in results if r.get("captcha_solved"))
    errors = sum(1 for r in results if "error" in r)

    print(f"\n  Total: {total} | Safe: {safe} | CAPTCHAs solved: {captchas} | Errors: {errors}")

    return results


# Publisher URLs to verify
publisher_urls = [
    "https://publisher1.com/article/tech-news",
    "https://publisher2.com/sports/latest",
    "https://publisher3.com/finance/markets",
]

run_verification(publisher_urls)

Rendre le rapport exploitable

Ce niveau de détail rend le rapport exploitable : les équipes ad ops, conformité et brand safety y lisent non seulement le verdict, mais aussi le contexte qui l'explique. Côté RGPD, la logique reste sobre : vous inspectez des balises publicitaires et du contenu de page, pas des données personnelles. Gardez cette discipline dans vos logs et ne collectez que ce dont le contrôle a réellement besoin.

Cohérence multi-marchés : aligner proxy et session

Prenons une campagne diffusée en France, en Belgique et en Suisse romande, à vérifier depuis vos workers hébergés sur OVHcloud ou en région eu-west-3 (Paris). Chaque marché exige un proxy local pour voir la bonne création, et chaque changement de géolocalisation augmente la probabilité d'un défi. En verrouillant un couple proxy + session par marché et en passant ce même proxy à CaptchaAI lors de la résolution, vous obtenez des captures cohérentes : l'annonce vue à Genève correspond bien à ce que verrait un internaute suisse, pas à un rendu français validé par erreur.

Éditeurs derrière Cloudflare : Turnstile et pages de challenge

De nombreux grands éditeurs européens passent par Cloudflare. Vous devez donc traiter à la fois les widgets Turnstile légers et les pages de challenge plus lourdes, servies avec un code 403 tant que la session n'est pas validée.

def handle_cloudflare(url, session):
    """Handle Cloudflare-protected publisher pages."""
    resp = session.get(url)

    if "cf-turnstile" in resp.text:
        match = re.search(r'data-sitekey=["\']([^"\']+)', resp.text)
        if match:
            token = solve_captcha("turnstile", {
                "sitekey": match.group(1),
                "pageurl": url,
            })
            return session.post(url, data={
                "cf-turnstile-response": token,
            })

    if resp.status_code == 403 and "cf-browser-verification" in resp.text:
        data = solve_captcha("cloudflare_challenge", {
            "pageurl": url,
            "proxy": "user:pass@proxy:port",
            "proxytype": "HTTP",
        })
        # Parse cf_clearance and use same proxy
        return data

    return resp

Le point sensible est la cohérence du contexte. Sur des protections strictes, une incohérence entre la session navigateur et le solveur suffit souvent à invalider le résultat.

Gardez le même proxy entre la navigation et la résolution du challenge : c'est la règle qui fait le plus de différence sur les éditeurs derrière Cloudflare.

FAQ

CaptchaAI prend-il en charge Turnstile et les pages de challenge Cloudflare ?

Oui. CaptchaAI résout Cloudflare Turnstile ainsi que les pages Cloudflare Challenge, en plus de reCAPTCHA v2 et v3, GeeTest v3 et les CAPTCHA image/OCR. En revanche, hCaptcha et FunCaptcha ne sont pas pris en charge : prévoyez une autre stratégie pour les éditeurs qui les utilisent.

Comment rester sobre côté RGPD lors d'une collecte à grande échelle ?

Limitez la collecte aux éléments publicitaires et au contexte de page, sans stocker de données personnelles superflues. La résolution du défi ne change rien à vos obligations : documentez la finalité, minimisez ce que vos logs conservent et vérifiez vos propres obligations RGPD avant d'élargir le périmètre.

Faut-il un navigateur réel pour vérifier les formats vidéo ?

Pour le display et le natif, l'approche par requêtes suffit à confirmer la présence des balises. Pour la vidéo et les créations plus dynamiques, prévoyez un vrai rendu avec Selenium ou Playwright afin de confirmer la lecture et la visibilité effective.

Combien de threads faut-il pour vérifier des milliers de pages ?

Cela dépend de votre parallélisme, pas d'un quota de résolutions. Un thread traite un CAPTCHA à la fois puis enchaîne le suivant ; le plan BASIC ($15/mois, 5 threads) convient à un pilote, tandis qu'ADVANCE ($90/mois, 50 threads) absorbe une vérification continue à fort volume, avec des résolutions illimitées par thread.

Guides connexes


Pour une vérification publicitaire à grande échelle avec des journaux exploitables, obtenez votre clé CaptchaAI et construisez un pipeline qui gère le défi comme la trace de chaque contrôle.

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