Casos de Uso

Comparación de precios de entrega de alimentos con resolución CAPTCHA

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

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


Compara precios de reparto a escala: obtén tu clave de CaptchaAI y automatiza el análisis multiplataforma.

Los comentarios están deshabilitados para este artículo.