Use Cases

Мониторинг тарифов авиакомпаний с обработкой CAPTCHA

Если вы уже пробовали автоматически проверять тарифы авиакомпаний по расписанию, то знаете: циклический опрос быстро упирается в reCAPTCHA v2 или Cloudflare Turnstile — сайты реагируют на повторяющиеся запросы с одного IP уже после нескольких обращений. Решение не в отказе от автоматизации, а в отдельном шаге решения CAPTCHA внутри конвейера. Именно его берёт на себя CaptchaAI: получает токен reCAPTCHA или Turnstile через API, пока скрипт мониторинга продолжает собирать данные о ценах.

Ниже — рабочий процесс мониторинга, который сам обнаруживает и решает CAPTCHA во время проверки цен: от запроса по расписанию до алерта об изменении цены.

Особенно заметно это на маршрутах из России, Казахстана и Беларуси в Европу: сайт авиакомпании видит нетипичный всплеск запросов с одного диапазона IP и подключает reCAPTCHA или Turnstile быстрее, чем при обычном браузерном трафике. Дальше вопрос не в том, чтобы полностью убрать проверку из процесса, а в том, чтобы решать её штатным API-запросом и не терять окно мониторинга.


Как устроен пайплайн мониторинга

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

Каждый шаг пайплайна отвечает за одну задачу:

  1. Планировщик по расписанию запускает проверку маршрута.
  2. Скрипт запрашивает страницу тарифа обычным HTTP-запросом.
  3. Если в ответе обнаружена CAPTCHA — переход к решению через CaptchaAI, иначе сразу к парсингу.
  4. CaptchaAI возвращает токен, скрипт подставляет его в исходный запрос и повторяет его.
  5. Тариф парсится, сохраняется в истории, и при изменении цены уходит алерт.

Что понадобится для запуска

  • API-ключ CaptchaAI — получите на captchaai.com.
  • Python 3.8+ — с библиотекой requests.
  • Прокси — резидентный прокси для туристических сайтов.
pip install requests

Совет: если мониторите больше 10–15 маршрутов одновременно, сразу берите пул из нескольких резидентных прокси на ротации — один статический IP с таким объёмом запросов сайты авиакомпаний блокируют в течение первого дня.


Функции для решения CAPTCHA через API CaptchaAI

Две функции ниже отправляют задачу в in.php (reCAPTCHA v2 или Turnstile), затем опрашивают res.php каждые 5 секунд, пока не получат токен или не истечёт тайм-аут.

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")

Класс FareMonitor: логика проверки тарифов

FareMonitor запрашивает страницу маршрута, проверяет, не подставлена ли вместо тарифа CAPTCHA, при необходимости решает её и только затем парсит цену. Токен reCAPTCHA v2 идёт в поле g-recaptcha-response, токен Turnstile — в cf-turnstile-response; имена полей менять нельзя, иначе сайт отклонит запрос.

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:[email protected]:8080")
results = monitor.monitor_routes(routes)

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

Что делает каждый метод класса

  • check_fare — запрашивает страницу маршрута и решает, нужен ли вызов CaptchaAI, прежде чем парсить ответ.
  • _has_recaptcha / _has_turnstile — определяют тип CAPTCHA по маркерам в HTML, не дожидаясь явной ошибки.
  • _extract_sitekey / _extract_turnstile_key — вытаскивают sitekey из разметки страницы для передачи в CaptchaAI.
  • _parse_fare — точка расширения: замените заглушку на реальный парсинг тарифа под конкретный сайт.
  • monitor_routes — прогоняет весь список маршрутов и логирует результат по каждому, не прерывая цикл при ошибке одного маршрута.

Типичные проблемы и решения

Прежде чем ставить монитор на расписание, стоит один раз прогнать его вручную и свериться с этим списком:

  • CAPTCHA появляется слишком часто — обычно это много запросов с одного IP; используйте ротацию резидентных прокси.
  • Тариф не обновляется — страница отдаётся из кеша; добавьте параметр, сбивающий кеш, или рандомизируйте query-параметры.
  • IP заблокирован — сработало ограничение частоты запросов; увеличьте паузы между проверками и ротируйте прокси.
  • CAPTCHA не решается — sitekey извлечён неверно; убедитесь, что он соответствует виджету CAPTCHA на странице.

Расписание запуска через cron

Запускайте монитор по расписанию — через cron или системный планировщик задач:

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

Чек-лист сбора данных о тарифах

  • Сохраняйте базовый тариф, налоги, валюту и класс тарифа в одной записи — так проще сравнивать историю цен позже.
  • Держите минимум два запасных селектора и сохраняйте сырой HTML-снимок страницы на случай, если парсинг не сработает.
  • Логируйте маршрут, дату вылета, время проверки и задержку решения CAPTCHA при каждом запуске.

Агентство, которое отслеживает тарифы по десяткам маршрутов из Москвы, Алматы или Минска в Европу и Азию, обычно фиксирует именно этот набор полей — этого достаточно, чтобы поймать разовое падение цены и не спутать его со сбоем парсинга.

Вопросы про мониторинг тарифов авиакомпаний

Сколько потоков CaptchaAI нужно для мониторинга нескольких маршрутов одновременно?

Один поток решает одну CAPTCHA единовременно. Для 20–50 маршрутов с интервалом в несколько часов обычно хватает STANDARD ($30/мес, 15 потоков); при более плотном расписании или росте числа маршрутов берите ADVANCE ($90/мес, 50 потоков).

Как отличить реальное изменение тарифа от сбоя парсинга?

Сохраняйте вместе с каждым тарифом сырой HTML-снимок и код ответа. Если цена резко изменилась, а снимок выглядит иначе — другая вёрстка, ошибка 403 или заглушка вместо результатов — это сбой парсинга, а не реальное движение цены.

Какие типы CAPTCHA чаще всего встречаются на сайтах авиакомпаний?

Чаще всего — reCAPTCHA v2 и Cloudflare Turnstile, реже — полноэкранная страница Cloudflare Challenge («Checking your browser…») и изредка CAPTCHA с изображениями.

Как обрабатывать Cloudflare Challenge в этом пайплайне?

Cloudflare Challenge — не Turnstile: это полноэкранная страница вместо встроенного виджета. Отправьте задачу с method=cloudflare_challenge и обязательным прокси; в ответ придёт cookie cf_clearance, которую нужно подставить в сессию вместе с тем же прокси и User-Agent — иначе Cloudflare покажет проверку снова. Подробнее — в руководстве по решению Cloudflare Challenge.

Что делать, если авиакомпания меняет вёрстку страницы с тарифами?

Обновите _parse_fare и селекторы, держите про запас резервный вариант — сохранённый HTML-снимок из чек-листа сразу покажет, что изменилось в разметке.


Начните мониторинг тарифов с CaptchaAI

Получите API-ключ на captchaai.com и подключите решение CAPTCHA к пайплайну проверки тарифов.


Связанные руководства

Комментарии для этой статьи отключены.