比较 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,把手工比价变成可复用的自动化流程。