Одна и та же строка меню может стоить по-разному на 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 даёт трафик, максимально похожий на трафик из приложения.
С какой периодичностью запускать сравнение цен?
Раз в неделю — для общего анализа рынка. Ежедневно — во время промо-акций или конкурентных исследовательских спринтов.
Связанные руководства
- Мониторинг товарных остатков в рознице
- Мобильные прокси для решения CAPTCHA
- Ротация резидентных прокси
Сравнивайте цены на доставку еды в масштабе — получите API-ключ CaptchaAI и настройте кросс-платформенный мониторинг без ручных проверок.