ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Python抢票脚本实战:签名、时间同步与多平台适配

Python抢票脚本实战:签名、时间同步与多平台适配

简介:这是一套面向演唱会门票抢购场景的多平台自动化脚本工具,覆盖大麦网、淘票票、缤玩岛等主流票务渠道,适合具备一定 Python 基础、希望提升抢票效率的用户研究使用。资源包共 19 个文件,约 13.57MB,以 xml 配置、html 页面、md 说明文档为主,另含 exe 驱动、py 主程序、json 配置及 yml 工作流文件,结构上区分了脚本主体、页面模板与驱动依赖,便于按模块查阅。目前已有 1001 人学习下载。通过阅读主程序与配置文件,读者可了解多平台抢票的请求构造、页面交互与参数配置思路,并借助 README 等文档快速理解项目组织方式,为二次开发或脚本学习提供参考。

1. 抢票脚本的真相:从手动刷新到自动化监控的落地路径

抢票这件事,手动刷过的人都懂——页面卡顿、验证码弹窗、提交订单转圈,等你反应过来票已经没了。市面上所谓的“抢票脚本”,核心逻辑其实不复杂:用程序代替人手,在开票瞬间以毫秒级频率轮询票务接口,检测到余票立刻触发下单请求。这份资源包覆盖了大麦网、淘票票、缤玩岛等多个平台的抢票脚本实现,包含请求签名、时间同步、余票监控、自动提交订单等模块。它适合有一定编程基础、想研究自动化请求与反爬对抗的开发者,也适合想理解票务平台接口设计的技术人员。但先说清楚:脚本不是万能钥匙,平台风控随时在变,能不能抢到取决于网络延迟、账号权重、接口策略等多重因素,脚本只是把“手动点”变成“程序跑”,并不改变供需关系。

2. 拆解抢票脚本的核心模块:签名、时间与请求链路

2.1 请求签名机制:为什么直接调接口会失败

票务平台的接口不是裸奔的。每次请求都带有一组动态参数,常见的是sign、token、timestamp、nonce等字段。以某平台为例,签名算法通常是把请求参数按字典序拼接,加上一个固定的盐值,再做 MD5 或 HMAC-SHA256 运算。脚本里这部分一般封装在sign.js或sign.py中。

import hashlib import time import random import string def generate_sign(params: dict, salt: str) -> str: # 1. 过滤空值参数 filtered = {k: v for k, v in params.items() if v is not None and v != ""} # 2. 按 key 字典序排序 sorted_keys = sorted(filtered.keys()) # 3. 拼接成 key=value&key=value 形式 raw = "&".join([f"{k}={filtered[k]}" for k in sorted_keys]) # 4. 末尾追加盐值 raw_with_salt = raw + salt # 5. MD5 运算并转小写 return hashlib.md5(raw_with_salt.encode("utf-8")).hexdigest() # 调用示例 params = { "itemId": "123456789", "platform": "damai", "timestamp": str(int(time.time() * 1000)), "nonce": "".join(random.choices(string.ascii_letters + string.digits, k=16)) } salt = "xxxxxxxx" # 盐值需要从平台前端 JS 中逆向提取 sign = generate_sign(params, salt) print(sign)

这段代码的关键在于盐值salt的获取。盐值通常硬编码在平台前端 JavaScript 文件里,经过混淆处理。常见做法是用浏览器开发者工具搜索sign或md5关键字,定位到加密函数后手动还原。参数说明:timestamp是毫秒级时间戳,nonce是随机字符串,两者每次请求都必须不同,否则会被判定为重放攻击。如果签名算错,接口会返回403或签名校验失败,这是最常见的翻车点。

2.2 时间同步:本地时钟偏差如何毁掉抢票

抢票脚本对时间的敏感度极高。平台服务器时间和本地机器时间如果差了几百毫秒,开票瞬间的请求就可能落在“未开售”或“已售罄”区间。脚本里一般会先请求一个时间接口,拿到服务器时间戳,再计算本地与服务器的偏移量。

import requests import time def get_server_time_offset(server_time_url: str) -> float: # 发起请求前记录本地时间 local_before = time.time() resp = requests.get(server_time_url, timeout=3) local_after = time.time() # 服务器返回毫秒时间戳 server_ts = resp.json()["data"]["timestamp"] / 1000.0 # 估算网络往返延迟 rtt = local_after - local_before # 服务器时间 + 半个往返延迟 ≈ 当前服务器时间 estimated_server_now = server_ts + rtt / 2 # 计算偏移量 offset = estimated_server_now - local_after return offset # 使用偏移量校准本地时间 offset = get_server_time_offset("https://api.example.com/time") def get_calibrated_time(): return time.time() + offset

