Use Cases

Сравнение цен на доставку еды с решением CAPTCHA

Одна и та же строка меню может стоить по-разному на DoorDash, Uber Eats и Grubhub: рестораны закладывают в цену комиссию платформы, которая обычно составляет 15–30 %. Чтобы отслеживать эту разницу автоматически, инструменту сравнения цен сначала нужно пройти CAPTCHA и защиту от ботов, которую сервисы доставки ставят перед страницами с меню, стоимостью доставки и промо-акциями.

Сервисам мониторинга цен, ресторанным аналитикам и агрегаторам нужен предсказуемый способ проходить эту защиту, не переписывая парсер каждый раз, когда платформа меняет тип проверки.


CAPTCHA на платформах доставки еды

Ниже — актуальные типы защиты на популярных сервисах доставки и какие данные они закрывают от автоматического доступа.

Платформа Тип CAPTCHA Триггер Защищённые данные
DoorDash reCAPTCHA v3 + Cloudflare Обнаружение ботов Меню, цены, комиссии
Uber Eats Cloudflare Turnstile Автоматизированный доступ Список ресторанов, цены
Grubhub reCAPTCHA v2 Ограничение частоты запросов Позиции меню, акции
Postmates Cloudflare Challenge Обнаружение парсинга Стоимость доставки, расчётное время прибытия
Just Eat reCAPTCHA v2 Повторные поисковые запросы Данные о ресторанах
Instacart reCAPTCHA v3 Обнаружение ботов Цены на продукты

Компаратор цен на Python: рабочий пример

Ниже — пример на Python, который опрашивает несколько платформ, решает reCAPTCHA v3/v2 и Cloudflare Turnstile через API CaptchaAI и сопоставляет позиции меню по названию ресторана.

import requests
import time
import re
from bs4 import BeautifulSoup
import json

CAPTCHAAI_KEY = "YOUR_API_KEY"
CAPTCHAAI_URL = "https://ocr.captchaai.com"


def solve_captcha(method, sitekey, pageurl, **kwargs):
    data = {
        "key": CAPTCHAAI_KEY, "method": method,
        "googlekey": sitekey, "pageurl": pageurl, "json": 1,
    }
    data.update(kwargs)
    resp = requests.post(f"{CAPTCHAAI_URL}/in.php", data=data)
    task_id = resp.json()["request"]
    for _ in range(60):
        time.sleep(5)
        result = requests.get(f"{CAPTCHAAI_URL}/res.php", params={
            "key": CAPTCHAAI_KEY, "action": "get",
            "id": task_id, "json": 1,
        })
        r = result.json()
        if r["request"] != "CAPCHA_NOT_READY":
            return r["request"]
    raise TimeoutError("Timeout")


