Данные о статусе рейса устаревают быстрее, чем реагирует большинство трекеров: Cloudflare Turnstile и reCAPTCHA срабатывают именно при повторных запросах с одного IP — ровно тогда, когда нужен свежий ответ. Ниже — где появляется CAPTCHA и как встроить её решение в опрос статуса, не превращая скрипт в цепочку хрупких костылей.
Практическое правило: если один и тот же IP опрашивает один и тот же рейс чаще, чем раз в 5 минут, Cloudflare Turnstile почти наверняка включится. Это первое, что стоит проверить, прежде чем чинить парсер.
Где на порталах авиакомпаний и аэропортов встречается CAPTCHA
| Тип портала | CAPTCHA | Триггер |
|---|---|---|
| Страница статуса рейса авиакомпании | Cloudflare Turnstile | Частые запросы с одного IP |
| Табло вылета и прилёта аэропорта | Cloudflare Challenge | Обнаружение бота |
| Поисковики авиабилетов | reCAPTCHA v2/v3 | Отправка формы поиска |
| Проверка статуса бронирования | reCAPTCHA v2 | Перед показом маршрута |
| Страницы с ограничением частоты запросов API | Пользовательская CAPTCHA | После превышения лимита запросов |
Практический сценарий: агрегатор статусов рейсов для СНГ-рынка
Travel-агентство с клиентами в России, Беларуси и Казахстане опрашивает 40–60 рейсов в час с серверов в Европе (собственная инфраструктура в РФ для внешних API-запросов обычно нестабильна из-за блокировок и скачков задержки). При интервале 15 минут доля запросов, упирающихся в CAPTCHA, держится на уровне 5–10% — это укладывается в STANDARD-план CaptchaAI. Основные направления мониторинга — рейсы через Стамбул, Дубай и Ереван как транзитные хабы для рынков РФ и СНГ, где авиакомпании особенно чувствительны к автоматическим запросам с одного диапазона IP. Если сервис хранит данные о пассажирах, а не только статус рейса, стоит свериться с 152-ФЗ (для операторов, работающих с гражданами РФ) или GDPR (если аудитория — ЕС).
Архитектура скрипта мониторинга рейсов
Базовый класс ниже держит одну сессию на весь цикл проверок, распознаёт и Cloudflare Turnstile, и reCAPTCHA по содержимому ответа, решает капчу через CaptchaAI и повторяет запрос — без пересоздания сессии на каждый вызов.
import requests
import time
from datetime import datetime
class FlightMonitor:
def __init__(self, api_key):
self.api_key = api_key
self.session = requests.Session()
self.session.headers.update({
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
})
def check_flight(self, airline_url, flight_number):
"""Check flight status, handling CAPTCHAs if encountered."""
response = self.session.get(
f"{airline_url}/flight-status/{flight_number}"
)
if self._is_captcha_page(response):
response = self._solve_and_retry(response, airline_url)
return self._parse_flight_data(response.text)
def _is_captcha_page(self, response):
return (
response.status_code == 403 or
"cf-turnstile" in response.text or
"g-recaptcha" in response.text
)
def _solve_and_retry(self, response, url):
import re
# Detect CAPTCHA type
if "cf-turnstile" in response.text:
match = re.search(r'data-sitekey="(0x[^"]+)"', response.text)
token = self._solve_turnstile(match.group(1), url)
field = "cf-turnstile-response"
else:
match = re.search(r'data-sitekey="([^"]+)"', response.text)
token = self._solve_recaptcha(match.group(1), url)
field = "g-recaptcha-response"
return self.session.post(url, data={field: token})
def _solve_turnstile(self, site_key, page_url):
resp = requests.post("https://ocr.captchaai.com/in.php", data={
"key": self.api_key,
"method": "turnstile",
"sitekey": site_key,
"pageurl": page_url,
"json": 1
})
task_id = resp.json()["request"]
return self._poll_result(task_id)
def _solve_recaptcha(self, site_key, page_url):
resp = requests.post("https://ocr.captchaai.com/in.php", data={
"key": self.api_key,
"method": "userrecaptcha",
"googlekey": site_key,
"pageurl": page_url,
"json": 1
})
task_id = resp.json()["request"]
return self._poll_result(task_id)
def _poll_result(self, task_id):
for _ in range(60):
time.sleep(3)
result = requests.get("https://ocr.captchaai.com/res.php", params={
"key": self.api_key,
"action": "get",
"id": task_id,
"json": 1
})
data = result.json()
if data["status"] == 1:
return data["request"]
raise TimeoutError("CAPTCHA solve timed out")
def _parse_flight_data(self, html):
# Parse flight status from HTML
from bs4 import BeautifulSoup
soup = BeautifulSoup(html, "html.parser")
def text_or_none(node):
return node.text.strip() if node and node.text else None
return {
"status": text_or_none(soup.select_one(".flight-status")),
"departure": text_or_none(soup.select_one(".departure-time")),
"arrival": text_or_none(soup.select_one(".arrival-time")),
"gate": text_or_none(soup.select_one(".gate-info")),
"checked_at": datetime.now().isoformat()
}
Периодический опрос статуса с обработкой CAPTCHA
Дальше — обвязка вокруг FlightMonitor, которая опрашивает рейс с заданным интервалом, копит историю проверок и печатает изменение статуса, как только оно появляется.
def monitor_flight(monitor, airline_url, flight_number,
interval_seconds=300, max_checks=48):
"""Monitor a flight every N seconds, handling CAPTCHAs as needed."""
history = []
for check_num in range(max_checks):
try:
status = monitor.check_flight(airline_url, flight_number)
history.append(status)
# Alert on changes
if len(history) > 1 and status["status"] != history[-2]["status"]:
print(f"Status changed: {history[-2]['status']} → {status['status']}")
print(f"Check {check_num + 1}: {status['status']} "
f"(Gate: {status.get('gate', 'Coming soon')})")
except Exception as e:
print(f"Check {check_num + 1} failed: {e}")
time.sleep(interval_seconds)
return history
# Usage
monitor = FlightMonitor("YOUR_API_KEY")
monitor_flight(monitor, "https://airline.example.com", "AA1234")
Мониторинг нескольких авиакомпаний на JavaScript
Если рейсы нужно отслеживать сразу у нескольких авиакомпаний, удобнее держать один трекер с картой активных рейсов, чем плодить отдельные скрипты под каждый портал — ниже вариант на JavaScript с тем же принципом решения Turnstile just-in-time.
class FlightTracker {
constructor(apiKey) {
this.apiKey = apiKey;
this.flights = new Map();
}
async addFlight(airline, flightNumber, checkUrl) {
this.flights.set(flightNumber, {
airline,
url: checkUrl,
history: [],
lastCheck: null
});
}
async checkAll() {
const results = [];
for (const [flightNum, flight] of this.flights) {
try {
const status = await this.checkFlight(flight.url, flightNum);
flight.history.push(status);
flight.lastCheck = new Date();
results.push({ flight: flightNum, ...status });
} catch (error) {
results.push({ flight: flightNum, error: error.message });
}
}
return results;
}
async checkFlight(url, flightNumber) {
const response = await fetch(`${url}/status/${flightNumber}`);
const html = await response.text();
// Check for CAPTCHA
if (html.includes('cf-turnstile') || response.status === 403) {
return this.solveAndRetry(url, flightNumber, html);
}
return this.parseStatus(html);
}
async solveAndRetry(url, flightNumber, html) {
const siteKeyMatch = html.match(/data-sitekey="(0x[^"]+)"/);
if (!siteKeyMatch) throw new Error('No sitekey found');
const token = await this.solveTurnstile(siteKeyMatch[1], url);
const response = await fetch(`${url}/status/${flightNumber}`, {
method: 'POST',
body: new URLSearchParams({ 'cf-turnstile-response': token })
});
return this.parseStatus(await response.text());
}
}
Часовые пояса и международные рейсы
Для пользователей из России, Беларуси и Казахстана большинство отслеживаемых рейсов — международные: Москва, Стамбул, Дубай, Алматы находятся в разных часовых поясах, и порталы авиакомпаний обычно отдают время вылета в местном времени аэропорта, а не в UTC. Если сравнивать departure_time между проверками без нормализации в UTC, скрипт может решить, что статус рейса "изменился", хотя на самом деле сместился только формат отображения времени. Правило простое: приводите checked_at и все временные метки из ответа портала к UTC сразу при парсинге (datetime.fromisoformat(...).astimezone(timezone.utc)), и только потом сравнивайте историю проверок между собой — это устраняет ложные срабатывания алертов при смене летнего/зимнего времени в Европе или при работе с рейсами, пересекающими несколько часовых поясов.
Как выбрать интервал проверки, чтобы не спровоцировать CAPTCHA
Интервал проверки — главный рычаг, которым вы управляете:
| Частота проверки | Частота CAPTCHA | Рекомендация |
|---|---|---|
| Каждую минуту | Высокая (50–80%) | Слишком агрессивно |
| Каждые 5 минут | Умеренная (10–30%) | Для критических рейсов |
| Каждые 15 минут | Низкая (5–10%) | Баланс для повседневного мониторинга |
| Каждые 30 минут | Очень низкая (<5%) | Для долгосрочного отслеживания |
| Раз в час | Минимальная (<1%) | CAPTCHA срабатывает редко |
Оптимизация сессии: меньше CAPTCHA, стабильнее данные
Большинство лишних CAPTCHA — не результат защиты портала от парсинга как такового, а реакция на нестабильную сессию: новый User-Agent, новый IP или потерянный cookie при каждом запросе выглядят для Cloudflare как новый посетитель. Четыре вещи стабилизируют сессию:
- Сохраняйте cookie между проверками. Cloudflare
cf_clearanceдействителен 15–30 минут — переиспользованиеrequests.Session()избавляет от повторного challenge на каждый опрос. - Используйте один и тот же User-Agent на всём протяжении мониторинга рейса. Смена UA между запросами Cloudflare трактует как признак бота и запускает новую проверку.
- Держите прокси постоянным для одного рейса или клиента. Один и тот же IP на протяжении сессии снижает подозрительность сильнее, чем ротация прокси на каждый запрос.
- Распределяйте запросы во времени, а не отправляйте их пачками. Всплеск из 10–20 проверок за секунду провоцирует ограничение частоты быстрее, чем то же количество запросов, растянутое на минуту.
Типичные проблемы при мониторинге и как их исправить
Если мониторинг рейса начал сбоить, почти всегда причина — одна из четырёх:
- CAPTCHA появляется на каждой проверке. Причина — сессия не сохраняется между запросами. Переиспользуйте
requests.Session()вместо создания нового соединения на каждый опрос. - Ошибка 1020 (блокировка Cloudflare). Причина — слишком много запросов с одного IP за короткое время. Увеличьте интервал проверки или разнесите нагрузку по нескольким прокси.
- Данные устарели сразу после решения CAPTCHA. Причина — токен истёк за время между решением и фактическим запросом. Решайте CAPTCHA just-in-time, непосредственно перед запросом, а не заранее.
- Данные отличаются от того, что видно в браузере. Причина — ответ не рендерит JavaScript, а портал подгружает статус динамически. Используйте браузерную автоматизацию для таких сайтов вместо прямых HTTP-запросов.
Частые вопросы
Как часто проверять статус рейса, чтобы не словить блокировку по IP?
5–15 минут: реже CAPTCHA почти не появляется, раз в минуту — уже агрессивно.
Сколько потоков CaptchaAI нужно для 50 рейсов одновременно?
При интервале 15 минут и доле CAPTCHA 5–10% обычно хватает STANDARD ($30/мес, 15 потоков).
Почему после решения Turnstile страница отдаёт старые данные?
Токен истёк между решением и запросом. Решайте CAPTCHA непосредственно перед запросом, не кешируйте токен между проверками.
Мониторинг рейсов отечественных авиакомпаний работает так же, как международных?
Да, тот же принцип: любой портал, который часто опрашивают с одного диапазона IP, рано или поздно включает CAPTCHA или ограничение частоты запросов — это не специфика конкретных авиакомпаний. Стабильная сессия и разумный интервал проверки (см. раздел «Оптимизация сессии») снижают долю блокировок сильнее, чем смена прокси.