Use Cases

Surveillance des tarifs aériens sur des sites protégés par CAPTCHA

Un moniteur de tarifs aériens finit toujours par tomber sur un CAPTCHA. Dès qu'une même IP interroge plusieurs fois les mêmes routes, compagnies aériennes et agences de voyage renvoient un reCAPTCHA v2 ou une page Cloudflare Turnstile. La réponse tient en une phrase : traitez le défi comme une étape prévue du pipeline, pas comme une panne. CaptchaAI le résout, vous réinjectez le token, la collecte reprend.

Ce guide construit ce workflow en Python — détecter le défi pendant un contrôle de prix, le résoudre, reprendre l'extraction — que vous suiviez Paris–Montréal sur un comparateur québécois ou Casablanca–Bruxelles sur une compagnie du Maghreb.


Pourquoi les sites de voyage déclenchent autant de CAPTCHA

Chaque recherche de vol interroge des systèmes de tarification coûteux à servir : le rate limiting et le CAPTCHA écartent les vérifications automatisées répétées, et un moniteur qui balaie plusieurs routes depuis une même IP ressemble au trafic ciblé. La parade tient en deux réflexes :

  • Espacer et faire tourner les requêtes, pour rester sous les seuils de déclenchement.
  • Résoudre le défi proprement quand il apparaît malgré tout, sans casser la chaîne.

À quelle cadence interroger les prix ?

Type de route Cadence raisonnable Pourquoi
Routes très volatiles ou à forte valeur Toutes les quelques heures Les variations de prix méritent une veille plus serrée
Routes loisirs plus stables 1 à 2 fois par jour La pression sur le site et les proxys reste modérée
Gros portefeuille de routes Batchs étalés La charge est plus régulière et déclenche moins de CAPTCHA
Alertes ultra agressives À utiliser avec prudence Le coût et le risque de blocage montent très vite

Le pipeline en un coup d'œil

Schedule check -> Request fare page -> CAPTCHA detected?
                                         -> Yes
                                    Solve via CaptchaAI -> Inject token -> Retry request
                                         -> No
                                    Parse fare data -> Store -> Alert on price change

Prérequis

  • Clé API CaptchaAI — créez-en une sur captchaai.com.
  • Python 3.8+ avec la bibliothèque requests.
  • Proxy résidentiel, préférable sur les sites de voyage.
pip install requests

Le helper de résolution

Deux fonctions envoient le défi et interrogent le résultat jusqu'au token :

  • solve_recaptcha_v2 pour reCAPTCHA v2 (method=userrecaptcha).
  • solve_turnstile pour Cloudflare Turnstile (method=turnstile).
import requests
import time

API_KEY = "YOUR_API_KEY"


def solve_recaptcha_v2(sitekey, pageurl):
    """Solve reCAPTCHA v2 and return the token."""
    submit = requests.post("https://ocr.captchaai.com/in.php", data={
        "key": API_KEY, "method": "userrecaptcha",
        "googlekey": sitekey, "pageurl": pageurl, "json": 1
    }).json()

    if submit.get("status") != 1:
        raise RuntimeError(f"Submit error: {submit.get('request')}")

    task_id = submit["request"]
    time.sleep(20)

    for _ in range(30):
        result = requests.get("https://ocr.captchaai.com/res.php", params={
            "key": API_KEY, "action": "get", "id": task_id, "json": 1
        }).json()
        if result.get("status") == 1:
            return result["request"]
        if result.get("request") != "CAPCHA_NOT_READY":
            raise RuntimeError(f"Solve error: {result['request']}")
        time.sleep(5)
    raise TimeoutError("Solve timed out")


def solve_turnstile(sitekey, pageurl):
    """Solve Cloudflare Turnstile and return the token."""
    submit = requests.post("https://ocr.captchaai.com/in.php", data={
        "key": API_KEY, "method": "turnstile",
        "sitekey": sitekey, "pageurl": pageurl, "json": 1
    }).json()

    if submit.get("status") != 1:
        raise RuntimeError(f"Submit error: {submit.get('request')}")

    task_id = submit["request"]
    time.sleep(10)

    for _ in range(30):
        result = requests.get("https://ocr.captchaai.com/res.php", params={
            "key": API_KEY, "action": "get", "id": task_id, "json": 1
        }).json()
        if result.get("status") == 1:
            return result["request"]
        if result.get("request") != "CAPCHA_NOT_READY":
            raise RuntimeError(f"Solve error: {result['request']}")
        time.sleep(5)
    raise TimeoutError("Solve timed out")

Le moniteur avec gestion intégrée du défi

La classe FareMonitor garde une session, détecte le type de défi, appelle le bon helper, réinjecte le token (g-recaptcha-response ou cf-turnstile-response) puis renvoie la requête. Adaptez _parse_fare au HTML de chaque site.

import json
from datetime import datetime


