简介:面向B站会员购用户的自动化抢票脚本,解决热门活动门票开售瞬间手动操作慢、难以抢到的问题。脚本以Python为主,模拟用户填写购票信息、刷新页面、点击购买等操作,提升购票效率,适合有一定Python基础、希望提高抢票成功率或研究自动化购票逻辑的读者。压缩包共10个文件,约6KB,包含py主程序、txt配置与依赖说明、md说明文档以及xml/iml工程配置,结构精简,便于快速定位核心代码。目前已有6669人浏览学习。通过这份资源,读者可获得完整抢票脚本源码、运行依赖清单及基础使用说明,既能参考其自动化实现思路,也可自行修改账号、场次等参数。需要提醒的是,使用此类脚本应遵守平台规则,注意账号安全。
1. bilibili会员购抢票脚本是什么:从“演出票秒没”到自动化下单
热门演出的票往往在开售瞬间被抢空,手动操作从点击“立即购买”到成功提交订单,少说要两三秒,而脚本能把这个过程压缩到几百毫秒,并在开售时刻精确触发。这就是 bilibili会员购抢票脚本 的用处:它本质上是一个自动下单工具,用程序模拟你在会员购页面里的登录、选场次、点按钮、提交订单这一整套动作。标题里那个 zip 只是它的分发形态,里面通常装着一份 Python 脚本、依赖清单和运行说明。这个方案不承诺必抢到,它解决的是“手速和网络延迟追不上开售时间”这个真实问题。适合两类人:一是自己抢过几次票、总在最后一步失败,想搞明白下单链路的人;二是想研究活动页自动化下单方案、准备把它接到自己项目里的工程师。我拿到这类 zip 的第一反应不是双击运行,而是先拆开看它走通了哪条链路、在哪个环节抢时间。把这个链路摸清楚,比直接跑脚本重要得多。
2. 拆解bilibili会员购的购票链路:抢票脚本到底在抢什么
2.1 会员购的购票流程与四个关键节点
bilibili 会员购里买演出票,表面上看是「选场次 → 立即购买 → 提交订单 → 支付」,但站在脚本的角度,这条链路可以拆成四个必须精确命中的节点。
第一个节点是「登录态」。脚本必须带着一个有效的登录 Cookie 去请求页面和接口,否则在选座或提交订单阶段会被弹回登录页。第二个节点是「商品数据」。开售前页面会把场次、票价、库存状态渲染出来,脚本要能稳定地定位到目标场次,而不是等开售后再去找按钮,那会浪费最宝贵的几百毫秒。第三个节点是「按钮状态」。未开售时页面上通常有一个置灰的「立即购买」或「预约抢购」按钮,开售瞬间按钮变为可点击状态,这个状态切换就是脚本的下手时机。第四个节点是「订单提交」。点击立即购买之后,页面会跳到确认订单页,可能还要勾选观演人、阅读并同意购票协议,最后点「提交订单」。这一步最容易出现库存已被抢完、接口报错、参数校验失败等异常。
把这四个节点对应到代码上,就是一个固定的工作流:读取登录态 → 打开活动页 → 精确等待开售时刻 → 轮询按钮状态 → 点击并提交订单 → 记录结果。手动抢票输在反应速度,脚本赢在可以把最后两步的间隔压到几十毫秒,而且不会因为紧张点错位置。
2.2 校验参数与风控边界:为什么纯 requests 方案容易翻车
很多 zip 里带的脚本是纯 requests 方案:直接请求会员购的商品详情接口和创建订单接口,把下单动作简化成两次 HTTP 请求。这个方案理论速度最快,绕过页面渲染,直连接口,但落地时经常被两个问题卡住。
第一个问题是接口签名。bilibili 的 Web 接口普遍带签名参数,签名由固定参数和时间戳按特定规则计算生成,而且会不定期更新算法。zip 里如果附带的是几个月前的签名算法,开售当天大概率直接失效,脚本报错在签名校验不通过。第二个问题是风控。纯 requests 的请求特征太明显,没有浏览器环境指纹、没有完整 UA、没有页面上下文,高频请求同一个下单接口,很容易触发验证码或账号风控,而验证码一弹出来,纯 requests 方案基本就废了,因为它没有渲染验证码页面的载体。
所以我一般不太推荐新手直接跑纯 requests 方案,除非你确认 zip 里的签名算法是近期的、并且你有能力在报错时自己逆一下新的签名规则。大部分从社区流出来的抢票脚本,翻车点都在这:签名过期、缺少 Cookie 字段、请求频率过高。这也是为什么我更倾向于用 Playwright 这类浏览器自动化方案。它不关心接口签名怎么算,因为它做的就是模拟真人操作,页面要什么参数浏览器自己会带上,签名算法对脚本透明,降低的是维护成本,代价是速度比直连接口慢一点点,但对抢票来说,这点差距远小于翻车的概率。
2.3 两条技术路线怎么选:requests 定时请求还是 Playwright 无头浏览器
这里做一个直接对比,方便你拿到 zip 后快速判断要不要改造它。纯 requests 方案的优势是启动快、内存占用低、可以做到非常精确的定时请求,适合已经验证过签名算法、接口稳定、且你有能力处理风控的场景。Playwright 方案的强项是稳定、抗页面改版、验证码出现时还能人工介入救场,适合绝大多数非专业爬虫工程师。
| 对比项 | requests 定时请求 | Playwright 自动化 |
|---|---|---|
| 下单速度 | 最快,毫秒级直连接口 | 稍慢,需等待页面渲染和点击事件 |
| 签名算法依赖 | 高,签名一改就失效 | 低,浏览器自动处理 |
| 验证码应对 | 基本无法处理 | 可以切有头模式人工处理 |
| 风控特征 | 明显,容易被识别 | 接近真实用户 |
| 维护成本 | 高 | 低,选择器变了改一下即可 |
| 适合人群 | 熟悉接口逆向的工程师 | 大多数使用者 |
选型建议很直接:如果你只是想把 zip 里的脚本跑起来去抢一次票,优先选 Playwright 方案。如果 zip 里已经是一个完整的 requests 方案,而且你验证过签名还能用,那也可以直接跑,但要做好失败后自己动手修的准备。我个人的习惯是,无论哪种方案,先拿一个已经开售、还没卖完的场次做全流程测试,跑通了再等正式开售。这一步能筛掉九成的问题。
3. 用 Playwright 在本地跑通自动抢票的最小脚本,把 ZIP 里的方案还原出来
3.1 环境准备:创建虚拟环境与安装依赖
拿到 zip 之后,第一步不是找「立即抢购.exe」,而是先解压到一个纯英文路径下,比如D:\tools\bili-grab。解压后先看目录结构,常见做法是里面会有 Python 脚本、requirements.txt或run.bat。我们从头搭一个能跑的 Playwright 环境,顺便验证依赖有没有缺失。
cd D:\tools\bili-grab python -m venv venv venv\Scripts\activate pip install playwright playwright install chromium这段命令做了三件事:创建虚拟环境隔离依赖、安装 playwright 库、下载 Chromium 浏览器内核。需要注意,playwright install chromium会下载约 150MB 的浏览器文件,如果解压目录在中文路径下偶尔会出权限问题,所以刚才强调先用纯英文路径。装完后可以跑一句python -c "from playwright.sync_api import sync_playwright; print('ok')"确认环境可用。zip 里如果还有requirements.txt,就顺手执行pip install -r requirements.txt,把里面列出的依赖和 playwright 装到一起,避免后面运行时报 ModuleNotFoundError。
3.2 第一步:手动登录一次并把登录态存成文件
抢票脚本最难处理的往往不是抢购逻辑,而是登录态。每次开售时扫码登录肯定来不及,所以常规做法是提前手动登录,把登录后的 Cookie 存到本地文件,脚本启动时直接加载。下面这个脚本用有头模式打开浏览器,你手动扫码登录,回车后保存登录态。
# login.py from playwright.sync_api import sync_playwright STATE_FILE = "state.json" with sync_playwright() as p: browser = p.chromium.launch(headless=False) context = browser.new_context( user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " "(KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36" ) page = context.new_page() page.goto("https://www.bilibili.com") input("完成扫码登录后,回到这里按回车键继续...") context.storage_state(path=STATE_FILE) browser.close()这段代码先用launch(headless=False)打开带界面的浏览器,这是为了让你能看见页面并完成扫码。new_context里显式设置了 UA,减少被风控识别为自动化工具的概率。最关键的是context.storage_state(path=STATE_FILE),它会把你登录后的 Cookie 和 LocalStorage 全部序列化到state.json。之后抢票脚本启动时只要加载这个文件,就相当于带着登录态直接进入活动页。建议登录完检查一下state.json文件大小,至少十几 KB 才算正常,如果只有几百字节,多半是没登录成功。
3.3 第二步:开售前的精确等待与按钮轮询
正式抢票脚本要做的事,是在开售时刻之前打开活动页、保持页面待命,然后以极高的频率监视「立即购买」按钮的状态。下面这段代码实现了这个核心逻辑:
# grab.py import asyncio, time, logging from playwright.async_api import async_playwright TARGET_URL = "https://show.bilibili.com/xxxx" # 替换成目标活动的场次页地址 BUY_BUTTON_TEXT = "立即购买" SUBMIT_TEXT = "提交订单" START_TIME = time.mktime(time.strptime("2025-06-01 20:00:00", "%Y-%m-%d %H:%M:%S")) logging.basicConfig( level=logging.INFO, filename="grab.log", format="%(asctime)s %(levelname)s %(message)s", ) async def wait_until_start(): while True: gap = START_TIME - time.time() if gap <= 0: return await asyncio.sleep(0.01 if gap > 0.1 else 0.001) async def main(): async with async_playwright() as p: browser = await p.chromium.launch(headless=False) context = await browser.new_context(storage_state="state.json") page = await context.new_page() await page.goto(TARGET_URL) logging.info("页面已加载,等待开售") await wait_until_start() for attempt in range(30): try: btn = page.get_by_text(BUY_BUTTON_TEXT, exact=True) if await btn.is_disabled(): await asyncio.sleep(0.05) continue await btn.click() logging.info("已点击立即购买,准备提交订单") submit = page.get_by_text(SUBMIT_TEXT, exact=True) await submit.click(timeout=3000) logging.info("订单提交成功,attempt=%s", attempt + 1) await page.screenshot(path="order_success.png") break except Exception as e: logging.warning("第 %s 次尝试失败:%s", attempt + 1, e) await asyncio.sleep(0.1) await browser.close() if __name__ == "__main__": asyncio.run(main())这段代码的核心在于wait_until_start和按钮轮询的组合。wait_until_start在剩余时间大于 0.1 秒时每 10ms 检查一次,最后 0.1 秒改为每 1ms 检查一次,目的是把触发误差控制在几毫秒内。按钮轮询用is_disabled()判断按钮是否可点,未开售时按钮是置灰的,脚本会每 50ms 轮询一次,一旦变成可点击状态立刻点击。exact=True参数很关键,避免误匹配到页面里其他包含「立即购买」字样的元素。
3.4 第三步:提交订单时的页面变化与日志落盘
点击「立即购买」之后页面会跳到确认订单页,这个页面通常包含观演人选择、运费或优惠计算、提交按钮。不同活动的确认页结构差异很大,有的默认勾选观演人,有的需要手动勾选,这也是 zip 里的脚本经常翻车的地方。我的做法是在测试阶段先手动点一次完整流程,用浏览器开发者工具记录确认页的 DOM 结构,然后在脚本里补一个选择器:
# 确认页可能出现的观演人选择,按实际页面调整 try: checkbox = page.locator(".order-person input[type=checkbox]").first if await checkbox.is_visible(): await checkbox.check() logging.info("已勾选观演人") except Exception: logging.warning("未找到观演人勾选框,可能已默认勾选")这段代码是防御性的,没有找到勾选框就跳过,不会导致脚本报错中断。日志落盘用的logging.FileHandler会把每次点击、每次失败都记录到grab.log,开售结束后打开日志就能看出脚本是在哪一步失败的。这一点很重要,因为抢票失败之后复盘,最怕的就是什么都没有记录,不知道是按钮没监听上、还是点击时页面卡顿、还是提交时接口被拒。有日志至少能定位到具体环节。
如果你需要跑在 Linux 服务器上做自动化值守,Playwright 也支持,安装时用playwright install --with-deps chromium会自动装齐系统依赖库。这样脚本就可以常驻一台小机器上,到点自动执行,比整天开着本地电脑更省心。
4. zip压缩包打开与运行失败的排查记录:伪加密、中文路径、依赖缺失
4.1 现象:明明没设密码,解压却提示要密码
这是从社区下载的抢票脚本 zip 里很常见的问题。zip 文件提示要密码,但发布者明明说没有密码,或者密码就是简单的1234但输入后仍报错。多数情况下这不是真正的加密,而是「zip 伪加密」。伪加密的原理是 zip 文件头里有一个「加密标志位」,某些打包工具或人为修改把这个标志位置 1,但文件数据本身并没有被加密。解压软件看到标志位就会索要密码,真实数据却可以直接读出来。
解决伪加密不需要爆破密码,只需要把文件头里的加密标志位清零。下面这段脚本可以批量修复:
import struct, sys def fix_zip_fake_encrypt(path: str) -> None: data = bytearray(open(path, "rb").read()) off = 0 fixed_local = 0 fixed_central = 0 while off < len(data): sig = bytes(data[off:off + 4]) if sig == b"PK\x03\x04": # 本地文件头 flags = struct.unpack_from("<H", data, off + 6)[0] struct.pack_into("<H", data, off + 6, flags & 0xFFFE) fixed_local += 1 name_len = struct.unpack_from("<H", data, off + 26)[0] extra_len = struct.unpack_from("<H", data, off + 28)[0] off += 30 + name_len + extra_len elif sig == b"PK\x01\x02": # 中央目录文件头 flags = struct.unpack_from("<H", data, off + 8)[0] struct.pack_into("<H", data, off + 8, flags & 0xFFFE) fixed_central += 1 off += 4 else: off += 1 open(path, "wb").write(data) print(f"已重置 {fixed_local} 个本地文件头、{fixed_central} 个中央目录文件头的加密位") if __name__ == "__main__": fix_zip_fake_encrypt(sys.argv[1])用法是python fix_zip_fake_encrypt.py xxx.zip,脚本会遍历 zip 文件的所有 PK 签名块,把通用标志位里最低位的「加密标记」清掉。需要注意,这个方案只对伪加密有效,如果文件是真正的 AES 或传统 ZipCrypto 加密,清掉标志位后解压出来的文件是损坏的,那种情况只能找正确密码。
4.2 现象:双击 run.bat 窗口闪退,什么输出都看不到
zip 解压出来通常带一个run.bat或启动.bat,双击后黑窗口一闪而过,什么错误信息都没留下。原因是 bat 脚本里执行的 Python 程序报错退出,但窗口在报错信息展示前就被关闭了。
解决方法是手动打开 CMD 再执行脚本,让窗口停留在报错界面:
cd /d D:\tools\bili-grab venv\Scripts\activate python grab.py pause在 CMD 里执行时,python报的ModuleNotFoundError、SyntaxError、ValueError都会保留在窗口里。我看到最多的就是ModuleNotFoundError: No module named 'playwright',这就对应着环境没装全,回到 3.1 节的安装步骤重新执行即可。还有一种闪退原因是 bat 文件里用了python3命令,Windows 上只有python没有python3,把命令改成python就能解决。
4.3 现象:运行报错 FileNotFoundError 或路径含中文
Windows 上解压到C:\Users\张三\下载\抢票脚本这类中文路径,Python 脚本里如果有相对路径或硬编码路径,偶尔会报编码错误或找不到文件。这个问题在 zip 脚本里尤其常见,因为脚本作者通常在 Linux 或纯英文 Windows 环境开发,没有覆盖中文目录场景。
我的建议是强制把目录建到D:\tools\或C:\tools\下,用纯英文目录名。这不是玄学,而是 Python 在 Windows 上处理中文路径时的默认编码可能和文件系统编码不一致,特别是脚本用open()打开日志文件、读取配置文件时会踩到坑。如果你必须放在中文目录下,也可以在脚本顶部加一句import os; os.chdir(os.path.dirname(os.path.abspath(__file__))),把工作目录强制切到脚本所在位置,缓解相对路径错乱的问题。
4.4 现象:杀毒软件把解压出来的 exe 或 dll 直接删除
有些 zip 里带的不是纯 Python 脚本,而是打包成 exe 的成品,比如用 PyInstaller 打的包。这类文件经常被杀毒软件误报,尤其是里面包含浏览器内核或自动化库时,杀软会把行为特征判定为可疑程序。我见过不止一次,解压时一切正常,运行时报「找不到模块」或「无法启动此程序」,结果去临时目录一看,exe 早就被隔离删除了。
遇到这种情况,先把杀毒软件的隔离区翻一遍,确认被删的是脚本依赖还是主程序。如果是误报,把解压目录加入杀毒白名单,重新解压一次再运行。更稳妥的做法是选择纯 Python 源码版本而不是 exe 版本,源码不会触发 exe 的行为检测,只是需要自己装环境。抢票工具这种东西本身确实有争议,杀软严格一点不是坏事,你要么接受误报加白名单,要么自己从源码跑。
4.5 现象:解压时提示「缺少分卷」或「压缩文件已损坏」
部分 zip 在网上传播时会被网盘拆分或上传出错,导致下载下来的 zip 不完整。解压软件提示「需要下一分卷」或「文件头损坏」时,先别急着修,重新下载一遍往往就解决了。如果重新下载后还是报错,再用 4.1 节的脚本检测是不是伪加密,因为伪加密有时也会让部分解压软件报「文件头损坏」。真正的 zip 数据损坏很难修复,不用浪费时间折腾。
5. 抢票脚本的关键参数:定时精度、重试策略与请求降频
5.1 定时器:不要在脚本里直接 sleep 到开售时刻
很多新手写抢票脚本时会这样写:time.sleep(开售时间 - 当前时间),然后再执行点击。这是典型的错误用法。time.sleep的精度依赖操作系统的线程调度,Windows 上默认时间片约 15.6ms,实际唤醒误差可能达到几十毫秒甚至更多,加上 sleep 之前代码本身的执行时间,触发时刻很容易晚 100ms 以上。100ms 对抢票来说可能就是有票和没票的区别。
更稳的做法是用一个「校准循环」,每一小段 sleep 之后重新计算剩余时间:
import time def calibrated_sleep(target_ts: float) -> None: while True: left = target_ts - time.perf_counter() if left <= 0: return if left > 0.2: time.sleep(0.01) elif left > 0.01: time.sleep(0.001) else: # 最后 10ms 用空转忙等,避免系统调度误差 pass这个函数的思路是:剩余时间大于 200ms 时,每 10ms 校准一次;小于 10ms 后不 sleep,直接空转循环,用time.perf_counter()的高精度时钟把触发误差压到 1ms 以内。调用方式很简单,把开售时间转成时间戳后传给calibrated_sleep(START_TS)即可。
5.2 重试策略:订单提交失败后指数退避,而不是瞬间连点
点击「提交订单」之后,网络请求需要几百毫秒返回。如果失败,最常见的错误是「手慢了,票已被抢完」或「接口繁忙」。这时要不要立刻重试?要,但不能每秒重试 10 次。瞬间连点会让接口压力集中在同一秒,反而更容易触发风控,而且如果上一次请求其实已经创建了订单,再点一次会报重复下单。
我常用的重试策略是指数退避加上最大次数限制:
import time, random def retry_with_backoff(fn, max_retries=5, base_wait=0.5): for i in range(max_retries): try: return fn() except Exception as e: wait = base_wait * (2 ** i) + random.uniform(0, 0.2) time.sleep(wait) raise RuntimeError("重试次数已用完")参数可以按实际情况调整:第一次失败后等约 0.5 秒,第二次约 1 秒,第三次约 2 秒,最多重试 5 次。加上random.uniform(0, 0.2)的随机抖动,是防止重试请求呈现严格的等间隔特征,这种特征容易被反爬识别。如果你抢的是非常热门的票,可以把max_retries调到 8,但base_wait不要低于 0.3 秒,低于这个值就失去了退避的意义。
5.3 请求降频与风控:抢票不是越快越好
这是血泪经验:抢票脚本最容易翻车的地方不是没抢到,而是抢的过程中被风控拦了。活动页面上除了你主动点击,脚本自己也在高频轮询按钮状态、刷新库存信息。如果每 50ms 就调一次is_disabled(),Playwright 内部会执行 DOM 查询,频率过高会拖慢页面响应,甚至被服务器侧识别为异常流量。
| 参数 | 推荐值 | 说明 |
|---|---|---|
| 按钮轮询间隔 | 50ms - 100ms | 太低会让页面卡顿,太高会错过按钮变化 |
| 商品接口刷新间隔 | 300ms 以上 | 低于 300ms 容易被风控 |
| 重试等待 | 0.5s 起步,指数退避 | 避免同一时刻爆发请求 |
| 登录态有效期 | 活动前重新生成 | 不要用一个月前的 state.json 抢票 |
还有一点容易被忽略:不要在整个抢票过程中反复刷新页面。页面刷新意味着重新加载整套资源,速度远不如在当前页面上轮询按钮。正确做法是开售前 1 分钟加载好页面,之后只在页面内部做 DOM 查询和点击操作,不触发整页刷新。日志里如果出现大量ERR_ABORTED或请求超时,优先检查是不是轮询频率太高把网络栈占满了。
5.4 日志记录:把每次请求结果落盘,方便复盘
最后补一个可能救命的习惯:抢票脚本一定要带日志,而且日志里要记录关键时间点。每轮轮询不记录,但点击按钮、提交订单、异常报错这三件事必须记录。开售结束后,打开日志文件,你就能看到从「页面已加载」到「订单提交成功」中间花了多少毫秒,失败的话卡在哪个环节。这个信息比任何猜测都有用。我见过太多人抢票失败后完全不知道发生了什么,就因为脚本里一个print都没写,窗口一关什么都没留下。加日志只需要几行代码,但能让你在下一次抢票前把问题定位清楚,这个投入非常值。
6. 验证你的脚本能正常下单,以及两个值得投入的进阶方向
验证脚本能不能用,只有一个标准:在没有开售压力的时候,能不能完整走通「点击立即购买 → 到达确认订单页 → 提交订单」。具体做法是,找一个已经开售、还没卖完的低价活动,把TARGET_URL换成这个活动页,正常跑一次脚本。如果它能成功提交订单(不付款,直接关掉浏览器),说明登录态、按钮选择器、确认页逻辑都是通的。这一步建议在正式开售前 1 到 2 天做,留出修改选择器和调试的时间。
第二个验证点是时间同步。脚本里START_TIME用的是你电脑本地时间,如果电脑时间不准,哪怕差 1 秒都直接失败。开售前半小时手动校准一次系统时间,或者脚本里改成从 NTP 服务器同步时间后再计算剩余秒数。我习惯在calibrated_sleep之前打一行日志输出当前时间和服务端返回的时间差,确认偏差在几百毫秒以内再进入等待循环。
如果你想在这个方案上继续投入,有两个方向值得做。一是多场次监控,把单个TARGET_URL改成配置文件里的场次列表,脚本启动后先遍历所有场次页,每个场次起一个独立的页面实例去轮询,适用于同时开售多个场次、需要抢其中一个的场景。二是结合消息通知,订单提交成功或脚本异常退出时,通过 Bark 或 Telegram bot 推一条消息到手机,避免你干等半小时不知道结果。这里注意把通知的 API Key 放在单独的配置文件里,不要写死在脚本中,防止 zip 分发时泄露。
我现在已经不在抢票前临时改脚本了,每次活动前都会拿一场非热门商品把全流程跑一遍,确认登录态、按钮选择器、提交订单三处都没变化,才敢在正式开售前坐下来等。这个习惯帮我避免过两次现场翻车,一次是按钮文案从「立即购买」改成了「马上抢」,一次是确认页新增了观演人勾选,都靠测试提前发现。抢票脚本说到底是个和时间赛跑的小工具,跑通一次不难,难的是每次开售前都保证它还是通的。希望帮到你。
本文还有配套的精品资源,点击获取