简介:这是一份面向Python自动化学习者的B站会员购漫展抢票脚本项目资源,聚焦图形化操作与纯接口调用,并附带验证码预演练习模块,适合具备一定Python基础、希望研究网络请求与验证码识别流程的开发者参考。压缩包共66个文件,约19.54MB,以21个py脚本为核心,涵盖登录、下单、请求封装、验证码校验等模块,另含yml工作流配置、txt说明、md文档及sample示例文件,并配有ico、icns图标资源,整体结构接近可运行的完整项目。资源中集成了多种验证码校验器与工具类,如Cookie管理、时间服务、消息推送等,便于读者理解抢票流程中的关键环节与模块划分。目前已有92人学习下载。通过阅读源码与示例,读者可掌握接口请求组织、验证码预演练习、配置管理与打包发布等实践思路,同时需注意遵守平台规则与法律法规,合理使用技术手段。
1. 漫展抢票脚本的图形化与纯接口两条路:先想清楚你要复现的是什么
漫展门票开售那一秒,页面卡死、验证码刷不出来、订单提交转圈然后提示“已售罄”——这套流程做过一次就不想再做第二次。标题里这堆词拆开看,其实指向一个很具体的东西:用脚本去自动化 B 站会员购的漫展抢票流程,并且给出了两种实现路线,一种是图形化操作(模拟人在界面上点),一种是纯接口调用(直接构造请求)。再加上“验证码预演练习”,说明重点不在绕过什么,而在于把验证码出现到提交的这段链路提前跑通、练熟。
这篇文章面向的是想自己动手复现一套抢票自动化流程的开发者。我会把两条路线的选型逻辑、请求链路怎么拆、验证码环节怎么预演、参数怎么调、哪里最容易翻车,按我自己踩过的顺序讲一遍。需要提前说清楚:这类脚本的价值在于练习请求构造、会话管理和时序控制,不是让你去破坏平台规则;实际使用要遵守平台的服务条款,本文只讨论技术实现路径。
2. 抢票链路拆解:从商品页到订单提交到底经过了几次请求
2.1 先搞清楚会员购抢票的完整请求序列
很多人一上来就写代码,结果卡在“为什么我提交订单一直返回未登录”。原因是没把链路拆干净。一次完整的漫展抢票,从技术视角看大致是这么几步:
- 获取商品详情,拿到
project_id(项目 ID)和sku_id(票档 ID) - 查询库存/开售状态,确认当前是否可买
- 创建订单(下单),这一步会触发风控
- 如果触发验证码,走验证码流程拿到 token
- 用 token 重新提交订单
- 轮询订单状态,确认是否锁座成功
关键点在于第 3 步和第 5 步之间。下单接口返回的往往不是成功,而是一个“需要验证”的中间态,里面带着验证码的类型和 token。你得把这个 token 拿去完成验证,再带着验证结果回来重新下单。这个来回就是整个脚本的核心难点,也是“验证码预演”要练的东西。
我一般会先用浏览器开发者工具把这几步的请求全部抓一遍,记录每个请求的 URL、method、关键 header、body 结构、以及响应里哪些字段是下一步要用的。这一步不做,后面全是盲写。
2.2 图形化路线和纯接口路线的本质区别
图形化路线,本质是用自动化工具(比如基于浏览器内核的自动化框架)去驱动一个真实浏览器,模拟点击、输入、滑动。它的优势是:登录态、cookie、设备指纹、JS 执行环境都是真的,风控识别成本低;劣势是慢,每一步都有渲染和等待开销,抢票这种毫秒级场景下,慢就是致命伤。
纯接口路线,是自己用 HTTP 客户端直接构造请求。优势是快,一次下单请求可能几十毫秒就发出去了;劣势是你得自己维护会话、自己处理签名参数、自己应对风控对请求特征的检测。而且一旦平台改了接口参数或加密逻辑,脚本立刻失效。
我的建议是:练习阶段两条都做。图形化用来理解流程和验证码交互,纯接口用来练请求构造和时序优化。真正要拼速度的时候,纯接口是唯一选择,但图形化是你理解整个链路的拐杖。
2.3 用 Python 请求库跑通一次商品详情查询
先从一个最简单的请求开始,确认你的会话能拿到数据。下面这段代码只做一件事:带上 cookie 去查一个商品的详情,打印关键字段。
import requests # 会话对象,保持 cookie 和连接复用 session = requests.Session() # 这些 header 是必须的,缺一个都可能被风控拦截 session.headers.update({ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " "AppleWebKit/537.36 (KHTML, like Gecko) " "Chrome/120.0.0.0 Safari/537.36", "Referer": "https://mall.bilibili.com/", "Origin": "https://mall.bilibili.com", "Accept": "application/json, text/plain, */*", }) # cookie 从浏览器登录后复制,注意 SESSDATA 和 bili_jct 是核心 session.cookies.update({ "SESSDATA": "你的SESSDATA", "bili_jct": "你的bili_jct", "DedeUserID": "你的UID", }) # 查询商品详情,project_id 换成你要抢的漫展项目 resp = session.get( "https://mall.bilibili.com/mall-ugc/api/ticket/project/detail", params={"projectId": 123456, "skuId": 789012}, timeout=5, ) data = resp.json() print("返回码:", data.get("code")) print("商品名:", data.get("data", {}).get("projectName")) print("票档信息:", data.get("data", {}).get("skuList"))逻辑说明:这段代码的核心是建立一个带登录态的会话。SESSDATA是身份凭证,bili_jct是 CSRF token,很多写操作接口都要带它。Referer和Origin必须指向会员购域名,否则请求会被判定为跨站。
参数说明:projectId和skuId是每个漫展项目独有的,从商品页 URL 或抓包结果里拿。timeout设 5 秒,抢票场景下不要设太长,超时了直接重试比干等强。返回的code字段是判断成功与否的关键,通常 0 表示成功,非 0 要对照错误码排查。
跑通这一步之后,你至少确认了三件事:cookie 有效、header 没被拦、接口地址没变。接下来才是下单和验证码。
3. 验证码预演:把最不确定的环节变成可重复练习的流程
3.1 验证码为什么会成为抢票脚本的分水岭
下单接口触发风控后,返回的验证码类型可能有好几种:滑块、点选、短信、甚至直接弹一个图形码。滑块和点选类验证码,图形化路线可以直接用自动化框架去拖拽或点击;纯接口路线就麻烦了,你得先拿到验证码图片或参数,识别出缺口位置或点击坐标,再构造验证请求。
“预演练习”的意义就在这里:你不需要真的等到开售那一刻才发现验证码流程没跑通。你可以主动触发验证码(比如频繁请求下单接口),把验证码出现的条件、返回结构、验证接口的参数格式全部摸清楚,然后写一个可重复调用的验证模块。
我一般会单独写一个captcha_solver模块,输入是验证码的初始化参数,输出是验证通过的 token。这个模块可以独立测试,不依赖抢票主流程。
3.2 滑块验证码的缺口识别与轨迹构造
滑块验证码通常分两步:先请求一个验证码初始化接口,拿到背景图和滑块图;然后计算缺口位置,构造拖动轨迹,提交验证。
缺口识别可以用 OpenCV 做模板匹配或边缘检测。下面是一个简化版的缺口定位代码:
import cv2 import numpy as np def find_gap_offset(bg_path, slider_path): """ 输入背景图和滑块图路径,返回缺口横向偏移量 bg_path: 背景图(带缺口) slider_path: 滑块图(不带缺口) """ bg = cv2.imread(bg_path, cv2.IMREAD_GRAYSCALE) slider = cv2.imread(slider_path, cv2.IMREAD_GRAYSCALE) # 边缘检测,突出缺口轮廓 bg_edge = cv2.Canny(bg, 100, 200) slider_edge = cv2.Canny(slider, 100, 200) # 模板匹配,找到滑块在背景中最匹配的位置 result = cv2.matchTemplate(bg_edge, slider_edge, cv2.TM_CCOEFF_NORMED) _, max_val, _, max_loc = cv2.minMaxLoc(result) # max_loc[0] 就是缺口左边缘的横坐标 return max_loc[0], max_val offset, score = find_gap_offset("bg.png", "slider.png") print(f"缺口偏移: {offset}px, 匹配度: {score:.3f}")逻辑说明:cv2.Canny做边缘检测是为了去掉颜色干扰,只保留形状轮廓。cv2.matchTemplate用归一化相关系数匹配,返回值越大说明越匹配。max_loc的横坐标就是滑块需要移动的距离。
参数说明:Canny 的两个阈值 100 和 200 是经验值,背景图复杂时可以调低到 50/150。匹配度低于 0.6 说明识别不可靠,需要换方法或人工介入。
拿到偏移量之后,不能直接瞬移过去,风控会检测轨迹。要构造一个带加速度曲线的轨迹,模拟人手拖动。常见做法是用贝塞尔曲线或分段加速度模型生成一系列坐标点,每个点之间加随机延时。
3.3 验证码预演练习的三种触发方式
预演的关键是能稳定复现验证码,而不是靠运气碰。我试过三种方式:
第一种,直接调用下单接口,用错误的参数或高频请求触发风控。这种方式最接近真实场景,但可能触发账号级别的限制,练习时用小号。
第二种,找到验证码的独立初始化接口,直接请求它。很多平台的验证码模块是独立的,不依赖下单上下文,你可以单独调用它来练习识别和提交。
第三种,用图形化工具打开商品页,手动触发一次验证码,然后把验证码的请求和响应完整记录下来,离线重放。这种方式最安全,适合新手先理解流程。
三种方式各有适用场景。我一般先用第三种摸清结构,再用第二种做高频练习,最后用第一种做全链路联调。
4. 避坑与排查:抢票脚本最容易翻车的五个地方
4.1 坑一:cookie 过期但脚本不报错,一直返回未登录
现象:脚本跑起来不报异常,但所有接口返回code: -101或“账号未登录”。
原因:SESSDATA有有效期,通常几天到几周。过期后请求不会抛异常,只是返回未登录状态。很多人只看 HTTP 状态码 200 就以为成功了。
解决:在每次请求后检查业务返回码,非 0 就立即停止并提示重新登录。可以写一个装饰器统一处理:
def check_login(func): def wrapper(*args, **kwargs): resp = func(*args, **kwargs) data = resp.json() if data.get("code") == -101: raise RuntimeError("登录态失效,请更新 SESSDATA") return resp return wrapper4.2 坑二:请求间隔太短被限流,IP 被临时封禁
现象:前几次请求正常,之后全部超时或返回 412。
原因:平台对同一 IP 或同一账号的请求频率有阈值,超过就限流。抢票脚本如果无脑循环请求,几秒内就能触发。
解决:控制请求频率,下单接口的轮询间隔不要低于 200ms。如果要多账号操作,每个账号走不同的出口 IP。另外,收到 412 后不要立即重试,等 30 秒以上再试,否则封禁时间会延长。
4.3 坑三:验证码识别率不稳定,时好时坏
现象:同样的代码,有时候一次过,有时候连续失败十几次。
原因:验证码背景图有随机干扰、缺口位置随机、甚至验证码类型会动态切换。模板匹配对某些背景图效果很差。
解决:准备多套识别策略,模板匹配失败时切换到边缘检测或轮廓查找。同时记录每次失败的图片,离线分析失败原因。如果识别率长期低于 70%,考虑接入更稳定的识别方案,或者退回图形化路线用自动化框架处理。
4.4 坑四:订单提交成功但没锁到票,状态轮询缺失
现象:下单接口返回成功,但最后发现没有票,订单状态是“待支付”然后超时取消。
原因:下单成功不等于锁票成功。有些场景下订单创建了但库存没锁定,需要在一定时间内完成支付或确认。脚本如果没有轮询订单状态,就会错过确认窗口。
解决:下单成功后立即启动一个轮询循环,每隔 1 到 2 秒查一次订单状态,直到状态变为“已锁座”或“待支付”。同时设置超时,超过 30 秒没变化就重新下单。
4.5 坑五:签名参数算错,请求被风控静默丢弃
现象:请求返回 200,但 body 是空的或者返回一个通用的错误码,没有任何有用信息。
原因:很多接口有签名参数(比如sign或token),由请求参数和时间戳经过特定算法生成。签名错了,风控不会明确告诉你哪里错,只会静默丢弃。
解决:抓包对比。用浏览器正常操作一次,把请求的原始参数和签名值记录下来,然后用你的代码生成同样的参数,对比签名是否一致。如果不一致,逐步排查参与签名的字段、排序方式、拼接格式。这一步没有捷径,只能耐心比对。
5. 从练习到实战:把脚本跑稳的三个进阶技巧
5.1 用时间同步把请求卡在开售瞬间
抢票的核心是时机。你的本地时间和服务器时间可能有几百毫秒偏差,这几百毫秒就是成败分界线。我一般会在脚本启动时先请求一次服务器时间接口,计算出本地与服务器的偏移量,然后在开售前用这个偏移量校准。
import time import requests def get_server_offset(session): """计算本地时间与服务器时间的偏移量(秒)""" local_before = time.time() resp = session.get("https://api.bilibili.com/x/report/click/now", timeout=3) local_after = time.time() server_ts = resp.json().get("data", {}).get("now", 0) # 用请求往返的中间时刻作为本地参考 local_mid = (local_before + local_after) / 2 offset = server_ts - local_mid return offset offset = get_server_offset(session) print(f"服务器比本地快 {offset:.3f} 秒") # 开售前等待,校准后的目标时间 target_ts = 1700000000 # 开售时间戳,从商品页获取 while time.time() + offset < target_ts - 0.05: time.sleep(0.001) # 提前 50ms 开始发请求,抵消网络延迟逻辑说明:local_mid是请求发出和返回的中间时刻,用它和服务器时间戳比较,得到的偏移量比单纯用local_before更准。提前 50ms 发请求是为了抵消网络往返延迟,具体提前多少要根据你的网络质量调整。
参数说明:target_ts是开售时间戳,通常在商品详情接口里能拿到。time.sleep(0.001)是 1 毫秒,循环等待时不要用更长的间隔,否则会错过窗口。
5.2 用多线程并发提高成功率,但别把账号搭进去
单线程下单,一次请求往返可能 100ms 以上。开售瞬间库存可能几百毫秒就没了。用多线程并发发多个下单请求,能提高命中概率。但要注意:同一账号并发请求过多会触发风控,建议控制在 3 到 5 个并发,且请求之间加微小随机延时。
import threading import random import time results = [] def try_order(session, payload, delay): time.sleep(delay) try: resp = session.post( "https://mall.bilibili.com/mall-ugc/api/ticket/order/create", json=payload, timeout=3, ) results.append(resp.json()) except Exception as e: results.append({"error": str(e)}) threads = [] for i in range(4): # 每个线程错开 10 到 30 毫秒 d = i * 0.01 + random.uniform(0, 0.02) t = threading.Thread(target=try_order, args=(session, order_payload, d)) threads.append(t) for t in threads: t.start() for t in threads: t.join() # 检查是否有成功的结果 for r in results: if r.get("code") == 0: print("下单成功:", r) break逻辑说明:每个线程的启动时间错开,避免同一毫秒发出多个请求被判定为机器行为。random.uniform增加随机性。收集所有结果后检查是否有成功的。
参数说明:线程数 4 是经验值,账号权重高可以适当增加,新号建议 2 到 3 个。timeout设 3 秒,超时直接放弃这个请求,不要等。
5.3 用日志和录屏把每次失败变成可复现的案例
抢票脚本最怕的是“这次失败了但不知道为什么”。我的习惯是每次运行都写详细日志:每个请求的 URL、参数、响应码、响应体关键字段、时间戳。同时用屏幕录制工具把整个操作过程录下来。失败之后回看日志和录像,能快速定位是网络问题、验证码问题还是参数问题。
日志建议用结构化格式,方便后续分析:
import logging import json logging.basicConfig( filename="ticket_run.log", level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s", ) def log_request(url, params, resp_data): logging.info(json.dumps({ "url": url, "params": params, "code": resp_data.get("code"), "message": resp_data.get("message"), "timestamp": time.time(), }, ensure_ascii=False))逻辑说明:用 JSON 格式记录,后续可以用脚本批量分析失败模式。ensure_ascii=False保证中文正常显示。
参数说明:日志文件按天切割,避免单个文件过大。关键字段是code和message,分析时先按这两个字段分组统计。
这套东西跑下来,你会发现抢票脚本的难点从来不是“写代码”,而是理解请求链路、处理验证码、控制时序、以及从失败中快速定位问题。我自己的习惯是每次开售前至少做三次全链路预演,把验证码、下单、轮询都跑一遍,确认没有遗漏。希望帮到你。
本文还有配套的精品资源,点击获取