class FareMonitor:
    def __init__(self, proxy=None):
        self.session = requests.Session()
        if proxy:
            self.session.proxies = {
                "http": f"http://{proxy}",
                "https": f"http://{proxy}"
            }
        self.fare_history = {}

    def check_fare(self, route):
        """Check fare for a route, solving CAPTCHAs if needed."""
        url = route["url"]

        response = self.session.get(url)

        # Detect CAPTCHA in response
        if self._has_recaptcha(response.text):
            sitekey = self._extract_sitekey(response.text)
            token = solve_recaptcha_v2(sitekey, url)
            response = self.session.post(url, data={
                "g-recaptcha-response": token,
                **route.get("params", {})
            })

        elif self._has_turnstile(response.text):
            sitekey = self._extract_turnstile_key(response.text)
            token = solve_turnstile(sitekey, url)
            response = self.session.post(url, data={
                "cf-turnstile-response": token,
                **route.get("params", {})
            })

        return self._parse_fare(response.text, route)

    def _has_recaptcha(self, html):
        return "g-recaptcha" in html or "recaptcha/api" in html

    def _has_turnstile(self, html):
        return "cf-turnstile" in html or "turnstile" in html

    def _extract_sitekey(self, html):
        # Extract data-sitekey from reCAPTCHA div
        if 'data-sitekey="' in html:
            start = html.index('data-sitekey="') + 14
            end = html.index('"', start)
            return html[start:end]
        return None

    def _extract_turnstile_key(self, html):
        if 'data-sitekey="' in html:
            idx = html.index("cf-turnstile")
            start = html.index('data-sitekey="', idx) + 14
            end = html.index('"', start)
            return html[start:end]
        return None

    def _parse_fare(self, html, route):
        """Parse fare data from the response. Customize per target site."""
        # Placeholder - implement per site
        return {
            "route": route["name"],
            "timestamp": datetime.now().isoformat(),
            "raw_length": len(html)
        }

    def monitor_routes(self, routes):
        """Check all routes and report price changes."""
        results = []
        for route in routes:
            try:
                fare = self.check_fare(route)
                results.append(fare)
                print(f"[OK] {route['name']}: checked")
            except Exception as e:
                print(f"[ERROR] {route['name']}: {e}")
        return results


# Usage
routes = [
    {
        "name": "NYC-LAX",
        "url": "https://example-airline.com/search?from=JFK&to=LAX&date=2025-08-15",
        "params": {"adults": 1}
    },
    {
        "name": "SFO-ORD",
        "url": "https://example-airline.com/search?from=SFO&to=ORD&date=2025-08-20",
        "params": {"adults": 1}
    }
]

monitor = FareMonitor(proxy="user:pass@proxy.example.com:8080")
results = monitor.monitor_routes(routes)

for r in results:
    print(json.dumps(r, indent=2))

Ne réduisez pas chaque contrôle à un simple succès ou échec. Deux signaux méritent d'être journalisés :

  • Le type de défi rencontré, ou son absence.
  • Le tarif extrait, pour distinguer une donnée réelle d'une page vide.

Erreurs fréquentes et correctifs

Problème Cause probable Action recommandée
CAPTCHA très fréquents Trop de requêtes depuis la même IP Utilisez des proxys résidentiels rotatifs
Prix obsolètes Réponses mises en cache Ajoutez des headers anti-cache ou faites varier certains paramètres
IP bloquée Rate limiting du site Augmentez le délai entre les contrôles et faites tourner les proxys
Échec de résolution CAPTCHA Mauvaise extraction du sitekey Vérifiez que le sitekey correspond bien au challenge présent sur la page

Planifier les contrôles

Déclenchez le moniteur avec cron ou un planificateur équivalent :

# Check fares every 6 hours
0 */6 * * * cd /path/to/project && python fare_monitor.py >> /var/log/fares.log 2>&1

FAQ

Combien de threads faut-il pour surveiller plusieurs routes ?

Un thread correspond à un défi en cours de résolution. Quelques threads suffisent en séquence ; comptez au moins un thread par contrôle simultané en parallèle. Le plan BASIC ($15/mois, 5 threads) couvre un petit portefeuille.

La surveillance des tarifs est-elle compatible avec le RGPD ?

Les prix des vols ne sont pas des données personnelles, mais restez sobre : ne stockez que la route, le prix et l'horodatage, et respectez les conditions d'utilisation du site cible.

Que faire si le token Turnstile est refusé après résolution ?

Le plus souvent, le pageurl envoyé à l'API ne correspond pas à la page réelle du défi, ou le token est réinjecté trop tard. Gardez un proxy cohérent entre résolution et requête finale ; le guide token Turnstile invalide détaille le reste.

CaptchaAI prend-il en charge hCaptcha sur les sites de voyage ?

Non — hCaptcha n'est pas pris en charge. Sur les portails aériens, vous rencontrerez surtout reCAPTCHA v2, Cloudflare Turnstile et des pages Cloudflare Challenge, tous couverts ici. Espacer les contrôles réduit leur fréquence, mais aucun réglage ne les supprime totalement.


Guides associés


Si vous surveillez un grand volume de routes, obtenez votre clé CaptchaAI et intégrez la résolution des défis comme une étape normale du pipeline plutôt que comme un cas d'exception.

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