历史代码分析。 本文以 2023 年 teket.jp 免费票流程为背景,讨论会话共享、并发限流、提交重试和成功判断。当前购票应使用官方流程;下面的并发与请求处理示例不调用真实票务接口。
一、当时支持的范围
2023 年的实验脚本通过 Requests 登录,在演出 HTML 中提取票务变量,选择场次和票种,取得确认页面,再提交免费票申请。它没有实现付费、付款验证或自动选座,因此不能称为通用的 teket 自动购买工具。HTML 中存在某个字段,只能证明当时页面使用过它,不能推断所有请求契约都公开且稳定。
该实验的流程可拆为:
- 读取登录页面,取得会话与 CSRF 信息。
- 完成账号认证,检查是否真正进入已登录状态。
- 读取演出与销售时间,区分尚未开始、售罄、没有所需票种和解析失败。
- 提交一次选票或确认请求,核对返回的票种、场次和数量。
- 在明确交易规则后提交订单,并以业务响应、订单号和官方订单列表核对结果。
每一步的会话、CSRF token 和确认上下文可能关联上一步。不能在共享 Session 中让多个线程同时修改购物或订单状态。
二、并发与订单状态中的问题
| 位置 | 问题 | 后果 |
|---|---|---|
last_request_time |
多线程读写没有锁 | 两个线程可同时通过等待判断,所谓全局 0.5 秒阈值失效 |
global_session / login_session |
共享 Cookie、请求头和确认状态 | 一个线程可能覆盖另一个线程所需的会话状态 |
safe_request |
将所有请求都纳入自动重试 | 下单 POST 超时后可能已成功,再发会产生重复或冲突 |
| 登录判断 | 把 302 直接当登录成功 | 重定向也可能指向错误页或二次验证 |
| 订单判断 | 把 HTTP 200 直接当成功 | 登录页、库存不足和业务拒绝也可能返回 200 |
| 销售时间 | 未提供时间时仍使用 diff_seconds |
变量未赋值,流程中断 |
| 日期计算 | 使用没有时区的本地 datetime |
非日本时区服务器可能计算错误的等待时间 |
| 请求调用 | 没有显式 timeout | 一个线程可长期卡在网络读取 |
| 线程管理 | 子线程异常没有统一收集 | 主线程结束时仍可能显示完成,实际没有成功结果 |
Session 的连接池可改善传输效率,但这不能替代订单状态隔离。即便连接层允许并发,Cookie 与业务上下文也必须由程序明确管理。
三、一个可复用的全局发送门
这个示例用锁和单调时钟串行分配发送时点。它只说明如何避免无锁竞争,间隔必须按目标服务允许的速率设置;示例值不是 teket 的许可额度。
import threading
import time
class RateGate:
def __init__(self, interval):
if interval <= 0:
raise ValueError('interval 必须大于 0')
self.interval = interval
self._lock = threading.Lock()
self._next = 0.0
def wait(self):
with self._lock:
now = time.monotonic()
delay = max(0.0, self._next - now)
if delay:
time.sleep(delay)
self._next = time.monotonic() + self.interval
把等待放在锁内牺牲了一些并发,但确保每个通过门的时点至少间隔 interval。它只能约束当前进程,多个进程或其他设备不受它控制。实际发起请求还会受线程调度影响,不应把这个门当成严格的服务端请求到达保证。
四、只读请求与提交请求要区别处理
下面用演示 URL 展示一次请求的边界:
import requests
class OutcomeUnknown(RuntimeError):
pass
def read_json(session, url, gate):
gate.wait()
response = session.get(url, timeout=(5, 20))
response.raise_for_status()
return response.json()
def submit_once(session, url, payload, gate):
gate.wait()
try:
response = session.post(url, json=payload, timeout=(5, 20))
response.raise_for_status()
except requests.RequestException as error:
raise OutcomeUnknown('提交结果未确认,请查询业务状态,不自动重发') from error
# HTTP 成功还要验证业务结果;此函数不能宣称已创建订单。
return response
读操作也要按服务规则处理 429、Retry-After 与总尝试上限。提交超时只能说明客户端没收到确定响应,不能证明服务器没执行;除非服务提供并明确支持幂等键等机制,否则不要自动重发下单请求。HTTP 非幂等请求重试规则、Requests timeout
五、销售时间、并发结果与验证
如果输入明确是日本当地的 ISO 日期时间,可在解析后使用 ZoneInfo('Asia/Tokyo');如果字符串已经带时区则应保留并转换。不能把未知格式的时间直接标成日本时间。缺少销售开始时间时要进入明确的“待确认”状态,不能继续使用未初始化的等待变量。
并发读取任务可使用 ThreadPoolExecutor,由主线程逐个调用 future.result() 收集异常;各任务独立 Session,订单提交阶段串行推进一个已确认的业务上下文。日志分别记录 HTTP、解析和业务结果,避免输出 Cookie、CSRF 或账号信息。
重新实现自动化之前,需要在自己的测试环境或得到许可的测试接口核对当前字段、库存、确认页与订单结果,处理重复提交和取消。上面的示例不包含当前 teket API 契约,不能直接用于真实订票。
评论区