Para que un monitor de tarifas aéreas siga capturando precios cuando el sitio interpone un CAPTCHA, hay que resolver ese desafío en el momento y reinyectar el token en la misma petición. Ese es todo el truco: no rendirse ante el reCAPTCHA ni ante la página intersticial de Cloudflare, sino delegar la resolución a un servicio y continuar con la extracción. En esta guía montas ese flujo completo en Python con CaptchaAI.
Piensa en una agencia de Ciudad de México que vigila la ruta MEX–MAD en varias aerolíneas cada pocas horas: en cuanto la frecuencia de solicitudes sube, empiezan a aparecer desafíos reCAPTCHA, límites de solicitudes por IP y páginas de Cloudflare. CaptchaAI se encarga del paso de resolución para que tu pipeline de precios no se detenga en ese muro.
El montaje tiene tres piezas que verás a lo largo de la guía:
- una función que delega el desafío a CaptchaAI y espera el token de vuelta;
- un monitor que detecta qué tipo de CAPTCHA trae la página y reinyecta ese token en la misma sesión;
- una tarea programada que repite la comprobación a un ritmo que no dispare bloqueos.
Cómo funciona el flujo de monitoreo
Schedule check → Request fare page → CAPTCHA detected?
↓ Yes
Solve via CaptchaAI → Inject token → Retry request
↓ No
Parse fare data → Store → Alert on price change
La idea es sencilla y cada comprobación recorre siempre las mismas etapas:
- pides la página de tarifas con tu sesión activa;
- si la respuesta trae un reCAPTCHA o un Turnstile, lo resuelves y reintentas con el token inyectado;
- cuando la página llega limpia, extraes el precio, lo guardas en el histórico y avisas solo si cambió respecto a la última lectura.
Antes de empezar: lo que necesitas
Antes de escribir código, ten a mano estos tres elementos; sirven igual para una ruta doméstica o una intercontinental.
| Requisito | Detalles |
|---|---|
| API key de CaptchaAI | Crea tu cuenta en captchaai.com |
| Python 3.8+ | Con requests |
| Proxy | Red residencial autorizada para sitios de viajes |
pip install requests
CaptchaAI factura por thread concurrente, no por resolución, así que un monitor pequeño arranca de sobra con el plan BASIC ($15/mes, 5 threads) y escalas los threads cuando añades más rutas. Para decidir cuándo subir de plan, mira el paralelismo, no el número de resoluciones:
- unas pocas rutas comprobadas de forma escalonada caben en los 5 threads del plan BASIC;
- si lanzas muchas rutas a la vez o reduces el intervalo, sube de plan para tener más threads en vuelo;
- las resoluciones son ilimitadas dentro del plan, así que el gasto no crece con cada CAPTCHA extra.
Función de ayuda para resolver el CAPTCHA
Este par de funciones envía el desafío a CaptchaAI y sondea el resultado hasta recibir el token. Una cubre reCAPTCHA v2 y otra Cloudflare Turnstile, los dos tipos que verás con más frecuencia en portales de aerolíneas.
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")
Fíjate en los nombres de los campos de token: reCAPTCHA devuelve g-recaptcha-response y Turnstile cf-turnstile-response. Cada uno viaja en su propia clave cuando reenvías la petición.
El monitor de tarifas con resolución integrada
La clase FareMonitor une todo: abre una sesión (opcionalmente detrás de un proxy), detecta qué tipo de CAPTCHA trae la página, lo resuelve y vuelve a enviar la solicitud con el token ya inyectado antes de leer el precio.
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 common airline result layouts."""
from bs4 import BeautifulSoup
import re
soup = BeautifulSoup(html, "html.parser")
selectors = [
"[data-testid='price-value']",
".fare-amount",
".price-total",
".booking-price",
]
price_text = ""
for selector in selectors:
el = soup.select_one(selector)
if el and el.get_text(strip=True):
price_text = el.get_text(strip=True)
break
amount_match = re.search(r"(\d[\d,\.]+)", price_text or html)
currency_match = re.search(r"(USD|EUR|GBP|\$|€|£)", price_text or html)
return {
"route": route["name"],
"carrier": route.get("carrier", "unknown"),
"price_text": price_text or None,
"amount": amount_match.group(1) if amount_match else None,
"currency": currency_match.group(1) if currency_match else route.get("currency"),
"captured_at": datetime.now().isoformat(),
"source_url": route["url"],
}
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))
Ajusta la lista de selectores de _parse_fare al maquetado de cada aerolínea: cada sitio expone el precio en una clase o atributo distinto, y ese método es el único que cambias por destino.
Programar las comprobaciones con cron
Ejecuta el monitor a intervalos con cron o cualquier programador de tareas. Cada cuánto conviene lanzarlo depende de tu tolerancia a los CAPTCHA y al gasto de proxy, no de la velocidad a la que cambien las tarifas:
# Check fares every 6 hours
0 */6 * * * cd /path/to/project && python fare_monitor.py >> /var/log/fares.log 2>&1
Redirige la salida a un log como en el ejemplo: así ves de un vistazo qué rutas se comprobaron y dónde saltó un CAPTCHA o un error.
Resolución de problemas frecuentes
Casi todos los tropiezos de un monitor de tarifas caen en cuatro categorías. Contrasta el síntoma con esta tabla:
| Problema | Causa | Solución |
|---|---|---|
| CAPTCHA muy seguidos | Demasiadas solicitudes desde la misma IP | Usa proxies residenciales rotativos |
| Precios obsoletos | Páginas servidas desde caché | Añade encabezados anti-caché o aleatoriza los parámetros de la solicitud |
| IP bloqueada | Rate limiting del sitio | Aumenta los tiempos de espera entre comprobaciones y rota los proxies |
| Falla la resolución del CAPTCHA | Extracción incorrecta del sitekey | Verifica que el sitekey coincida con el CAPTCHA de la página |
Dos hábitos evitan la mayoría de estos casos antes de que aparezcan:
- reparte las comprobaciones en el tiempo en lugar de lanzarlas todas de golpe;
- registra el sitekey extraído en el log para detectar de inmediato cuándo cambia el maquetado de una aerolínea.
Preguntas frecuentes
¿Cuánto cuesta resolver los CAPTCHA de un monitor de tarifas?
Depende del volumen, no del número de resoluciones. CaptchaAI cobra por thread concurrente con resoluciones ilimitadas dentro del plan, así que un monitor de unas pocas rutas funciona con el plan BASIC ($15/mes, 5 threads). Para agencias que facturan en monedas locales volátiles, el costo mensual fijo en USD es más fácil de presupuestar que un pago por resolución.
¿Cómo evito que bloqueen mi IP al vigilar varias rutas?
Reparte la carga en lugar de concentrarla. En la práctica, tres medidas bastan:
- rota entre varias salidas de red residenciales autorizadas;
- separa las rutas en el tiempo y no las lances todas en el mismo segundo;
- mantén un intervalo de 4 a 8 horas por ruta, que suele dejar la frecuencia de solicitudes por debajo del umbral que dispara bloqueos.
¿Puedo recibir un aviso cuando baja el precio de un vuelo?
Sí, con tres pasos sobre el código que ya tienes:
- guarda cada resultado en
fare_historyjunto con su fecha de captura; - compara el nuevo
amountcon el último registrado para esa ruta; - dispara la notificación (correo, webhook o mensaje) solo cuando la diferencia supere tu umbral, para no recibir avisos por variaciones mínimas.
¿Funciona si el sitio usa reCAPTCHA v3 en lugar de v2?
Sí. CaptchaAI resuelve reCAPTCHA v2 y v3; para v3 ajustas el method y pasas la acción y el min_score esperados. El resto del flujo —detectar, resolver, reinyectar el token— es idéntico.
¿Cómo manejo las páginas de Cloudflare Challenge?
Usa method=cloudflare_challenge con un proxy: la cookie que devuelve te da acceso al sitio, y luego repites la petición dentro de la misma sesión. Tienes el detalle en la guía de Cloudflare Challenge.
Empieza a monitorear tarifas con CaptchaAI
Crea tu cuenta en captchaai.com, integra las funciones de resolución en tu pipeline y deja que el CAPTCHA deje de frenar tu seguimiento de precios. Respeta siempre los términos de servicio de cada sitio y la normativa de protección de datos aplicable.