简介:这是一份围绕i茅台平台预约流程整理的自动化脚本资源包,面向具备一定Python基础、希望了解电商预约自动化实现思路的技术爱好者与开发者。资源以脚本源码与配置示例为主,可用于研究登录、预约、订单提交等环节的自动化逻辑,并理解请求加密与流程编排的基本方法。压缩包共10个文件,包含6个py脚本、2个txt说明、1个md文档和1个example配置示例,整体约8KB,体积轻量,便于快速阅读与本地调试。其中py文件承担核心业务逻辑与测试模块,txt与md提供依赖说明和使用指引,example文件则给出配置模板,方便对照修改。目前已有142人学习,适合作为Python网络编程与自动化流程的入门参考案例,帮助读者梳理项目目录结构、理解模块划分,并在此基础上进行二次学习与功能扩展。
1. i茅台预约脚本:从手动抢购到自动化调度的工程化拆解
每天上午九点,i茅台 App 的申购页面会准时涌入大量请求,手动点击的窗口期往往只有几秒。很多人以为抢不到是网速问题,其实真正的瓶颈在于「人肉操作的响应延迟」和「请求时序的不可控」。i茅台预约脚本要解决的核心问题,就是把「打开 App、定位商品、点击申购、确认提交」这条链路压缩成毫秒级的自动化流程。它适合两类人:一类是想提升申购成功率的普通用户,另一类是想研究移动端接口调度与任务编排的开发者。需要先明确一点,脚本的本质是「用程序模拟合法用户操作」,而不是绕过平台规则。理解了这个边界,后面的技术选型和实现路径才不会跑偏。
2. 拆解 i茅台预约链路:接口、参数与调度模型
2.1 从 App 操作到 HTTP 请求的映射关系
要写一个能跑的预约脚本,第一步不是打开编辑器,而是搞清楚 i茅台 App 在申购时到底发了什么请求。常见做法是用抓包工具观察 App 与服务器之间的通信,重点关注三个维度:请求地址、请求头、请求体。
申购动作通常对应一个 POST 请求,请求体里会携带商品 ID、门店 ID、申购数量等字段。请求头里则包含用户身份凭证,比如 token 或 cookie。这些信息不是固定不变的,token 有有效期,门店 ID 会随每日放货情况调整。所以脚本不能把参数写死,必须做成可配置、可刷新的结构。
我一般会先把一次完整的申购流程拆成四个阶段:登录态维持、商品信息获取、申购请求构造、结果回调处理。每个阶段对应不同的接口和参数,下面这张表是我在实际调试中整理的字段说明。
| 阶段 | 关键参数 | 来源 | 是否可变 |
|---|---|---|---|
| 登录态维持 | token / cookie | 登录接口返回 | 每日刷新 |
| 商品信息获取 | 商品 ID、门店列表 | 商品查询接口 | 每日变动 |
| 申购请求构造 | 商品 ID、门店 ID、数量 | 上一步结果 | 每次请求 |
| 结果回调处理 | 订单号、状态码 | 申购接口返回 | 实时 |
这张表的价值在于:它告诉你哪些参数需要动态获取,哪些可以缓存。把可变参数和固定参数分开管理,是脚本稳定运行的前提。
2.2 请求签名的常见处理方式
i茅台接口在请求头或请求体中通常会带一个签名字段,用来校验请求的合法性。这个签名的生成逻辑是脚本能否跑通的关键卡点。常见做法有两种:一种是把签名算法逆向出来,用代码复现;另一种是直接复用 App 内的签名逻辑,通过 Hook 方式动态获取。
第一种方式对技术能力要求较高,需要分析 APK 里的加密函数,涉及 Java 层或 Native 层的逆向。第二种方式相对省事,但需要设备有 root 权限或使用模拟器环境。我一般会先尝试抓包看签名是否固定,如果每次请求签名都不同,再考虑逆向。
这里要提醒一句:签名算法可能会随 App 版本更新而变化。今天能用的逻辑,下个版本可能就失效了。所以脚本架构上要把签名模块做成可替换的插件,而不是硬编码在主流程里。
2.3 定时调度与并发控制的最小实现
预约脚本的核心竞争力在于「在正确的时间发出正确的请求」。i茅台的放货时间通常是固定的,比如每天上午九点。脚本需要在放货前完成登录态检查、商品信息预加载,然后在放货瞬间发起申购请求。
下面是一个用 Python 实现的最小调度框架,重点看时间同步和重试逻辑。
import time import requests from datetime import datetime, timedelta # 配置区:放货时间和提前量 TARGET_HOUR = 9 TARGET_MINUTE = 0 PREPARE_AHEAD_SECONDS = 30 # 提前30秒开始准备 def wait_until_target(): """阻塞等待到目标时间前30秒""" now = datetime.now() target = now.replace(hour=TARGET_HOUR, minute=TARGET_MINUTE, second=0, microsecond=0) if target < now: target += timedelta(days=1) prepare_time = target - timedelta(seconds=PREPARE_AHEAD_SECONDS) sleep_seconds = (prepare_time - now).total_seconds() if sleep_seconds > 0: time.sleep(sleep_seconds) return target def submit_order(session, payload, max_retries=3): """提交申购请求,带重试""" for attempt in range(max_retries): try: resp = session.post( url="https://app.moutai519.com.cn/xhr/front/mall/reservation/add", json=payload, timeout=5 ) if resp.status_code == 200: return resp.json() except requests.RequestException as e: print(f"第{attempt+1}次请求失败: {e}") time.sleep(0.2) # 短暂退避后重试 return None if __name__ == "__main__": target_time = wait_until_target() print(f"准备就绪,目标时间: {target_time}") # 此处应已完成登录和商品信息预加载 # session = login_and_prepare() # result = submit_order(session, build_payload())这段代码的逻辑说明:wait_until_target负责时间对齐,提前 30 秒唤醒是为了留出网络抖动和本地处理的余量。submit_order里的重试次数不要设太大,3 次足够,因为放货窗口很短,重试太多反而会错过时机。参数方面,PREPARE_AHEAD_SECONDS可以根据本机时钟精度调整,如果系统时间不够准,建议先做 NTP 同步。
并发控制是另一个容易翻车的点。有些人为了提升成功率,会同时开多个线程或进程发请求。但 i茅台对同一账号的并发请求通常有风控,轻则返回错误码,重则临时封禁。我一般会控制在单账号单线程,如果需要多账号,用队列串行处理,每个账号之间加 1 到 2 秒间隔。
3. 环境搭建与脚本运行:从零跑通第一个预约任务
3.1 Python 环境与依赖安装
脚本用 Python 写是最常见的选择,因为 HTTP 请求库和定时任务库都很成熟。建议用 Python 3.8 以上版本,太老的版本在 SSL 和异步支持上会有问题。
依赖方面,核心就三个:requests负责发请求,schedule或直接time做定时,pycryptodome在需要处理加密签名时用到。安装命令如下:
pip install requests pycryptodome如果你打算用模拟器抓包,还需要在电脑上装抓包工具,并配置模拟器的网络代理。这部分不展开,重点放在脚本本身的运行环境上。
提示:不要把脚本放在系统盘根目录或需要管理员权限的路径下,避免因权限问题导致日志写不进去。
3.2 登录态获取与 token 刷新
脚本运行的第一步是拿到有效的登录态。i茅台的登录方式通常是手机号加验证码,验证码需要人工输入一次。所以脚本没法完全无人值守,但可以把登录态缓存下来,有效期通常能维持几天。
常见做法是:首次运行时手动登录,把返回的 token 和 cookie 保存到本地文件。后续运行直接读取,如果发现过期再重新登录。下面是一个简单的 token 管理模块。
import json import os from datetime import datetime TOKEN_FILE = "token_cache.json" def save_token(token, cookie, expire_hours=72): """保存登录态到本地""" data = { "token": token, "cookie": cookie, "saved_at": datetime.now().isoformat(), "expire_hours": expire_hours } with open(TOKEN_FILE, "w") as f: json.dump(data, f) def load_token(): """读取本地登录态,过期返回None""" if not os.path.exists(TOKEN_FILE): return None with open(TOKEN_FILE, "r") as f: data = json.load(f) saved_at = datetime.fromisoformat(data["saved_at"]) elapsed = (datetime.now() - saved_at).total_seconds() / 3600 if elapsed > data["expire_hours"]: return None return data["token"], data["cookie"]参数说明:expire_hours默认设 72 小时,是根据实际观察到的 token 有效期保守估计的。如果你发现经常过期,可以调小这个值,让脚本更早触发重新登录。TOKEN_FILE的路径建议放在脚本同级目录,方便迁移。
3.3 商品信息预加载与门店选择
放货前几分钟,脚本需要先拉取当天的商品列表和可申购门店。这一步的目的是把后续申购请求需要的参数提前准备好,避免在放货瞬间还要等接口返回。
商品查询接口通常返回 JSON 格式的数据,里面包含商品 ID、名称、价格、可申购门店列表。脚本要做的是根据预设规则筛选出目标商品和门店。比如你只想申购某款酒,就按名称过滤;如果对门店没要求,就选库存最多的那个。
def fetch_products(session): """获取当日可申购商品列表""" resp = session.get( url="https://app.moutai519.com.cn/xhr/front/mall/shop/list", timeout=5 ) if resp.status_code != 200: return [] data = resp.json() products = [] for item in data.get("data", []): products.append({ "id": item["skuId"], "name": item["name"], "shops": item.get("shopList", []) }) return products def pick_target(products, keyword): """按关键词筛选目标商品""" for p in products: if keyword in p["name"]: return p return None这段代码的关键在于pick_target的筛选逻辑。实际使用时,关键词要写准确,比如「飞天」或「珍品」。如果返回的门店列表为空,说明当天该商品没有放货,脚本应该跳过而不是硬发请求。
3.4 申购请求构造与结果解析
前面三步准备好之后,最后一步就是构造申购请求并发送。请求体里通常需要商品 ID、门店 ID、申购数量。数量一般是 1,部分商品可能允许 2。
def build_payload(sku_id, shop_id, count=1): """构造申购请求体""" return { "skuId": sku_id, "shopId": shop_id, "count": count, "type": 1 # 常见值,具体以抓包为准 } def parse_result(resp_json): """解析申购结果""" code = resp_json.get("code") if code == 200: return "申购成功", resp_json.get("data", {}).get("orderId") elif code == 401: return "登录态失效", None else: return f"失败: {resp_json.get('message', '未知错误')}", None结果解析里要特别关注 401 状态码,它意味着 token 过期,脚本需要触发重新登录流程。其他错误码比如「库存不足」「不在申购时间」属于正常业务返回,不需要重试。
注意:申购接口的返回速度直接影响成功率。如果解析逻辑太复杂,会拖慢下一次请求的发出时间。建议把结果解析做成异步或轻量处理。
4. 避坑与排查:预约脚本常见的五个翻车现场
4.1 现象:脚本显示请求成功,但 App 里查不到订单
原因:请求被服务器接收了,但业务层校验没通过。常见的是门店 ID 和商品 ID 不匹配,或者申购数量超出了该门店的限购数。
解决:抓包对比手动操作和脚本请求的完整参数,重点检查shopId和count字段。另外,部分商品要求先预约再申购,脚本如果跳过了预约步骤,申购请求会被静默丢弃。
4.2 现象:运行几天后突然全部失败,返回 401
原因:token 过期了,但脚本没有正确处理刷新逻辑。i茅台的 token 有效期不是固定的,可能因为异地登录、修改密码等操作提前失效。
解决:在每次请求前检查 token 有效性,或者捕获 401 后自动触发重新登录。重新登录需要人工输入验证码,所以脚本要能暂停并提示。
4.3 现象:放货瞬间请求超时,日志显示连接被重置
原因:本机网络到服务器的链路在高峰时段拥塞,或者请求频率触发了风控的限流策略。
解决:把请求超时时间从默认的 10 秒调小到 3 到 5 秒,失败后快速重试。同时检查是否有其他程序占用带宽。如果是风控限流,需要降低请求频率,单账号每秒不超过 1 次。
4.4 现象:签名校验失败,返回 403
原因:App 版本更新后签名算法变了,脚本里用的还是旧逻辑。
解决:重新抓包分析新版本的签名生成方式。如果用的是 Hook 方案,检查 Hook 脚本是否适配了新版本。建议在脚本里加一个签名校验的开关,方便快速切换。
4.5 现象:多账号运行时,后面的账号全部失败
原因:多个账号共用同一个 IP 或设备指纹,触发了平台的反作弊机制。
解决:每个账号之间加足够的间隔时间,至少 2 秒。如果条件允许,不同账号走不同的网络出口。设备指纹方面,模拟器要修改 IMEI 和 MAC 地址,避免被识别为同一设备。
5. 进阶技巧:用日志回放和参数热更新提升脚本存活率
脚本跑通只是第一步,能不能长期稳定运行,取决于你对失败案例的复盘能力。我习惯在脚本里加一个「请求日志」模块,把每次申购的完整请求和响应写到本地文件。格式用 JSON Lines,每行一条记录,方便后续用脚本分析。
import json from datetime import datetime LOG_FILE = "reservation_log.jsonl" def log_request(url, payload, resp_status, resp_body): """记录每次请求的完整信息""" record = { "time": datetime.now().isoformat(), "url": url, "payload": payload, "status": resp_status, "body": resp_body[:500] # 截断避免日志过大 } with open(LOG_FILE, "a", encoding="utf-8") as f: f.write(json.dumps(record, ensure_ascii=False) + "\n")有了日志之后,可以写一个回放脚本,把失败请求重新发一遍,观察返回是否一致。如果某个参数在多次失败中都出现,那它就是重点排查对象。比如连续多次返回「门店不存在」,说明门店 ID 的获取逻辑有问题。
参数热更新是另一个实用技巧。把商品关键词、门店优先级、重试次数这些配置项放到一个独立的 JSON 文件里,脚本启动时读取。这样调整策略不需要改代码,改完配置文件重启脚本就行。
{ "target_keyword": "飞天", "shop_priority": ["旗舰店", "自营店"], "max_retries": 3, "retry_interval_ms": 200, "prepare_ahead_seconds": 30 }最后说一个我踩过的坑:不要迷信「多线程并发能提高成功率」。i茅台的风控对并发请求非常敏感,我见过有人开了 10 个线程,结果账号被限制申购 7 天。后来改成单线程加精准定时,成功率反而更稳定。脚本这东西,稳定比激进重要。希望帮到你。
本文还有配套的精品资源,点击获取