Use Cases

Surveillance des tarifs de voyage avec gestion CAPTCHA

Un moniteur de tarifs échoue rarement sur le parsing HTML : il échoue sur un défi CAPTCHA affiché à la place de la grille de prix, après quelques heures de relevés réguliers. La parade tient en deux réglages : espacer les passages pour rester sous le seuil de déclenchement, et résoudre le défi dans la session en cours plutôt que relancer un scraper aveugle. Cet article assemble les deux avec l'API CaptchaAI, du script Python à la cadence cron et au budget en threads.

Périmètre : relevés espacés sur des pages de tarifs publiques, pour votre veille commerciale ou vos tests de QA. Vérifiez les conditions d'utilisation du site visé et, côté RGPD, ne collectez aucune donnée personnelle.

Pourquoi un moniteur de tarifs s'éteint au bout de quelques heures

Les sites de réservation ne bloquent pas la veille tarifaire par principe : ils protègent des moteurs de disponibilité coûteux en calcul. Trois signaux déclenchent presque toujours le défi : une cadence régulière à la seconde près, une session sans cookie de navigation préalable, et une IP datacenter qui interroge dix itinéraires en trente secondes.

Un moniteur ne doit donc pas seulement résoudre un CAPTCHA : il doit le résoudre dans la même session, puis réutiliser cette session pour les relevés suivants. Un token injecté dans une session neuve à chaque passage double la charge et fait recroiser le défi plus vite.

Quels défis CAPTCHA attendre selon le type de site

Catégorie de site Type de CAPTCHA Difficulté
Compagnies aériennes (sites directs) reCAPTCHA v3, Cloudflare Moyenne
OTA (Expedia, Booking) reCAPTCHA v2, Cloudflare Turnstile Moyenne à élevée
Méta-moteurs (Google Flights, Kayak) reCAPTCHA v3 Moyenne
Compagnies low cost CAPTCHA image, reCAPTCHA Faible à moyenne
Agrégateurs hôteliers Cloudflare Challenge Élevée

Ces familles sont prises en charge par CaptchaAI : reCAPTCHA v3 se résout en moins de 4 s, Cloudflare Turnstile en moins de 10 s et Cloudflare Challenge en moins de 15 s, avec un taux de réussite élevé. hCaptcha et FunCaptcha (Arkose Labs), en revanche, ne sont pas encore pris en charge : prévoyez une source alternative pour ces axes.

Construire le moniteur de tarifs en Python

Le script ci-dessous récupère la page, détecte le type de défi dans le HTML (data-sitekey pour reCAPTCHA, cf-turnstile pour Turnstile), envoie la tâche à in.php puis interroge res.php jusqu'au token. L'historique reste en mémoire : de quoi calculer les baisses de prix sans base de données.

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

API_KEY = os.environ["CAPTCHAAI_API_KEY"]


