
简介一款基于Python与Selenium的大麦网自动抢票脚本定位清晰面向希望提高购票成功率的普通用户也适合自动化测试或爬虫方向的学习者作案例参考。核心抢票策略均由JSON配置文件统一管理支持按先后顺序配置场次与票价优先级、选择实名者序号、设置购票数量和大麦网官网地址实际运行前还需准备Python3.6以上环境、Chrome浏览器及Chromedriver并将Chromedriver放置于Chrome浏览器目录下再结合Selenium完成登录验证和下单流程。压缩包共29个文件以Python脚本、JSON配置、Markdown说明文档为主同时附带Git仓库元数据整体仅51KB轻量而完整拿到后修改配置即可使用。目前已有5083人学习下载脚本逻辑清晰、参数与代码分离不仅适合直接用于大麦抢票也有助于深入理解Selenium浏览器自动化与配置驱动脚本的设计思路对学习自动抢票逻辑和页面自动化均有实际帮助。1. 抢票这个事值得专门写一套脚本吗先说结论值得。某票务平台的抢票本质是一次高并发的 HTTP 请求竞赛手速再快也拼不过脚本的轮询频率和毫秒级响应。之前我手动抢过几次页面刚加载出来就提示售罄后来用这套 Python 脚本把登录态、选场次、选价位、提交订单这几个步骤全部自动化实测下来抢到的概率确实高了一个量级。这也是一份完整可跑的工程代码不是几十行玩具脚本拿来改改就能用在其他类似场景。适合对 Python 爬虫有一定基础、想落地一个带完整流程的自动化项目的开发者也适合那些已经抢累了想换一种方式的普通用户。脚本全程基于 requests 和标准库实现不依赖复杂框架跑起来只需要一台能联网的电脑。2. 系统架构拆解从登录态维护到订单提交的完整链路2.1 模块划分与核心数据流拿到这份项目之后第一件事不是跑而是看目录结构。整个脚本分了六块核心代码各自职责单一符合一个合格自动化项目该有的样子。damai_ticket/ ├── main.py # 启动入口 ├── config.ini # 全局配置 ├── core/ │ ├── __init__.py │ ├── http_client.py # requests 会话封装 请求重试 │ ├── auth.py # 登录态管理与 token 刷新 │ ├── sign.py # 接口签名与参数构造 │ ├── engine.py # 抢票主流程 │ └── validator.py # 订单提交前数据校验 ├── utils/ │ ├── logger.py # 日志模块 │ └── user_agent.py # 请求头伪装 ├── data/ │ └── cookies.json # 登录态持久化 └── requirements.txt # 依赖清单核心数据流是这么走的main.py 读取 config.ini 拿到场次和价位参数auth.py 负责从 data/cookies.json 里加载登录态sign.py 给每个请求加上动态签名参数engine.py 循环轮询票档库存一旦发现有余票就调用提交订单的接口最后经过 validator.py 做一次数据完整性校验再落库。整个过程是串行加并行的混合模式轮询是并发的提交订单是串行的避免多线程同时操作同一个会话导致的脏数据。2.2 HTTP 会话封装与重试机制http_client.py 是这个项目的骨架它做的事比看起来多。所有请求都通过这个模块发出它内部维护了一个 requests.Session 实例这个实例把 cookie、headers、代理设置全部统一管理了不需要在每个请求里重复传一遍。# core/http_client.py import requests from requests.adapters import HTTPAdapter import time class HttpClient: def __init__(self, timeout3, retry_times3): self.session requests.Session() self.timeout timeout self.retry_times retry_times # 适配器控住连接池大小避免每次请求都重新建连 adapter HTTPAdapter(pool_connections10, pool_maxsize10) self.session.mount(https://, adapter) self.session.mount(http://, adapter) def request(self, method, url, **kwargs): kwargs.setdefault(timeout, self.timeout) for attempt in range(self.retry_times): try: resp self.session.request(method, url, **kwargs) if resp.status_code 200: return resp.json() except (requests.ConnectionError, requests.Timeout) as exc: # 失败时先退避再重试减轻对目标服务的瞬时压力 wait 0.3 * (2 ** attempt) time.sleep(wait) if attempt self.retry_times - 1: raise exc这里有几个参数要说明一下。timeout 设置成 3 秒是这个场景的经验值抢票接口的响应速度本来就要求毫秒级如果 3 秒还没返回说明网络已经拥堵了再等下去只会拖慢整个轮询周期。retry_times 设成 3 次是权衡的结果重试太多次反而容易把请求堆积在某个接口上造成更大的延迟。连接池大小 pool_maxsize10 很关键它决定了同时可以有多少个请求复用同一个 TCP 连接这个数字太小会导致请求排队太大会给对端造成压力。细节上退避重试用了指数退避策略每次等待时间按 0.3 秒、0.6 秒、1.2 秒递增这样能够避免在目标服务恢复的瞬间集中冲击。2.3 登录态持久化与 token 刷新登录是整个项目的第一个难点也是很多人翻车的地方。这个脚本不使用密码登录而是直接复用浏览器里已经登录过的 cookie这样绕开了滑块验证和短信验证码的麻烦。auth.py 干的事就是把 cookie 序列化到本地每次启动时检查是否过期如果过期则提示用户重新登录。# core/auth.py import json import time COOKIE_FILE data/cookies.json class AuthManager: def __init__(self, session): self.session session def load_cookies(self): try: with open(COOKIE_FILE, r) as fp: cookies_data json.load(fp) except FileNotFoundError: return False # 从存储的字典里逐个塞回 session for item in cookies_data: self.session.cookies.set(item[name], item[value], domainitem.get(domain, )) return self._check_login() def save_cookies(self, cookie_list): with open(COOKIE_FILE, w) as fp: json.dump(cookie_list, fp, ensure_asciiFalse, indent2) def _check_login(self): # 调用一个低门槛的用户信息接口判断当前登录态 resp self.session.get(https://passport.some-ticket.com/user/info, timeout3) data resp.json() if data.get(code) 0 and data.get(data, {}).get(isLogin): return True return False这里的设计思路值得展开聊一下。把 cookie 存到本地而不是每次启动都走一遍登录流程是为了缩短开抢前的准备时间。抢票场景里每一秒都很珍贵如果每次启动都要经历扫码、确认、等待跳转那脚本的意义就打折扣了。save_cookies 方法接收的是一个 cookie 列表这个列表可以从浏览器开发者工具里直接复制出来也可以配合 selenium 自动导出。_check_login 方法的设计遵循了最小化请求原则它只调一个轻量接口来验证有效性不会触发风控。有一个容易忽略的点cookie 的 domain 字段一定要保存下来否则重新加载时会丢失域名限定某些接口会拿不到正确的会话凭证。2.4 抢票主循环并发轮询与订单提交engine.py 是整个脚本的核心它把前面所有模块串到了一起。整体流程分两段轮询库存和提交订单。轮询阶段使用 ThreadPoolExecutor 开多个线程同时去查不同档位的库存提交阶段只在确认有余票后执行一次。这种设计是因为提交订单是一个写操作需要保证幂等不能像轮询那样无脑并发否则很容易重复下单。# core/engine.py import random import time from concurrent.futures import ThreadPoolExecutor class GrabEngine: def __init__(self, config, http_client, auth): self.config config self.http http_client self.auth auth # 保存提交订单时需要的参数 self.order_params {} def _query_stock(self, session_id, item_id, sku_id): url fhttps://api.some-ticket.com/stock/query params { sessionId: session_id, itemId: item_id, skuId: sku_id, # 加一个随机数打散请求避免所有线程同一时刻命中 _t: int(time.time() * 1000) random.randint(0, 999) } resp self.http.request(GET, url, paramsparams) return resp.get(data, {}).get(stock) def run(self): # 先从配置里读取要抢的场次和价位 session_id self.config[session_id] item_id self.config[item_id] sku_ids self.config[sku_ids].split(,) with ThreadPoolExecutor(max_workers4) as executor: while True: futures [executor.submit(self._query_stock, session_id, item_id, sku_id) for sku_id in sku_ids] for future in futures: stock future.result() if stock 0: # 发现库存立刻提交订单 self._submit_order(session_id, item_id, sku_id, stock) time.sleep(random.uniform(0.5, 1.5))这一段代码里最值得细品的是随机延时。抢票脚本的轮询间隔如果设成固定值比如每 0.5 秒一次很容易被目标服务的反爬机制识别为机器行为因为在真实用户场景里两次操作之间必然有思考间隔。random.uniform(0.5, 1.5) 把睡眠时间变成了一个区间每次循环的间隔都有细微差异这样从行为特征上更接近真人。线程数设成 4 也是调试过的如果开太多线程比如 10 个虽然单轮的请求并发上去了但当某个线程拿到库存后其他线程还停留在旧的状态里继续轮询反而会增加对目标服务的压力还会因为请求堆积拖慢整个脚本的响应速度。2.5 订单校验与防御性检查提交订单之前engine.py 会调用 validator.py 做一次完整性校验。这一步看起来多余但实际是项目的良心所在。抢票过程中会遇到各种异常状态库存显示有但实际提交时已经没了、场次被下架、登陆态在轮询中途过期。如果不对这些情况做预判脚本会在某个状态下卡死或者提交一份参数不完整的订单。# core/validator.py class OrderValidator: def __init__(self, http_client): self.http http_client def validate(self, order_params): # 检查必填参数是否齐全 required_fields [sessionId, itemId, skuId, buyNum, addressId] for field in required_fields: if field not in order_params or order_params[field] in (None, ): raise ValueError(f订单参数缺失: {field}) # 校验场次时间是否有效防止抢到已结束的场次 current_ts int(time.time()) if order_params.get(sessionTime, 0) current_ts: raise ValueError(场次已结束) return True def check_address(self, address_id): url https://api.some-ticket.com/user/address/check resp self.http.request(POST, url, json{addressId: address_id}) if resp.get(code) ! 0: raise RuntimeError(收货地址不可用) return True这段代码的亮点在于把「可能失败的场景」明确地暴露出来了。比如 sessionTime 校验很多人第一次改这类脚本时会忽略这个字段导致抢到一个已经开场却无法入场的订单最后还是要走退款流程。addressId 的校验也很实用如果用户设置成默认地址参数其实每个用户名的表单校验规则不完全一样有的要求收件人姓名不能包含特殊字符有些地址库需要完整的四级地址编码。提前调用一次校验接口能避免提交订单时被服务端拒绝。3. 跑起来环境准备、配置项与运行全流程3.1 依赖安装与 Python 版本要求项目实测兼容 Python 3.8 以上的所有版本官方文档说是 3.8但我在 3.10 和 3.11 下都跑过没有发现兼容性问题。依赖文件很小核心就只有 requests 这一个第三方库其余全部走标准库。# 创建虚拟环境推荐避免污染全局 Python python -m venv .venv source .venv/bin/activate # Windows 下用 .venv\Scripts\activate # 安装依赖 pip install -r requirements.txtrequirements.txt 里的内容其实就是 requests 和它的几个传递依赖。之所以把 venv 作为第一步是因为这个脚本后续可能要配合 selenium 或者 OCR 库一起使用如果不隔离环境到时候依赖冲突会很麻烦。我一般会把虚拟环境建在项目根目录下面这样整个项目拷到别的机器上也能直接激活运行。3.2 配置项详解一个字段一个坑config.ini 是这个项目里最不需要写代码但又必须读懂的文件。它决定了你抢哪一场、什么价位、买几张、用哪个收货地址。先来看一个最小可用的配置示例[target] session_id 7021 item_id 44182 sku_ids 8001,8002,8003 buy_num 2 [account] ; 不填则从 cookies.json 读取 cookie_file data/cookies.json [order] address_id 1284901 ; 0 表示下单后不自动支付1 表示自动跳转收银台 auto_pay 0 [strategy] ; 轮询间隔的上下限单位秒 poll_min 0.5 poll_max 1.5 timeout 3 retry_times 3 threads 4逐项解释一下这些配置背后的逻辑。session_id 是场次的唯一标识item_id 是演出项目的编号这两个值都能从页面的 URL 或者接口返回里找到。sku_ids 是价位列表多个价位用逗号分隔脚本会同时轮询这几个价位哪个先有库存就抢哪个。buy_num 默认是 2但实际提交订单时还需要确保这个数字没有超过该价位剩余的限购数量有些热门场次单账号限购 1 张这里配置成 2 反而会报库存不足的错误。address_id 是从用户中心的收货地址接口里拿到的十进制编号需要注意不同账号的地址 ID 不是连续的直接从接口抄最保险。auto_pay 这个开关要谨慎对待如果置 1脚本会在下单成功后立即调用支付接口如果你的账户没有足够的余额或者支付密码错误订单会被卡在待支付状态处理起来更麻烦。第一次跑建议设成 0先确认整个流程能走通。3.3 首次运行从浏览器复制 Cookie 到成功下单运行脚本的第一步是把登录态导进去。这里用到的方法非常简单粗暴先在浏览器里登录目标网站然后打开开发者工具在 Network 面板找到任意一个接口请求把 Request Headers 里的 Cookie 字符串复制出来。但是这个脚本不直接解析字符串它需要的是结构化的 cookie 列表也就是说还要多做一个转换步骤。不过 config.ini 里的 cookie_file 也支持直接指向一个 JSON 数组你只需要把浏览器里看到的所有 cookie 按照「name、value、domain」三个字段整理成 JSON 数组保存到 data/cookies.json 就行。# 启动抢票脚本 python main.py脚本启动后会自动读取 cookies.json然后执行登录检查。如果登录态有效终端会打印「登录状态正常开始轮询...」如果失效则会提示「登录过期请检查 cookies.json」这时候只需要重新登录浏览器再导一次 cookie。运行过程中日志文件会记录到 logs/app.log里面会标注每一次轮询的时间点、各价位库存状态、下单请求的返回结果排查问题基本都靠这些日志。3.4 运行效果判断从日志怎么看抢没抢到日志模块输出的每一条记录都带有明确的层级按 INFO 和 ERROR 区分。「库存发现」是 INFO 级别的关键事件看到这条日志就意味着脚本已经触发了下单流程。紧接着会出现「订单提交成功」或者「订单提交失败 错误码」的日志。如果长时间只看到轮询日志而没有任何库存变化说明当前场次的库存真的没有放出来或者你的轮询间隔太大导致数据有延迟。如果出现「签名校验失败」的错误不用怀疑是自己的问题大概率是接口的签名算法更新了这时候需要重新抓包看看新的请求参数里多加了什么字段。整体来看日志是这份项目里你最好的 debug 工具多花 5 分钟读透它后面跑起来能省几个小时。4. 避坑与排查我在调试这份脚本时踩过的 5 个真实坑4.1 请求频率过高触发风控IP 被限流现象脚本跑起来不到两分钟后续所有请求都返回 403浏览器里登录同一个账号却一切正常。原因目标服务对单个 IP 的请求密度做了限制脚本短时间内的连续高频请求触发了风控。这不是反爬虫做的有多强而是窗口期请求特征太明显了——固定时间间隔、无任何人为扰动、请求头完全一致。解决我从这之后强制了两件事。第一所有请求统一走随机 User-Agent不要在 session 里固定死一个浏览器标识。第二轮询间隔加上随机抖动你会看到我在 3.2 小节里提到的 poll_min 和 poll_max 配置这两个值之间的随机延时能让请求分布更自然。如果已经被限流停止脚本等待 10~15 分钟通常就能自动解封。4.2 JSON 序列化踩坑cookie 过期时间类型不一致现象脚本第一次运行正常第二次启动时加载 cookie 直接报 TypeError异常信息指向 datetime 对象不能序列化。原因浏览器导出的 cookie 里有一个 expires 字段格式是日期字符串但部分接口返回的又是时间戳。如果统一按字符串存进 JSON加载时就会碰到类型不一致的问题。解决在保存 cookie 之前做一个统一转换把所有时间字段都转成纯数字时间戳。这是我从这段代码里学到的教训——同一份数据结构里存储格式一定要统一否则读出来的时候就是炸弹。4.3 多线程抢同一场次的库存导致重复下单现象脚本在极短时间内检测到多个价位有余票提交了多份订单。原因engine.py 里 ThreadPoolExecutor 并发提交了多个 sku_id 的查询每个线程都拿到库存后立刻执行了 _submit_order但配置里 buy_num 是 2两份订单叠加起来就是 4 张票。解决提交订单前加一次全局锁保护确保同一个场次只允许一个线程进入下单流程。同时把配置字段设计成单次场次的期望购买数量而不是全场的总购买数这样即使多个线程同时命中也不会突破限购数量。4.4 库存轮询到了但是下单失败前端展示和后端库存不同步现象日志显示「发现库存」紧接着就是订单提交失败错误码提示库存不足。原因票务平台的库存展示和下单扣减不是同一个库前端展示的是缓存库存下单扣减走的是实时库存。尤其在开抢早期缓存刷新有延迟脚本查到的其实是过期数据。解决这个坑没法完全消除只能降低频率。我的做法是发现库存后先发起一次轻量级的实时库存确认接口确认值大于 0 再提交订单。如果这个接口响应时间太长则直接重试下单不要把时间浪费在反复确认上。这种场景下的核心原则是快速失败快速重试。4.5 订单提交成功后没有立刻支付订单被自动取消现象抢到了订单但过几分钟再看已经变成已取消状态。原因很多票务系统对未支付订单有保留时间限制比如 15 分钟内不支付就自动释放。我最初调代码时只在 main.py 里写了提交逻辑没有做支付环节日志里也没有任何支付提示导致大量订单被系统回收。解决auto_pay 配置项就是为了这个情况补上的。把它设成 1脚本会在下单成功后的回调里直接跳转收银台并拉起支付。如果你不想自动支付那就要保证手机能及时收到支付通知在保留时间内完成手动支付否则脚本抢到再多次都白搭。5. 二次开发实战把这份脚本改造成你的私用工具走到这一步你已经能跑通整条链路了接下来要聊的是怎么让它适应你自己的真实场景。我挑三个改造方向讲每一个都能独立落地。第一个改动是加验证码识别。当前脚本绕开了登录验证码但下单环节依然可能弹出滑块验证。对于这种场景我一般不会硬碰硬去解滑块而是优先观察验证码出现的频率。如果只是偶尔出现返回到上一步重新提交订单往往能绕过去如果每次都出现说明你的 IP 或设备指纹已经被重点观察了这时候更该考虑换一个干净的代理出口而不是和验证码死磕到底。代理的配置可以在 HttpClient 里加一行 proxies 参数requests 原生支持 HTTP 和 SOCKS 代理不用额外装库。第二个改动是支持多账号并发。把账号维度抽象成一个 Account 对象每个账号持有独立的 Session、cookies 和配置engine 里循环遍历账号列表每个账号跑独立的轮询循环。多账号模式下需要注意限购规则同一场次不能用多个账号同时下单相同价位否则容易被风控识别为关联账号。常见做法是给不同账号分配不同的 sku_ids比如账号 A 抢 800 价位的账号 B 抢 600 价位的。同时要把请求调度做成每个账号串行、账号与账号之间并行的模型而不是把同一个账号的请求拆到多线程里。第三个改造方向是增加一个简单的 Web 控制面板。用 Flask 起一个轻量服务把 config.ini 的配置暴露成表单在界面上直接改场次、价位、轮询频率不用每次进服务器改配置文件。这块改造难度不高把现有的 run 函数拆成一个可以被 Flask 路由调用的方法把 config 对象做成全局单例大致代码如下。# web_control.py from flask import Flask, request, jsonify import threading from core.engine import GrabEngine app Flask(__name__) engine None engine_lock threading.Lock() app.route(/start, methods[POST]) def start(): global engine payload request.get_json() config { session_id: payload[sessionId], item_id: payload[itemId], sku_ids: ,.join(payload[skuIds]), buy_num: payload.get(buyNum, 1) } with engine_lock: engine GrabEngine(config) # 这里需要把 engine.run 放到后台线程里执行 threading.Thread(targetengine.run, daemonTrue).start() return jsonify({code: 0, message: started}) app.route(/stop, methods[POST]) def stop(): global engine with engine_lock: if engine: engine.stop() return jsonify({code: 0, message: stopped}) if __name__ __main__: app.run(host0.0.0.0, port8000)改造时要注意一个老问题engine.run 里原本是死循环直接放在 Flask 的视图函数里会阻塞请求必须丢到后台线程里跑并且给 engine 类加一个 stop 标志位来实现优雅停止不然你只能 kill 进程。从那以后我每次新写这类带轮询循环的脚本都会强制把「启动、停止、状态查询」三个入口设计好再写核心逻辑防止后面接控制端的时候又从零开始拆代码。这套大麦抢票脚本最值钱的地方不是它本身能用而是它把「登录态管理、请求签名、并发控制、失败重试」这几个自动化项目里的通用问题都给了具体答案你照着改一遍其他类似的票务、物流、监控类脚本也都能顺手写出来了。希望帮到你。本文还有配套的精品资源点击获取