class FoodDeliveryComparator:
    def __init__(self, proxy=None):
        self.session = requests.Session()
        if proxy:
            self.session.proxies = {"http": proxy, "https": proxy}
        self.session.headers.update({
            "User-Agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 17_5 like Mac OS X) "
            "AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 "
            "Mobile/15E148 Safari/604.1",
            "Accept-Language": "en-US,en;q=0.9",
        })

    def search_restaurants(self, platform_url, location, cuisine=None):
        """Search restaurants on a delivery platform."""
        params = {"address": location}
        if cuisine:
            params["cuisine"] = cuisine

        url = f"{platform_url}/search"
        resp = self.session.get(url, params=params, timeout=30)

        if self._has_captcha(resp.text):
            resp = self._solve_and_retry(resp.text, url)

        return self._parse_restaurants(resp.text)

    def get_menu(self, restaurant_url):
        """Get menu with prices from a specific restaurant."""
        resp = self.session.get(restaurant_url, timeout=30)

        if self._has_captcha(resp.text):
            resp = self._solve_and_retry(resp.text, restaurant_url)

        return self._parse_menu(resp.text)

    def compare_restaurant_across_platforms(self, restaurant_name, platforms, location):
        """Compare same restaurant's pricing across delivery platforms."""
        results = []

        for platform in platforms:
            try:
                restaurants = self.search_restaurants(
                    platform["url"], location,
                )

                # Find matching restaurant
                match = None
                for r in restaurants:
                    if restaurant_name.lower() in r["name"].lower():
                        match = r
                        break

                if match and match.get("url"):
                    menu = self.get_menu(match["url"])
                    results.append({
                        "platform": platform["name"],
                        "restaurant": match["name"],
                        "delivery_fee": match.get("delivery_fee", ""),
                        "delivery_time": match.get("delivery_time", ""),
                        "menu_items": len(menu),
                        "sample_prices": menu[:5],
                    })
                else:
                    results.append({
                        "platform": platform["name"],
                        "restaurant": restaurant_name,
                        "status": "not found",
                    })

            except Exception as e:
                results.append({
                    "platform": platform["name"],
                    "error": str(e),
                })
            time.sleep(5)

        return results

    def track_delivery_fees(self, platforms, location, output_file):
        """Track delivery fees across platforms for analysis."""
        all_data = []

        for platform in platforms:
            try:
                restaurants = self.search_restaurants(
                    platform["url"], location,
                )
                for r in restaurants[:20]:  # Top 20 per platform
                    all_data.append({
                        "platform": platform["name"],
                        "restaurant": r["name"],
                        "delivery_fee": r.get("delivery_fee", ""),
                        "delivery_time": r.get("delivery_time", ""),
                        "rating": r.get("rating", ""),
                    })
                time.sleep(5)
            except Exception as e:
                print(f"Error on {platform['name']}: {e}")

        with open(output_file, "w") as f:
            json.dump(all_data, f, indent=2)

        return all_data

    def _has_captcha(self, html):
        return any(tag in html.lower() for tag in [
            'data-sitekey', 'g-recaptcha', 'cf-turnstile',
            'challenge-platform',
        ])

    def _solve_and_retry(self, html, url):
        match = re.search(r'data-sitekey="([^"]+)"', html)
        if not match:
            return self.session.get(url)
        sitekey = match.group(1)
        if 'cf-turnstile' in html:
            token = solve_captcha("turnstile", sitekey, url)
            return self.session.post(url, data={"cf-turnstile-response": token})
        token = solve_captcha("userrecaptcha", sitekey, url)
        return self.session.post(url, data={"g-recaptcha-response": token})

    def _parse_restaurants(self, html):
        soup = BeautifulSoup(html, "html.parser")
        restaurants = []
        for card in soup.select(".restaurant-card, .store-card, .merchant"):
            name_el = card.select_one(".name, .store-name, h3")
            if name_el:
                restaurants.append({
                    "name": name_el.get_text(strip=True),
                    "url": self._link(card),
                    "delivery_fee": self._text(card, ".delivery-fee, .fee"),
                    "delivery_time": self._text(card, ".delivery-time, .eta"),
                    "rating": self._text(card, ".rating, .stars"),
                })
        return restaurants

    def _parse_menu(self, html):
        soup = BeautifulSoup(html, "html.parser")
        items = []
        for item in soup.select(".menu-item, .item-card"):
            items.append({
                "name": self._text(item, ".item-name, .name"),
                "price": self._text(item, ".price, .item-price"),
                "description": self._text(item, ".description, .item-desc"),
            })
        return items

    def _text(self, el, selector):
        found = el.select_one(selector)
        return found.get_text(strip=True) if found else ""

    def _link(self, card):
        a = card.select_one("a")
        return a.get("href", "") if a else ""


# Usage
comparator = FoodDeliveryComparator(
    proxy="http://user:[email protected]:5000"
)

# Compare platforms
platforms = [
    {"name": "Platform A", "url": "https://delivery-a.example.com"},
    {"name": "Platform B", "url": "https://delivery-b.example.com"},
    {"name": "Platform C", "url": "https://delivery-c.example.com"},
]

comparison = comparator.compare_restaurant_across_platforms(
    restaurant_name="Pizza Palace",
    platforms=platforms,
    location="10001",
)

for result in comparison:
    print(f"{result.get('platform')}: Fee={result.get('delivery_fee')} "
          f"ETA={result.get('delivery_time')}")

