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_v2pour reCAPTCHA v2 (method=userrecaptcha).solve_turnstilepour 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
- Suivi des tarifs voyage à grande échelle
- Faire tourner les proxys pour le scraping avec CAPTCHA
- Réduire les interruptions CAPTCHA dans le web scraping
- Token Turnstile invalide après résolution
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.