def solve_captcha(params):
    params["key"] = API_KEY
    resp = requests.get("https://ocr.captchaai.com/in.php", params=params)
    if not resp.text.startswith("OK|"):
        raise Exception(f"Submit: {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(f"Solve: {result.text}")
    raise TimeoutError()


class FareMonitor:
    def __init__(self):
        self.session = requests.Session()
        self.session.headers["User-Agent"] = (
            "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
            "AppleWebKit/537.36 Chrome/120.0.0.0"
        )
        self.history = []

    def fetch_with_captcha(self, url):
        """Fetch a travel page, solving CAPTCHAs if encountered."""
        resp = self.session.get(url)

        # reCAPTCHA v2/v3
        match = re.search(
            r'data-sitekey=["\']([A-Za-z0-9_-]+)["\']', resp.text
        )
        if match:
            site_key = match.group(1)

            # Detect v3 vs v2
            if "recaptcha/api.js?render=" in resp.text:
                token = solve_captcha({
                    "method": "userrecaptcha",
                    "googlekey": site_key,
                    "pageurl": url,
                    "version": "v3",
                    "action": "search",
                })
            else:
                token = solve_captcha({
                    "method": "userrecaptcha",
                    "googlekey": site_key,
                    "pageurl": url,
                })

            resp = self.session.post(url, data={
                "g-recaptcha-response": token,
            })

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

        return resp.text

    def check_fares(self, routes):
        """Check fares for a list of routes."""
        results = []
        for route in routes:
            try:
                html = self.fetch_with_captcha(route["url"])
                prices = self._extract_prices(html)

                result = {
                    "route": f"{route['origin']}-{route['destination']}",
                    "date": route["date"],
                    "prices": prices,
                    "min_price": min(prices) if prices else None,
                    "timestamp": datetime.utcnow().isoformat(),
                }
                results.append(result)
                self.history.append(result)

                if prices:
                    print(f"  {result['route']} ({route['date']}): "
                          f"${min(prices)}-${max(prices)}")
                else:
                    print(f"  {result['route']}: No prices found")

                time.sleep(3)  # Respectful delay

            except Exception as e:
                print(f"  {route.get('origin', '?')}-"
                      f"{route.get('destination', '?')}: ERROR - {e}")

        return results

    def _extract_prices(self, html):
        """Extract prices from travel page HTML."""
        prices = []
        # Common price patterns
        for match in re.finditer(
            r'\$\s*([\d,]+(?:\.\d{2})?)', html
        ):
            price = float(match.group(1).replace(",", ""))
            if 20 < price < 10000:  # Filter noise
                prices.append(price)
        return sorted(set(prices))

    def detect_price_drops(self, threshold_pct=5):
        """Detect significant price drops in history."""
        route_prices = {}
        for entry in self.history:
            key = f"{entry['route']}_{entry['date']}"
            if key not in route_prices:
                route_prices[key] = []
            if entry["min_price"]:
                route_prices[key].append(entry["min_price"])

        alerts = []
        for key, prices in route_prices.items():
            if len(prices) >= 2:
                prev = prices[-2]
                current = prices[-1]
                change_pct = ((current - prev) / prev) * 100
                if change_pct < -threshold_pct:
                    alerts.append({
                        "route": key,
                        "previous": prev,
                        "current": current,
                        "change": f"{change_pct:.1f}%",
                    })

        return alerts

    def export_report(self, filename="fare_report.json"):
        """Export fare history to JSON."""
        with open(filename, "w") as f:
            json.dump(self.history, f, indent=2)
        print(f"Exported {len(self.history)} fare checks to {filename}")


# Define routes to monitor
routes = [
    {
        "origin": "JFK",
        "destination": "LAX",
        "date": "2025-03-15",
        "url": "https://example-airline.com/flights?from=JFK&to=LAX&date=2025-03-15",
    },
    {
        "origin": "SFO",
        "destination": "ORD",
        "date": "2025-03-20",
        "url": "https://example-airline.com/flights?from=SFO&to=ORD&date=2025-03-20",
    },
]

monitor = FareMonitor()
results = monitor.check_fares(routes)
monitor.export_report()

Planifier les relevés sans matraquer le site

La cadence est le premier levier anti-blocage, avant même la résolution des défis. Un passage toutes les quatre heures suffit pour la plupart des axes : les compagnies républient leurs grilles par lots, pas en continu. Décalez les itinéraires de quelques minutes plutôt que de lancer cinquante requêtes à la même seconde, et gardez le délai de courtoisie de check_fares.

# Check fares every 4 hours
0 */4 * * * cd /opt/fare-monitor && python fare_monitor.py

Dimensionner le budget : raisonner en threads, pas en résolutions

La facturation CaptchaAI porte sur les threads simultanés, avec des résolutions illimitées par thread. La question n'est donc pas « combien de CAPTCHA vais-je résoudre ? » mais « combien de défis auront lieu au même instant ? ». Un cron qui traite ses itinéraires en série n'occupe qu'une poignée de threads.

Profil de veille Itinéraires suivis Relevés par jour Défis attendus par jour Formule adaptée
Veille personnelle 5 6 par itinéraire ~30 BASIC ($15/mois, 5 threads)
Petite agence 50 4 par itinéraire ~200 STANDARD ($30/mois, 15 threads)
Plateforme métier 500 6 par itinéraire ~3 000 ADVANCE ($90/mois, 50 threads)

Les volumes ci-dessus reposent sur des mesures observées et des retours d'utilisateurs ; les résultats varient selon l'environnement, le volume et le moment de la journée. La facturation est en dollars US.

Scénario : une agence lyonnaise qui suit 40 axes long-courriers

Une agence d'affaires lyonnaise surveille quarante axes Europe–Afrique de l'Ouest pour renégocier ses contrats corporate chaque trimestre. Le moniteur tourne chez un hébergeur européen, avec un passage à 6 h, 12 h, 18 h et minuit : les sites des compagnies servent surtout du reCAPTCHA v3, les deux OTA utilisées basculent en Cloudflare Turnstile dès le troisième relevé.

Le bilan après quelques semaines : la plupart des passages n'affichent aucun défi, et la formule STANDARD suffit parce que les quarante axes sont traités en série, avec trois threads occupés au maximum. Les montants restent dans la devise affichée par le site : comparer deux captures faites dans des devises différentes reste la première source de fausses alertes de baisse. Seuls l'itinéraire, la date et le montant sont conservés, ce qui garde la démarche simple à documenter côté RGPD.

Dépannage des pannes les plus fréquentes

Problème Cause probable Correctif
Le token est accepté mais la page réaffiche le défi Token injecté dans une session neuve Réutiliser l'objet Session et ses cookies après la résolution
CAPCHA_NOT_READY en boucle jusqu'au timeout Interrogation trop rapprochée ou charge élevée sur le type Garder l'intervalle de 5 s et laisser la boucle aller à son terme
Aucun prix extrait alors que la page s'affiche Grille rendue en JavaScript après le chargement Récupérer la page avec Selenium ou Playwright, puis résoudre le défi via l'API
Alertes de baisse incohérentes Devise ou marché qui change selon l'IP sortante Fixer le marché dans l'URL et garder un proxy stable par itinéraire
Défis de plus en plus fréquents sur un même axe Cadence trop régulière, empreinte de session identique Décaler les passages de quelques minutes et alterner les proxys résidentiels

FAQ

Quelle formule choisir pour surveiller 200 itinéraires par jour ?

STANDARD ($30/mois, 15 threads) suffit dans la plupart des cas. Le facteur limitant est la simultanéité, pas le volume : tant que le cron traite les itinéraires par petits lots, quinze threads absorbent les défis rencontrés.

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

Non — hCaptcha n'est pas encore pris en charge, pas plus que FunCaptcha (Arkose Labs). Si un site de votre périmètre bascule sur l'un de ces types, prévoyez une source de données alternative pour cet axe.

Faut-il des proxys résidentiels pour la veille tarifaire ?

Pas systématiquement. Un proxy datacenter stable convient pour des relevés espacés ; les proxys résidentiels deviennent utiles quand vous suivez plusieurs marchés géographiques ou que le défi revient à chaque passage.

Comment relever un prix sur une page rendue en JavaScript ?

Chargez la page dans Selenium ou Playwright, laissez le rendu se terminer, puis envoyez le sitekey à l'API et injectez le token avant de lire la grille.

Guides connexes

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