简介:这是一份面向Python初中级学习者的演唱会抢票实战项目代码包,覆盖网络爬虫、多线程与多进程、定时任务、模拟登录等核心技能,适合有Python基础语法知识、希望挑战综合项目的开发者。压缩包共2个文件,包含1个Python脚本和1个文本文件,整体大小仅6KB,脚本结构精简,便于直接阅读和二次修改,文本侧重点明实现思路、关键步骤与常见坑位。该资源已有1066人学习下载,是抢票类小项目中较受欢迎的入门资料。通过脚本示例可了解如何结合Requests与BeautifulSoup解析票务页面,借助threading或multiprocessing提升并发请求效率,并利用schedule等库在指定时间自动触发程序;同时,模拟登录、会话维持、异常处理及日志记录等实战细节也都有所体现。整体适合作为课程设计参考、技能进阶练习或抢票场景的技术入门模板,帮助开发者快速搭建自己的自动化脚本框架,并为后续更复杂的Web自动化项目打下基础。
1. 抢票脚本这件事,先搞明白它到底卡在哪
提起 Python 抢演唱会门票,很多人第一反应是“写个脚本疯狂点按钮”。真做过的人会告诉你,这个思路从起跑线就错了。演唱会抢票的难点从来不在“点得快”,而在于你发出的每一次请求,都要在几万人的并发里挤进一个极窄的时间窗口,同时还要绕过平台的风控判断、验证码校验和库存扣减逻辑。单纯把按钮点击频率拉满,只会让你更快被系统识别成机器行为,直接封掉账号的抢购资格。
这一篇实战笔记,想帮你把“Python 演唱会抢票”这件事从玄学变成清晰的操作路径。我会先拆解抢票流程里真正的技术壁垒在哪里,然后给出可复现的抓包、登录态管理、接口构造和抢购策略,最后把最容易翻车的几个坑单独拿出来讲。适合谁看?有一定 Python 基础、想在合法合规前提下提升自己购票成功率的开发者——注意,是提升成功率,不是承诺必中。演唱会门票是稀缺资源,代码能帮你把请求发得比手点更快更准,但库存放不放给你,平台说了算。想明白这一点,再往下读。
2. 抢票请求的完整链路:从登录态到下单接口,差一环都白搭
2.1 先理解票务平台的交互流程,别急着写代码
演唱会抢票看起来是一个动作,实际拆开是至少五步的完整链路:登录认证、场次查询、票档选择、下单锁定、支付确认。每一步对应一个或多个网络请求,而这些请求之间有着严格的时序依赖——你不可能跳过查询直接下单,也不可能在没有有效登录态的情况下发起购买。
我一般会把这条链路画成一条带状态的流水线:
- 登录态获取:通过扫码或账号密码获得身份凭证,后续所有请求都带着这个凭证。
- 场次与票价查询:获取某场演唱会的场次 ID、票档列表、库存状态。
- 下单参数构造:选中票档后,收集演出 ID、场次 ID、票档 ID、购买数量、观演人信息。
- 提交订单:向下单接口发送 POST 请求,平台校验登录态、库存、风控策略。
- 订单确认与支付:下单成功后,跳转支付或自动唤起支付。
这里面最容易让初学者栽跟头的,是登录态和接口参数之间的耦合关系。很多票务平台的接口签名不仅包含当前时间戳,还把用户 Cookie 里的一段核心字段作为签名因子。也就是说,你从别人那里抄来的抢票脚本,如果没适配自己账号的 Cookie 结构,即使请求构造得一模一样,也会在服务端签名校验时直接被拒。
另一个容易忽略的点是观演人信息。现在主流票务平台都要求实名制购票,每张票对应一个身份证号。抢票脚本里如果没有预先设置观演人列表,真到了下单那一刻,你还要手动填身份证,那脚本就失去了意义。常见的做法是提前在平台 App 里录入观演人信息,脚本下单时直接传观演人 ID 列表,而不是传身份证文本。
2.2 工具选型:requests 还是 playwright,两条路线背后的取舍
选工具之前,先说一个核心矛盾。票务平台的页面是动态渲染的,很多关键数据不是直接写在 HTML 里,而是由 JavaScript 异步加载后再填充到页面上。这意味着静态抓取页面拿不到完整数据,你必须选择:要么模拟浏览器完整执行 JS,要么直接调接口。
这两条路线的代表性工具分别是 playwright 和 requests,它们的取舍很直接:
| 维度 | requests 路线 | playwright 路线 |
|---|---|---|
| 上手难度 | 低,纯 HTTP 请求构造 | 中偏高,需要理解浏览器自动化 |
| 执行速度 | 快,毫秒级发出请求 | 慢,每次要启动浏览器核心 |
| 风控对抗 | 弱,请求特征容易被识别 | 强,行为模拟更接近真实用户 |
| 接口变动影响 | 小,只要接口不变就能用 | 大,页面改版可能导致选择器失效 |
| 依赖环境 | 轻,一个 Python 脚本即可 | 重,需要安装浏览器内核 |
我自己在实际项目中会优先选 requests 路线。原因很实际:抢票拼的是下单接口的请求速度,这个环节用 playwright 模拟点击,启动浏览器和执行 JS 的时间足够让库存从有到无。playwright 更适合做页面自动化验证、确认订单这类对速度不敏感的环节。
但 requests 路线的一个前置条件是,你必须能抓到完整的接口请求。这需要用抓包工具拦截 App 或网页的请求流量,把每个步骤对应的 URL、请求头、请求体、Cookie 变化都记录下来。有经验的做法是先用 playwright 或手动操作跑通一遍完整流程,同时开着抓包工具记录,再把关键接口逐个用 requests 重放验证,最后才进入抢票逻辑编写。跳过抓包直接猜接口参数,是新手最常见的翻车起点。
2.3 登录态与 Cookie 管理:抢票脚本的命脉所在
登录态是整个抢票脚本里最重要、也最容易被轻视的部分。票务平台的登录凭证通常有两层:一层是短期有效的会话凭证,比如登录后写入 Cookie 的特定字段;另一层是长期有效的刷新凭证,用于会话过期后自动续期。
我先说一个常见的血泪教训:很多人写好脚本后本机调试一切正常,但一到抢票当天,脚本跑起来却提示登录过期。原因往往是 Cookie 是昨天手动从浏览器复制出来的,而平台的会话凭证有较短的有效期,过了时间就失效。避免这个问题有三个层面:
第一,抢票前 10 分钟重新获取一次登录态,保证凭证新鲜。第二,如果平台支持扫码登录,优先用扫码方式,因为扫码拿到的凭证有效期通常比账号密码登录更长。第三,不要把 Cookie 硬编码在脚本文件里,应该用一个独立的配置文件或环境变量管理,便于每次抢票前快速更新。
import json from pathlib import Path # 登录态配置文件,每次抢票前更新 def load_auth(): auth_file = Path("auth.json") if not auth_file.exists(): raise FileNotFoundError("缺少 auth.json,请先完成登录并导出凭证") with open(auth_file, "r", encoding="utf-8") as f: return json.load(f) # 使用示例 auth = load_auth() cookie_str = auth["cookie"] # 完整 Cookie 字符串 csrf_token = auth.get("csrf_token") # 部分接口需要单独的 CSRF 凭证这段代码做的事情很简单,但对应着一个关键设计原则:把认证信息和业务逻辑分开。抢票脚本的请求构造、下单策略属于业务逻辑,而 Cookie、Token 这类会频繁变化的信息应该隔离在外部配置里。这样每次抢票前只需要更新 auth.json,不用改动脚本本体,也方便多个账号切换。
参数说明上,cookie 字段是完整的请求头 Cookie 字符串,格式是 key=value; key2=value2 这样用分号加空格分隔;csrf_token 不是所有平台都要求,但当你看到请求头里带着 x-csrf-token 或 X-CSRFToken 时,就必须把这个值加进业务请求里,否则接口会返回 403 或校验失败。
3. 从抓包到下单脚本:用 requests 在本地跑通抢票全流程
3.1 抓包定位关键接口:找到这 4 个请求就够了
开始写抢票脚本前,必须先完成抓包。我用的是最常见的流程:手机开抓包工具,然后手动在票务平台 App 里走一遍“选场次→选票档→立即购买→确认订单”的完整流程,把过程中产生的请求全部记录下来。
在这堆请求里,真正值得关注的其实只有 4 个关键接口:
- 场次查询接口:GET 请求,返回演出场次列表和每个场次的库存状态。
- 票档查询接口:GET 请求,返回某个场次下的票价档位和库存数量。
- 预下单/创建订单接口:POST 请求,提交演出信息、票档信息、观演人信息。
- 确认订单接口:POST 请求,对预下单结果做二次确认,通常包含订单号。
怎么在这堆请求里快速找到它们?看请求的响应体。场次查询的响应里会包含场次 ID 和日期,票档查询的响应里会出现“票档”“价格”“库存”这些字段,创建订单的响应里会出现“订单号”“待支付金额”。如果看到字段名和业务含义都能对上,基本就是目标接口。
抓包工具的选择上,PC 端我用的是市面常见的抓包代理工具,手机端配合装证书来解密 HTTPS 流量。这里有个提醒:2024 年之后很多票务平台启用了证书绑定,也就是 App 内部只信任特定证书,常规抓包工具装完证书也解不了 HTTPS。遇到这种情况,换一条路走——用 playwright 脚本模拟浏览器登录网页版,在浏览器自动化过程中拦截响应数据,同样能拿到接口信息,还省了装证书的麻烦。
3.2 构造场次与票档查询:参数和返回结构的对应关系
拿到抓包数据后,第一步是把场次和票档查询拿下来。这两个接口都是只读操作,不会对账号产生副作用,适合用来做抢票前的自测。
先看一个典型的结构设计。假设某票务平台查询场次的接口是/api/performance/query,参数包含itemId(演唱会项目 ID)、cityId(城市 ID),返回的数据里有一个场次列表,每个场次包含performanceId、startTime、ticketStockStatus。票档查询接口则是/api/performance/ticketTiers,参数是performanceId,返回票档数组,每个票档包含tierId、price、remainCount。
import requests import json # 会话保持,带上完整登录态 session = requests.Session() session.headers.update({ "User-Agent": "Mozilla/5.0 ...", # 伪装成浏览器 UA "Referer": "https://ticket.example.com/", # 部分接口校验 Referer }) session.cookies.update(auth["cookie"]) # 第一步:查场次 def fetch_performances(item_id, city_id): url = "https://api.ticket.example.com/performance/query" params = {"itemId": item_id, "cityId": city_id} resp = session.get(url, params=params) resp.raise_for_status() # 非 2xx 直接抛异常,方便定位问题 return resp.json()["data"]["list"] # 第二步:查票档 def fetch_ticket_tiers(performance_id): url = "https://api.ticket.example.com/performance/ticketTiers" params = {"performanceId": performance_id} resp = session.get(url, params=params) resp.raise_for_status() return resp.json()["data"]["tiers"]这段代码里有两个值得说明的设计。第一是使用 requests.Session(),它会自动维持 Cookie,确保后续请求都在同一个会话里。第二是 UA 和 Referer 的处理,票务平台的风控会检查请求来源,如果发现一个登录态突然换了一个陌生的 UA 或者从异常域名发来请求,会触发风控标记。所以在 headers 里至少要带上正常的浏览器 UA 和 Referer。
参数层面的细节在于,不同场次的ticketStockStatus字段值并不一定是布尔值。有的平台用 0 和 1 表示有无库存,有的用SOLD_OUT、ON_SALE这类枚举。写轮询逻辑时要注意先打印一次真实返回值,确认状态字段的含义,再做条件判断。
3.3 创建订单接口:参数签名和时间窗口的博弈
创建订单是整个抢票脚本的核心动作。这个接口的参数最多,时序要求最严格,也是最容易被平台风控盯上的环节。
先梳理一个常见的创建订单接口需要哪些参数。业务参数包括performanceId(场次 ID)、tierId(票档 ID)、num(购买数量)、viewerIds(观演人 ID 列表)。非业务参数包括timestamp(当前时间戳)、sign(接口签名)、token(短期动态凭证)。
import time import hashlib def build_sign(params, secret_key): # 按照参数名 ASCII 码升序拼接 sorted_keys = sorted(params.keys()) raw_string = "&".join(f"{k}={params[k]}" for k in sorted_keys) raw_string += "&key=" + secret_key return hashlib.md5(raw_string.encode("utf-8")).hexdigest() def create_order(performance_id, tier_id, num, viewer_ids): url = "https://api.ticket.example.com/order/create" payload = { "performanceId": performance_id, "tierId": tier_id, "num": num, "viewerIds": ",".join(viewer_ids), "timestamp": int(time.time() * 1000), # 毫秒级时间戳 } # 从抓包结果中提取签名密钥和拼接规则 payload["sign"] = build_sign(payload, auth["sign_secret"]) resp = session.post(url, json=payload) data = resp.json() if data.get("code") == 0: return data["data"]["orderId"] else: # 常见错误码:库存不足、风控拦截、参数错误 print(f"下单失败: {data.get('code')} {data.get('msg')}") return None签名逻辑是这段代码的灵魂。绝大多数票务平台不会明文传参数,而是要求客户端把所有业务参数按一定规则拼接后做哈希,再把哈希值放在请求里一起提交。服务端收到后会用自己的密钥重算一遍,比对一致才视为合法请求。
这里我要特意提醒一个细节:签名算法和密钥完整性比业务参数更容易被忽略。抢票前几天我会把抓包记录里的请求参数和签名值保存下来,写一个离线脚本尝试用不同的拼接规则复现签名,如果能复现出来,下单接口基本就稳了。复现不出来也不要气馁,说明平台可能引入了动态密钥或者更复杂的加密逻辑,这时候考虑下午节说的 playwright 方案——用浏览器自动化的方式绕开签名问题。
3.4 用 playwright 兜底:签名实在解不开,就走浏览器自动化
requests 路线要求你能完整复现签名,但现实中有相当一部分票务平台的签名算法做了加固,比如密钥来自服务端动态下发,或者签名是一次性的用后即焚。这种情况下,线性思维会卡死,换路线才是正解。
playwright 的价值在于,它不需要你理解签名逻辑。你只要写一个脚本,控制浏览器执行和人类用户一样的操作步骤:打开演唱会页、选择场次、点击票档、点击立即购买。浏览器自己会执行平台下发的加密 JS 代码,自动完成签名计算,你需要做的只是在合适的时点介入点击。
from playwright.sync_api import sync_playwright import time def auto_buy(performance_id, tier_id): with sync_playwright() as p: # 启动浏览器,带已有的登录态 browser = p.chromium.launch(headless=False) # 抢票时用有头模式,降低风控嫌疑 context = browser.new_context(storage_state="auth_state.json") page = context.new_page() # 跳到演唱会详情页 page.goto("https://ticket.example.com/performance/" + performance_id) page.wait_for_selector(f"[data-tier-id='{tier_id}']", timeout=5000) page.click(f"[data-tier-id='{tier_id}']") # 点击目标票档 page.click("text=立即购买") # 触发下单 page.wait_for_url("**/order/confirm**", timeout=3000) page.click("text=提交订单") time.sleep(1) print("最终页面 URL:", page.url) # 确认是否进入支付页 browser.close()这段脚本的核心思路是先用storage_state注入登录态,避免在自动化环境里重新扫码;然后用wait_for_selector确保目标票档元素已渲染;最后按业务顺序依次点击。这里headless=False是有意为之——无头浏览器在风控系统眼中的特征太明显,有头模式虽然占资源,但请求特征更接近真实用户。
playwright 的坑主要在元素定位上。票务平台的页面元素类名通常有动态前缀,比如class="vhp7TkQd-btn"这种每次发布都会变的混淆类名。所以我不建议写死 class,优先用文本(text=立即购买)或自定义属性(>import time import threading # 抢票主循环示例 def grab_ticket(): order_id = None fail_count = 0 while order_id is None and fail_count < 50: tiers = fetch_ticket_tiers(target_performance_id) target_tier = next((t for t in tiers if t["tierId"] == target_tier_id), None) if target_tier and target_tier["remainCount"] > 0: order_id = create_order( target_performance_id, target_tier_id, quantity, viewer_ids ) if order_id: print("抢到订单:", order_id) break else: # 没库存,或接口返回的是卖完状态,隔一会再查 fail_count += 1 time.sleep(0.5) if not order_id: print("连续 %d 次查询未抢到,建议检查是否触发风控" % fail_count) grab_ticket()
这段代码看起来简单,但它隐含了一个关键策略:失败重试加次数上限。fail_count < 50的循环条件意味着最多进行 50 次查询尝试,之后自动停止。为什么要设上限?因为如果脚本无限循环高频请求,一旦触发风控,账号会被限制登录,导致后续所有操作失败。有界重试虽然可能错过后面的库存释放,但至少保住账号还活着。
重试间隔 0.5 秒也是一个经验值。太短容易触发风控,太长可能错过库存释放窗口。如果你通过日志观察到某场售票是分批放票、每批间隔 5 秒左右,可以把这个值调到 1 秒,这样既有足够的频率覆盖,又降低风控失分风险。
5. 抢票脚本的常见问题排查:5 个让脚本失效的隐性原因
5.1 接口返回正常但下单失败:库存非实时与本地缓存偏差
现象:查询接口显示有票,但提交订单时提示“库存不足”或“已售罄”。
原因:票务平台的库存数据是多级缓存架构。查询接口读的是缓存层,而下单接口直接操作数据库库存。当大量用户同时抢票时,缓存里的库存数和真实库存之间存在秒级延迟,查询显示有票时真实库存其实已经卖完。
解决:不要把这个当成脚本 bug,这是正常现象。调整策略是缩短从查询到下单之间的时间,比如把库存判断和下单调用合并到一个函数里,减少中间变量赋值、打印日志等额外耗时。另一个思路是直接接受“下单失败就重新查询”的循环模式,把它看作概率事件——多试几次才能碰上缓存与真实库存同步的瞬间。
5.2 脚本运行正常但始终不触发下单:状态字段判断写错了
现象:轮询一直在跑,日志显示接口返回 200,但所有票档都被判定为“无库存”,实际上页面上明明显示有票。
原因:票档的库存状态字段不是布尔值。有的平台用remainCount表示剩余数量,有的用hasStock,还有的用字符串"Y"和"N"。更隐蔽的情况是嵌套结构——真实库存字段在data.tiers[].stock.status里,你从顶层取就永远是空。
解决:在写判断逻辑之前,先单独跑一次接口,用print(json.dumps(resp.json(), ensure_ascii=False, indent=2))把完整响应打印出来,人工核对字段路径和值的真实类型。这看起来笨,但能避开九成的判断错误问题。
5.3 请求频次过高被风控:从请求频率到验证码出现的连锁反应
现象:抢票中途接口开始返回特殊响应码,或者页面跳出滑块验证、点选验证码。
原因:频繁请求触达了平台风控阈值。风控系统通过统计单个账号在单位时间内的接口调用次数、请求间隔的规律性(比如固定间隔触发人工扫描特征)、请求来源 IP 的分布,综合判断是否为机器行为。
解决:立即停止高频请求,切换为人工操作模式或降低请求频率到正常用户水平。验证码出现后,先用人工完成验证,稍等几分钟让风控标记降级,再继续脚本操作。注意临时调大轮询间隔、增加请求时间的随机抖动(比如在 0.5 秒基础上加 0~0.3 秒随机值),能有效降低被识别为机器行为的概率。
5.4 登录态失效但报错信息误导:接口返回 401 还是跳转登录页?
现象:脚本没有报错,但请求返回的数据是 HTML 而不是 JSON,或者返回 200 但内容是一段登录跳转提示。
原因:登录凭证过期后,接口层不会统一返回 401,而是由网关统一拦截并返回一个登录页面。脚本如果只判断 HTTP 状态码(200 即正常),就会把登录页当成正常响应处理,导致解析 JSON 失败或拿到空数据。
解决:在每个关键接口的响应解析处增加“登录态有效性校验”,具体做法是判断响应体是 JSON 还是 HTML,如果是 HTML 或包含特定标志字段(比如“登录”字样),就抛出登录过期异常,并停止抢票流程。这一步绝对值得做,它能让问题在第一时间暴露,而不是等到下单失败才后知后觉。
5.5 抢到订单但支付环节失败:库存锁单后的窗口期没抓住
现象:脚本提示下单成功,订单也生成了,但人工打开支付页面时订单已经被取消。
原因:演唱会票务的下单和支付之间通常有严格的时间限制,常见的是 5 分钟或 15 分钟内必须完成支付,超时自动释放库存。如果下单成功脚本就停了,人工去支付时已经超过时限,订单就被系统回收。
解决:下单成功后不要立即退出,让脚本继续辅助支付流程。如果是扫码支付,保持页面停留并提醒人工尽快扫码;如果是调起支付接口,可以用 playwright 自动点击“确认支付”并进入支付 App。核心认知是:抢票成功不是终点,支付成功才是。脚本里要有支付倒计时提醒或自动拉起支付链路的逻辑,不要把最后一步甩给手工。
6. 从“能跑”到“能中”:三个可能撬动成功率的进阶认知
先说一个容易被忽视的参数:本地系统时间。很多票务接口的签名里包含时间戳,而平台服务端对时间偏差有校验,通常只允许正负 5 分钟的误差。如果你的本机时间慢了或快了,签名校验直接失败。更隐蔽的是,有些接口签名用分钟级时间戳,抢票前会有一个“时间窗口”,比如整点开抢,脚本在 10:59:59.800 发出的请求和 11:00:00.100 发出的请求,签名的分钟字段可能一样,但服务端却能识别出毫秒级差异。解决方法是抢票前用时间同步工具校准本机时间,并在脚本里把时间戳生成逻辑与实际业务对齐,不要依赖操作系统默认时区。
第二个进阶认知是库存释放的节奏感。热门演唱会通常不是一次性放完所有票,而是分批释放,每批次之间的间隔短则几秒、长则几分钟。第一波没抢到不要立即停止脚本,保持低频轮询可能等到第二波甚至第三波。有个有效的做法是记录每次“库存从 0 变为有票”的时间点,分析批次间隔规律,动态调整轮询间隔——间隔短就说明是连续放票,保持高频率;间隔变长就调慢频率,避免在空档期浪费请求额度。
第三个认知是关于多设备协同的边界。很多人问是不是手机和电脑同时跑脚本概率更高,实际上票务平台对同一账号的并发登录有严格限制,同一时间只能有一个有效会话。多设备同时下线意味着后登录的设备会踢掉前一个设备的登录态,结果两边一起失败。有效的多设备策略是分工而不是并行:一台设备跑脚本抢票,另一台设备不登录、只用来查看页面数据或监控是否有余票释放。脚本负责执行下单,人工负责情况判断和支付,这样才真正发挥出各自优势。
最后说一个我自己的教训。有段时间我把太多精力花在优化脚本的“抢票速度”上,试图把请求延迟从 300 毫秒压到 100 毫秒,后来发现对结果几乎没有影响——真正决定成败的是登录态是否稳定、签名是否正确、是否踩中风控红线。从那以后我养成了一个习惯:每次抢票前两小时,先跑一次完整的自测流程,用模拟数据验证查询、下单、支付各个环节都能正常响应,然后把日志关掉、把所有环境变量检查一遍、把手机调成免打扰,确保抢票那一刻脚本是干净的、可追踪的状态。这个习惯帮我避开了不止一次“开抢 5 分钟后才发现脚本配置错了”的窘境。抢票脚本这条路,做到“稳定可靠不出错”比“极限压缩耗时”重要得多。希望这篇笔记里的流程和踩坑记录,能帮你把这条路走得更顺一些。
本文还有配套的精品资源,点击获取