Логика та же, что и в большинстве интеграций CaptchaAI: скрипт ищет data-sitekey в HTML, определяет тип защиты по маркеру (cf-turnstile или g-recaptcha), отправляет sitekey и pageurl в in.php, опрашивает res.php до готовности токена и передаёт его вместе с повторным запросом. Один и тот же solve_captcha() закрывает и Cloudflare Turnstile, и все три версии reCAPTCHA из таблицы выше — не нужно писать отдельный обработчик под каждую платформу.


Рекомендации по прокси для каждой платформы

Приложения доставки в первую очередь мобильные, поэтому мобильный трафик выглядит для них естественнее десктопного.

Платформа Рекомендуемый прокси Почему
DoorDash Мобильный (4G) Строгое обнаружение ботов, ожидается мобильный трафик
Uber Eats Мобильный (4G) Mobile-first платформа
Grubhub Резидентный Стандартная защита
Instacart Резидентный Умеренное обнаружение ботов
Just Eat Ротируемый резидентный Стандартный Cloudflare

Мобильный прокси с User-Agent iPhone или Android чаще всего даёт цены, совпадающие с показаниями в приложении — веб-версия платформы иногда отдаёт другие цифры, чем мобильное приложение.


Какие данные имеет смысл отслеживать

Для регулярного сравнения цен обычно достаточно шести метрик:

Метрика Ценность для бизнеса
Цены на позиции меню Анализ паритета цен и наценки
Стоимость доставки Сравнение комиссий платформы
Минимальная сумма заказа Анализ барьеров входа
Расчётное время доставки Сравнение уровня сервиса
Акции и скидки Маркетинговая разведка
Наличие ресторана на платформе Анализ покрытия

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

Проблема Причина Решение
Пустой список ресторанов Адрес не обслуживается или показана CAPTCHA Проверьте корректность почтового индекса доставки
Цены в меню отличаются от приложения Разница между веб- и app-ценами Используйте мобильный User-Agent, чтобы получить app-эквивалентные цены
Циклическая проверка Cloudflare Несовпадение User-Agent и сетевых сигналов Используйте мобильный прокси вместе с мобильным User-Agent
Ресторан найден на одной платформе, но не на другой Разное покрытие сервисов Помечайте как «недоступно» в сравнении
Неверная стоимость доставки Цена зависит от адреса Совместите гео прокси с целевым адресом доставки

Если в итоговый набор данных попадают адреса доставки или контакты клиентов, а не только цены и меню, относитесь к этим полям как к персональным данным: собирайте только то, что нужно для анализа цен, и учитывайте требования 152-ФЗ «О персональных данных» для аудитории из РФ или GDPR — для европейской.


Вопросы о сравнении цен на доставку еды

Как понять, какой тип CAPTCHA стоит за конкретной платформой?

Проще всего — открыть страницу поиска или карточку ресторана и посмотреть HTML: data-sitekey с cf-turnstile указывает на Cloudflare Turnstile, g-recaptcha — на reCAPTCHA v2, а невидимый скоринг без визуального виджета обычно означает reCAPTCHA v3. DoorDash и Instacart чаще используют reCAPTCHA v3, Uber Eats — Cloudflare Turnstile.

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

Для последовательного опроса шести сервисов раз в день хватает тарифа BASIC ($15/мес, 5 потоков). Если нужно сравнивать цены в нескольких городах параллельно или чаще, чем раз в час, разумнее взять STANDARD ($30/мес, 15 потоков) или ADVANCE ($90/мес, 50 потоков).

Мобильный или десктопный трафик использовать для парсинга приложений доставки?

Мобильный — эти платформы mobile-first, и цены в вебе иногда отличаются от цен в приложении. Мобильный прокси с User-Agent iPhone или Android даёт трафик, максимально похожий на трафик из приложения.

С какой периодичностью запускать сравнение цен?

Раз в неделю — для общего анализа рынка. Ежедневно — во время промо-акций или конкурентных исследовательских спринтов.


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


Сравнивайте цены на доставку еды в масштабе — получите API-ключ CaptchaAI и настройте кросс-платформенный мониторинг без ручных проверок.

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