逻辑说明:网络往返延迟rtt被均分到请求和响应两个方向,所以用server_ts + rtt/2估算当前服务器时间。偏移量offset为正说明本地慢,为负说明本地快。抢票前把time.time()替换成get_calibrated_time(),能把误差压到 50ms 以内。注意:不同平台的时间接口地址不同,有的藏在商品详情页的响应头里,需要抓包确认。

2.3 余票监控与自动提交:轮询频率的取舍

余票监控的本质是高频轮询。脚本会以固定间隔请求票务接口,解析返回的skuId、inventory字段,一旦发现目标票档有库存,立即调用下单接口。

import requests import time def monitor_ticket(session, item_id, sku_id, interval=0.3): url = f"https://api.example.com/item/{item_id}/inventory" while True: try: resp = session.get(url, timeout=2) data = resp.json() for sku in data.get("skuList", []): if sku["skuId"] == sku_id and sku["inventory"] > 0: print(f"有票!库存 {sku['inventory']}") return submit_order(session, item_id, sku_id) except Exception as e: print(f"请求异常: {e}") time.sleep(interval) def submit_order(session, item_id, sku_id): order_url = "https://api.example.com/order/submit" payload = { "itemId": item_id, "skuId": sku_id, "quantity": 1, "timestamp": str(int(time.time() * 1000)) } resp = session.post(order_url, json=payload, timeout=3) return resp.json()

参数说明:interval是轮询间隔,设太小(如 0.1 秒)容易触发风控封 IP,设太大(如 1 秒)可能错过库存窗口。我一般会设在 0.2 到 0.5 秒之间,同时配合多个账号轮换请求。session对象需要提前登录并保持 Cookie,否则下单接口会返回未登录。常见做法是先用账号密码或扫码登录,把 Cookie 序列化到本地文件,脚本启动时反序列化加载。

3. 多平台适配:大麦、淘票票、缤玩岛的接口差异与统一封装

3.1 各平台接口特征对比

不同票务平台的接口设计差异很大,脚本不能一套代码通吃。下面这张表是拆包后整理的典型差异点:

平台签名算法时间戳精度库存接口频率限制下单前置条件
大麦网MD5 + 盐值毫秒约 5 次/秒需实名观演人
淘票票HMAC-SHA256毫秒约 3 次/秒需绑定手机号
缤玩岛自定义混淆秒约 2 次/秒需预填收货地址

从表里能看出,大麦的签名最简单但频率限制最严,缤玩岛的签名最麻烦但限制相对松。脚本里一般会为每个平台写一个adapter类,统一暴露get_inventory()和submit_order()两个方法,上层调度逻辑不关心底层差异。

3.2 统一封装层的实现

class BaseAdapter: def __init__(self, session): self.session = session def get_inventory(self, item_id, sku_id): raise NotImplementedError def submit_order(self, item_id, sku_id): raise NotImplementedError class DamaiAdapter(BaseAdapter): def get_inventory(self, item_id, sku_id): # 大麦特有的签名和请求头 params = {"itemId": item_id, "skuId": sku_id} params["sign"] = generate_sign(params, DAMAI_SALT) resp = self.session.get(DAMAI_INVENTORY_URL, params=params) return resp.json() def submit_order(self, item_id, sku_id): # 大麦下单需要额外传观演人 ID payload = {"itemId": item_id, "skuId": sku_id, "viewerIds": ["xxx"]} payload["sign"] = generate_sign(payload, DAMAI_SALT) return self.session.post(DAMAI_ORDER_URL, json=payload).json() class TaopiaopiaoAdapter(BaseAdapter): def get_inventory(self, item_id, sku_id): # 淘票票用 HMAC-SHA256,密钥从本地配置读取 headers = {"X-Sign": hmac_sign(item_id, TAOPIAOPIAO_SECRET)} resp = self.session.get(TAOPIAOPIAO_INVENTORY_URL, headers=headers) return resp.json() def submit_order(self, item_id, sku_id): # 淘票票下单需要手机号验证 payload = {"itemId": item_id, "skuId": sku_id, "mobile": "138xxxx"} return self.session.post(TAOPIAOPIAO_ORDER_URL, json=payload).json()

逻辑说明:BaseAdapter定义了统一接口,每个平台适配器负责处理自己的签名、请求头和参数格式。上层调度器只需要根据目标平台实例化对应的 adapter,然后调用同样的方法。这样新增平台时只需加一个 adapter 类,不用改核心逻辑。注意:各平台的item_id和sku_id格式不同,配置里要写清楚对应关系,否则会请求到错误的商品。

