演唱会开票几十秒后页面就变“已售罄”——问题往往不是网络延迟,而是订票页面甩来一个 reCAPTCHA 或 Turnstile,脚本卡在验证环节。本文用 Python 搭建余票监控流程:定时查库存、遇到验证码交给 CaptchaAI 识别、库存变化时发提醒。
国内外票务平台的验证码差异
国内票务平台常用 GeeTest(极验)滑块,境外平台以 reCAPTCHA、Turnstile 为主。后两者境外托管,国内网络连通性不稳,测试阶段常见超时——网络限制,非代码问题。solve_captcha 按 method 复用即可,三类 CaptchaAI 均支持。
监控思路:从查库存到发提醒
遇到验证码就先识别,再解析页面:
Configure events → Check availability → CAPTCHA?
↓ Yes
Solve via CaptchaAI → Retry
↓ No
Parse availability → Changed?
↓ Yes
Send alert
拿到 token 后重新提交,才能拿到真实库存 HTML。
开始前需要准备什么
- CaptchaAI API Key:在 captchaai.com 注册获取
- Python 3.8+:使用
requests库发起请求 - 自有服务器基础设施:可显著降低验证码触发频率
pip install requests
验证码识别模块:封装 CaptchaAI 调用
提交任务和轮询结果封装成一个通用函数,reCAPTCHA v2、Turnstile 都能复用:Turnstile 等 10 秒,其余等 20 秒,之后每 5 秒轮询一次,最多 30 次。
import requests
import time
API_KEY = "YOUR_API_KEY"
def solve_captcha(method, params):
"""Generic CaptchaAI solver for any supported method."""
params["key"] = API_KEY
params["json"] = 1
submit = requests.post("https://ocr.captchaai.com/in.php", data=params).json()
if submit.get("status") != 1:
raise RuntimeError(f"Submit error: {submit.get('request')}")
task_id = submit["request"]
initial_wait = 10 if method == "turnstile" else 20
time.sleep(initial_wait)
for _ in range(30):
result = requests.get("https://ocr.captchaai.com/res.php", params={
"key": API_KEY, "action": "get", "id": task_id, "json": 1
}).json()
if result.get("status") == 1:
return result["request"]
if result.get("request") != "CAPCHA_NOT_READY":
raise RuntimeError(f"Solve error: {result['request']}")
time.sleep(5)
raise TimeoutError("Solve timed out")
门票监控器:整合请求与验证码识别
TicketMonitor 负责发请求、识别验证码、比对库存变化三件事:遇到 g-recaptcha 调 userrecaptcha,遇到 cf-turnstile 调 turnstile,拿到 token 后重新提交再解析页面。
from datetime import datetime
import json
class TicketMonitor:
def __init__(self, proxy=None):
self.session = requests.Session()
self.session.headers.update({
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
})
if proxy:
self.session.proxies = {
"http": f"http://{proxy}",
"https": f"http://{proxy}"
}
self.last_status = {}
def check_event(self, event):
"""Check ticket availability for an event, solving CAPTCHAs if needed."""
url = event["url"]
response = self.session.get(url)
# Handle CAPTCHA if detected
if "g-recaptcha" in response.text or "recaptcha" in response.text:
sitekey = self._extract_sitekey(response.text)
if sitekey:
token = solve_captcha("userrecaptcha", {
"method": "userrecaptcha",
"googlekey": sitekey,
"pageurl": url
})
response = self.session.post(url, data={
"g-recaptcha-response": token
})
elif "cf-turnstile" in response.text:
sitekey = self._extract_turnstile_key(response.text)
if sitekey:
token = solve_captcha("turnstile", {
"method": "turnstile",
"sitekey": sitekey,
"pageurl": url
})
response = self.session.post(url, data={
"cf-turnstile-response": token
})
# Parse availability
availability = self._parse_availability(response.text, event)
# Check for changes
event_key = event["name"]
if event_key in self.last_status:
if availability != self.last_status[event_key]:
self._send_alert(event, availability)
self.last_status[event_key] = availability
return availability
def _extract_sitekey(self, html):
if 'data-sitekey="' in html:
start = html.index('data-sitekey="') + 14
end = html.index('"', start)
return html[start:end]
return None
def _extract_turnstile_key(self, html):
if 'data-sitekey="' in html:
start = html.index('data-sitekey="') + 14
end = html.index('"', start)
return html[start:end]
return None
def _parse_availability(self, html, event):
"""Parse ticket availability. Customize per ticketing site."""
available = "sold out" not in html.lower()
return {
"event": event["name"],
"available": available,
"checked_at": datetime.now().isoformat()
}
def _send_alert(self, event, availability):
"""Send availability change notification."""
status = "AVAILABLE" if availability["available"] else "SOLD OUT"
print(f"[ALERT] {event['name']}: {status}")
def monitor_all(self, events):
"""Check all events and return results."""
results = []
for event in events:
try:
result = self.check_event(event)
results.append(result)
print(f"[OK] {event['name']}: {'available' if result['available'] else 'sold out'}")
except Exception as e:
print(f"[ERROR] {event['name']}: {e}")
return results
# Usage
events = [
{
"name": "Concert - Madison Square Garden - Aug 15",
"url": "https://example-tickets.com/event/12345"
},
{
"name": "Basketball Finals - Game 7",
"url": "https://example-tickets.com/event/67890"
}
]
monitor = TicketMonitor(proxy="user:[email protected]:8080")
results = monitor.monitor_all(events)
for r in results:
print(json.dumps(r, indent=2))
预期输出:
[OK] Concert - Madison Square Garden - Aug 15: available
[OK] Basketball Finals - Game 7: sold out
部署到生产环境:定时任务
用 cron 定时触发即可,脚本不需要常驻:
# Check every 15 minutes
*/15 * * * * cd /path/to/project && python ticket_monitor.py >> /var/log/tickets.log 2>&1
间隔越短越容易触发验证码,具体见下面的排查表。
踩坑与排查
- 每次检查都弹验证码(同一 IP 无会话保持)→ 复用 session/cookie
- 检查几次就被封(触发速率限制)→ 拉长间隔,分散出口 IP
- 库存判断错误(页面结构变了)→ 更新
_parse_availability - 识别变慢(识别器负载高峰)→ 加退避重试逻辑
常见问题
多久检查一次余票比较合适?
一般每 10–30 分钟查一次;高需求场次可缩到 2–5 分钟,但频率越高越容易触发验证码。
CaptchaAI 支持哪些类型的验证码?
共 12 种:reCAPTCHA v2/v3、GeeTest v3、Turnstile/Challenge、图片/九宫格、BLS;CaptchaFox、Friendly Captcha、Lemin 为测试版,hCaptcha、FunCaptcha 不支持,GeeTest v4 即将支持。
验证码识别会不会拖慢整体监控节奏?
Turnstile 通常 10 秒内完成,reCAPTCHA v2 上限 60 秒。并发场次多时按线程计费(BASIC $15/月 5 线程、ADVANCE $90/月 50 线程),线程数决定并发量。
频繁触发验证码或被封 IP 怎么办?
先排查是否同一 IP 高频请求、未保持会话,或间隔太短;配合自有服务器基础设施与 cookie/session 可降低频率。
相关指南
现在就为你的监控脚本接入 CaptchaAI
在 captchaai.com 注册,把 CaptchaAI 接入监控流程,遇到 reCAPTCHA 或 Turnstile 自动识别,不必手动刷新等结果。