Use Cases

外卖价格跨平台比较:如何识别验证码

比较 DoorDash、Uber Eats、Grubhub 等平台上同一家餐厅的价格,是餐饮数据分析和市场调研的常规工作——但这些平台几乎都用 reCAPTCHA 或 Cloudflare Turnstile 挡住自动访问。逻辑其实和国内美团、饿了么之间的价格差异类似:佣金结构不同,配送费和促销策略也各自独立。这篇文章给出一套可复用的 Python 方案:识别验证码类型、处理验证码、解析菜单价格,整理成可对比的数据表。


外卖平台会用到哪些验证码?

平台 验证码类型 触发条件 受保护数据
DoorDash reCAPTCHA v3 + Cloudflare 机器人检测 菜单、价格、配送费
Uber Eats Cloudflare Turnstile 自动化访问 商户列表、价格
Grubhub reCAPTCHA v2 速率限制 菜单项、促销
Postmates Cloudflare Challenge 抓取检测 配送费、预计到达
Just Eat reCAPTCHA v2 重复搜索 商户数据
Instacart reCAPTCHA v3 机器人检测 生鲜杂货价格

六个平台合计只用到三种验证码:reCAPTCHA v2、reCAPTCHA v3、Cloudflare Turnstile(含 Challenge),都是 CaptchaAI 的 GA 类型,同一套脚本就能统一处理。


该跟踪哪些数据指标?

还值得记录这几项:

指标 商业价值
菜单项价格 价格平价与加价分析
配送费 平台费用对比
最低起送金额 访问门槛分析
预计送达时间 服务水平对比
促销与折扣 营销情报
商户覆盖情况 覆盖率分析

自有服务器基础设施建议

不同平台对网络来源的敏感度不同:

  • DoorDash / Uber Eats:优先用移动网络出口(4G/5G),检测严格。
  • Grubhub / Instacart:标准出口 IP 即可,检测中等。
  • Just Eat:适合轮换出口 IP,Cloudflare 挑战对重复来源敏感。

配送类 App 是移动优先场景:移动网络 + 移动端 UA 组合更不容易触发二次验证码。


多平台比价脚本(Python 实现)

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

常见报错与排查方法

问题 原因 处理方式
搜索结果为空 地址未覆盖,或命中验证码页面 确认送货地址邮编正确
菜单价格与 App 内不一致 网页端与 App 端定价有差异 用移动端 UA 获取对齐 App 的定价
Cloudflare 挑战反复出现 网络与设备特征不匹配 统一用移动网络出口 + 移动端 UA
一个平台能找到餐厅,另一个找不到 平台覆盖范围不同 标记为"不可用"即可
配送费数值不对 定价与地理位置强相关 出口 IP 地理位置匹配目标地址

常见问题

为什么同一家餐厅在不同外卖平台上价格不一样?

每个平台向餐厅收取的佣金比例不同(大致 15%–30%),餐厅按平台单独定价覆盖成本;配送费和服务费也各自设置。

比价脚本会遇到哪些验证码类型?

只有三种:reCAPTCHA v2、reCAPTCHA v3 和 Cloudflare Turnstile(含 Challenge)。solve_captcha() 已覆盖两条分支,接入 CaptchaAI 后统一处理即可。

遇到限流或反复弹验证码怎么办?

先检查请求节奏——time.sleep(5) 这类间隔是必要的,不是可选项。频繁切换网络特征也会触发更多验证码。

多久做一次跨平台比价合适?

常规市场分析每周一次;促销季或有竞对研究项目时可以拉到每天一次。


相关指南


获取你的 CaptchaAI API Key,把手工比价变成可复用的自动化流程。

该文章已禁用评论。