Para comparar el precio de un mismo restaurante en DoorDash, Uber Eats o Grubhub necesitas dos piezas: un cliente que imite tráfico móvil y una forma de resolver el CAPTCHA que salta en cuanto envías varias solicitudes seguidas. Esta guía muestra cómo montar ese comparador en Python y delegar el reCAPTCHA v2, el reCAPTCHA v3 y el Cloudflare Turnstile en la API de CaptchaAI, sin frenar el flujo de extracción.
El caso de uso es habitual en servicios de comparación, investigación de mercado y analítica de restaurantes. Si trabajas en el mercado hispanohablante, el mismo patrón se aplica a Glovo y Uber Eats en España o a Rappi y PedidosYa en América Latina: apps mobile-first, precios que cambian por plataforma y una capa de detección de bots delante de los datos. Respeta siempre los términos de servicio de cada plataforma y la normativa de protección de datos aplicable.
Qué datos vale la pena comparar
Antes de escribir una sola línea de código conviene tener claro qué campos justifican el esfuerzo. La comparación entre plataformas suele girar en torno a estas métricas:
| Métrica | Valor de negocio |
|---|---|
| Precios de los platos del menú | Análisis de paridad de precios y margen |
| Tarifas de reparto | Comparación de comisiones por plataforma |
| Pedido mínimo | Análisis de barreras de acceso |
| Tiempo estimado de entrega | Comparación del nivel de servicio |
| Promociones y descuentos | Inteligencia de marketing |
| Disponibilidad del restaurante | Análisis de cobertura |
El dato más revelador suele ser el diferencial de precio del mismo plato entre dos apps: refleja las comisiones que el restaurante traslada al cliente en cada una.
Qué CAPTCHA usa cada plataforma
Cada plataforma protege sus datos con un mecanismo distinto y se activa por un motivo diferente. Conocer el tipo te dice qué method pasar a la API:
| Plataforma | Tipo de CAPTCHA | Qué lo activa | Datos protegidos |
|---|---|---|---|
| DoorDash | reCAPTCHA v3 + Cloudflare | Detección de bots | Menús, precios y tarifas |
| Uber Eats | Cloudflare Turnstile | Acceso automatizado | Listados de restaurantes y precios |
| Grubhub | reCAPTCHA v2 | Límite de solicitudes | Platos del menú y promociones |
| Postmates | Cloudflare Challenge | Detección de scraping | Tarifas de reparto y tiempos estimados |
| Just Eat | reCAPTCHA v2 | Búsquedas repetidas | Datos de restaurantes |
| Instacart | reCAPTCHA v3 | Detección de bots | Precios de la compra |
Todos estos tipos —reCAPTCHA v2, reCAPTCHA v3, Cloudflare Turnstile y Cloudflare Challenge— están dentro de los que resuelve CaptchaAI, así que un único integrador cubre las seis plataformas.
Comparador de precios multiplataforma en Python
La clase FoodDeliveryComparator encapsula la sesión, detecta si la respuesta trae un CAPTCHA y, en ese caso, lo resuelve antes de reintentar. El método compare_restaurant_across_platforms busca el mismo restaurante en cada app y devuelve tarifas y precios lado a lado; track_delivery_fees recorre los primeros resultados de cada plataforma y los vuelca a un JSON para análisis posterior.
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:pass@mobile.proxy.com: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')}")
El espaciado de cinco segundos entre plataformas no es decorativo: mantiene la frecuencia de solicitudes por debajo del umbral que dispara la mayoría de los desafíos, así que el CAPTCHA aparece menos y gastas menos resoluciones.
Red de salida por plataforma
Las apps de reparto son mobile-first, y su detección de bots lo tiene en cuenta. Igualar el tipo de red de salida al comportamiento que la plataforma espera reduce los desafíos de forma notable:
| Plataforma | Red de salida recomendada | Motivo |
|---|---|---|
| DoorDash | Red móvil (4G) | Detección intensa de bots; espera tráfico móvil |
| Uber Eats | Red móvil (4G) | Plataforma mobile-first |
| Grubhub | Red residencial | Protección estándar |
| Instacart | Red residencial | Detección moderada de bots |
| Just Eat | Red residencial rotativa | Cloudflare estándar |
Una red de salida móvil autorizada, combinada con un User-Agent móvil como el del ejemplo, produce el tráfico más coherente con lo que la app espera de un usuario real.
Errores frecuentes y cómo resolverlos
| Problema | Causa probable | Solución |
|---|---|---|
| Resultados de restaurantes vacíos | Zona no cubierta o página con CAPTCHA | Fija el código postal de entrega correcto |
| Precios del menú distintos a los de la app | Diferencia de precios entre web y app | Usa un User-Agent móvil para obtener precios equivalentes a los de la app |
| Bucle de desafío de Cloudflare | Señales del navegador incoherentes | Combina red móvil autorizada con User-Agent móvil |
| Restaurante visible en una plataforma pero no en otra | Cobertura distinta | Márcalo como "no disponible" en la comparación |
| Tarifas de reparto incorrectas | Precios según la ubicación | Haz coincidir la geografía de la red con la zona de destino |
Preguntas frecuentes
¿Es legal comparar precios entre apps de comida a domicilio?
Depende. La comparación de precios públicos con fines de investigación de mercado es una práctica común, pero cada plataforma tiene sus propios términos de servicio y cada jurisdicción su normativa de protección de datos (GDPR y LOPDGDD en España, LFPDPPP en México, entre otras). Revisa las condiciones de cada plataforma y limita la extracción a datos públicos.
¿Qué plan de CaptchaAI necesito para varias plataformas a la vez?
Depende de cuántas solicitudes resuelvas en paralelo. CaptchaAI factura por thread concurrente con resoluciones ilimitadas dentro de cada plan, así que un comparador secuencial como el del ejemplo funciona con el plan BASIC ($15/mes, 5 threads). Si lanzas varias plataformas en paralelo o aumentas la frecuencia, el plan STANDARD ($30/mes, 15 threads) o ADVANCE ($90/mes, 50 threads) te dan más margen. Puedes ver todos los tramos en captchaai.com/pricing.
¿Qué tipos de CAPTCHA aparecen en estas apps y cuáles resuelve CaptchaAI?
Los más habituales son reCAPTCHA v2, reCAPTCHA v3, Cloudflare Turnstile y Cloudflare Challenge, y CaptchaAI resuelve los cuatro. Ten en cuenta que CaptchaAI no resuelve hCaptcha ni FunCaptcha (Arkose Labs); si una plataforma migra a esos tipos, tendrás que replantear el flujo.
¿Cómo evito quedar atrapado en el bucle de desafío de Cloudflare?
El bucle suele indicar señales del navegador incoherentes: una red de salida de tipo datacenter con un User-Agent móvil, por ejemplo. Alinea la red y el User-Agent, resuelve el Cloudflare Challenge con la API y reutiliza la misma sesión para las peticiones siguientes en lugar de abrir una nueva en cada intento.
Guías relacionadas
- Monitoreo de inventario en retail
- Resolución de CAPTCHA en pruebas autorizadas
- Redes de salida autorizadas para scraping
Compara precios de reparto a escala: obtén tu clave de CaptchaAI y automatiza el análisis multiplataforma.