Use Cases

演出门票余票监控实战:验证码识别怎么做

演唱会开票几十秒后页面就变“已售罄”——问题往往不是网络延迟,而是订票页面甩来一个 reCAPTCHA 或 Turnstile,脚本卡在验证环节。本文用 Python 搭建余票监控流程:定时查库存、遇到验证码交给 CaptchaAI 识别、库存变化时发提醒。


国内外票务平台的验证码差异

国内票务平台常用 GeeTest(极验)滑块,境外平台以 reCAPTCHA、Turnstile 为主。后两者境外托管,国内网络连通性不稳,测试阶段常见超时——网络限制,非代码问题。solve_captchamethod 复用即可,三类 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-recaptchauserrecaptcha,遇到 cf-turnstileturnstile,拿到 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 自动识别,不必手动刷新等结果。

该文章已禁用评论。