3.3 配置文件与账号管理

脚本一般会把平台配置、账号信息、目标商品 ID 放在一个config.yaml里:

platforms: damai: enabled: true item_id: "123456789" sku_id: "987654321" viewers: ["观演人ID1", "观演人ID2"] taopiaopiao: enabled: false item_id: "abcdefg" sku_id: "hijklmn" mobile: "138xxxx1234" accounts: - platform: damai username: "user1" cookie_file: "cookies/damai_user1.json" - platform: taopiaopiao username: "user2" cookie_file: "cookies/taopiaopiao_user2.json"

参数说明:enabled控制是否启用该平台,viewers是大麦要求的观演人 ID 列表,cookie_file是登录后保存的 Cookie 路径。常见做法是把 Cookie 文件加入.gitignore,避免账号信息泄露。多账号场景下,脚本会轮流使用不同账号的 Cookie 发起请求,降低单账号被风控的概率。

4. 避坑与排查:抢票脚本最常见的五个翻车现场

4.1 签名校验失败:现象是接口返回 403 或“签名错误”

原因通常是盐值过期或参数拼接顺序不对。平台前端更新后,盐值可能变化,或者新增了某个必传参数。解决方法是重新抓包,对比脚本请求和浏览器请求的完整参数列表,确认没有遗漏字段。另外注意参数值是否需要 URL 编码,有些平台要求先编码再签名。

4.2 时间偏移导致请求落在错误区间

现象是脚本显示“未开售”但手动刷新已经能买了。原因是本地时钟和服务器时间偏差超过阈值。解决方法是每次抢票前强制调用一次时间同步接口,把偏移量写入全局变量。如果平台没有公开时间接口,可以从任意一个 API 响应的Date头里提取服务器时间。

4.3 轮询频率过高被封 IP

现象是脚本运行几秒后所有请求都超时或返回 429。原因是平台风控检测到同一 IP 的高频请求。解决方法是降低轮询频率到 0.5 秒以上,或者使用多个出口 IP 轮换。注意:不要用免费代理池,延迟高且不稳定,反而会错过库存窗口。

4.4 Cookie 过期导致下单失败

现象是库存监控正常,但提交订单时返回“请先登录”。原因是 Cookie 有效期短,通常几小时到一天。解决方法是在脚本里加一个 Cookie 有效性检测,发现失效时自动重新登录。常见做法是用 Selenium 打开登录页面,等待用户扫码后保存新的 Cookie。

4.5 下单接口缺少必填参数

现象是库存有票但提交订单返回“参数错误”。原因是不同票档的下单参数不同,比如某些平台要求传deliveryType、invoiceInfo等字段。解决方法是先用浏览器手动下单一次,抓包记录完整的请求体,然后对照脚本的 payload 逐字段补齐。

5. 进阶技巧:用异步请求把轮询延迟压到极限

同步请求的瓶颈在于每次requests.get()都要等响应返回才能发下一次。用aiohttp改成异步后,可以在等待响应的间隙发起其他请求,把有效轮询频率提高三到五倍。

import aiohttp import asyncio import time async def fetch_inventory(session, url, params): try: async with session.get(url, params=params, timeout=2) as resp: return await resp.json() except Exception: return None async def monitor_async(item_id, sku_id, concurrency=5): async with aiohttp.ClientSession() as session: url = f"https://api.example.com/item/{item_id}/inventory" params = {"skuId": sku_id, "sign": "xxx"} while True: tasks = [fetch_inventory(session, url, params) for _ in range(concurrency)] results = await asyncio.gather(*tasks) for data in results: if data and data.get("inventory", 0) > 0: print(f"有票!{time.time()}") return data await asyncio.sleep(0.1) asyncio.run(monitor_async("123456789", "987654321"))

逻辑说明:concurrency=5表示同时发起 5 个请求,asyncio.gather等待它们全部返回。这样即使单个请求耗时 200ms,一轮的总耗时也接近 200ms 而不是 1000ms。参数说明:timeout=2防止某个请求卡死拖慢整体节奏,asyncio.sleep(0.1)控制轮询间隔。注意:并发数不要设太高,否则容易触发风控,我一般控制在 3 到 5 之间。

验证脚本是否生效的方法很简单:找一个已开售但库存充足的商品,运行监控逻辑,看能否在 1 秒内检测到库存并成功提交订单。如果提交成功但未支付,说明链路通了。从那以后我每次改完签名或时间同步逻辑,都会先用这个“低风险商品”跑一遍全流程,确认无误再上真实目标。希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进