尧图精选

Python自动化抢票脚本技术拆解:从HTTP请求到Playwright实战

🕒 发布时间:2026/9/16 16:24:02 📁 来源:尧图网络
简介这份代码是一套基于Python的大麦网演唱会抢票自动化程序面向有一定Python基础的开发者、自动化测试工程师及运维人员用于解决手动抢票操作繁琐、时机难把握的问题。压缩包共2个文件包含1个py脚本和1个txt说明文档整体仅3KB轻量易读py为主程序逻辑txt为配套说明便于对照学习。目前已有9045人学习下载说明其内容对自动化抢票场景有实际参考价值。程序完整覆盖登录模拟、余票监控、多线程并发抢票、OCR验证码识别、异常处理与日志记录等关键环节并融入了requests、selenium、BeautifulSoup、pytesseract等常见Python库的使用思路还提及云服务器部署与容器化运维的方法。阅读源码后读者既能为自己的自动化脚本项目提供直接借鉴也能加深对Web自动化、定时任务和网络请求处理的理解适合作为相关课程设计或毕业设计的参考资料。1. 抢票拼手速的本质是自动化的临界请求大麦网演唱会抢票难在“开票瞬间”的高并发页面加载、选座、提交订单每一步都有人工延迟。基于 Python 的自动化大麦网抢票程序就是把“看到票可点”到“提交订单”这段人工链路替换为脚本直接请求。适合懂一点爬虫、想用自动化解决重复操作的开发者也可以作为 Playwright、协程和请求重试的练手项目。需要先说清楚本文只做技术拆解请遵守平台用户协议不要把脚本用于批量抢票、囤票或任何商业倒卖行为。2. 先拆 HTTP 层购票请求、Cookie 与风控很多人上手就写“点击按钮”但抢票真正可控的部分在 HTTP 请求层。浏览器里每次点击都对应一个接口脚本要做的不是模拟鼠标而是模拟接口调用。先拆链路再决定用哪种方式实现。2.1 一次抢票涉及的关键请求与字段演唱会项目的购票流程通常由四条请求组成查详情、查库存、创建订单、提交订单。查详情用来拿到场次和票档查库存用来判断是否可买创建订单会生成订单号提交订单才真正锁票。第二步到第四步之间往往有时间窗口脚本必须保证在这几秒里按顺序完成。用开发者工具抓包时重点记录这几个字段字段名含义来源itemId演唱会项目 ID详情页 URL 中的 idperformId场次 ID详情接口返回skuId票档/座位级别 ID详情接口返回priceName票档名称如“看台580”详情接口返回stockInfo当前库存状态库存接口返回下面这段代码演示了如何用 requests 拉取大麦网详情接口并从中取出场次和票档信息。它是抢票程序的入口也是后面定时任务轮询的对象。import requests import json session requests.Session() session.headers.update({ User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) ..., Referer: https://detail.damai.cn/item.htm?id1000000, }) def fetch_perform(item_id: str) - dict: url https://mtop.damai.cn/h5/mtop.alibaba.damai.wireless.item.detail/1.0/ params { jsv: 2.6.1, appKey: 12574478, data: json.dumps({itemId: item_id}, separators(,, :)), } resp session.get(url, paramsparams, timeout5) resp.raise_for_status() return resp.json()这段代码最需要注意的是data参数必须用 JSON 字符串很多初学者直接传 Python 字典导致接口签名校验失败。appKey不是固定值需要从页面某个 JS 文件中动态读取常见做法是用正则从 HTML 源码里抓取。Referer要和当前项目详情页保持一致否则部分接口会返回风控错误。拿到返回值后data.result里会有performList和priceList。实际开发中一般先在开发者工具里确认当前版本的返回结构再写解析逻辑不同场次、不同票档的 ID 都从这里取。还需要注意某些场次有“缺货登记”而不是“立即购买”此时详情接口返回的库存状态会是不可售轮询循环里要主动过滤掉。2.2 登录态 Cookie 的获取与复用创建订单和提交订单都需要登录态。抢票程序里最常见的登录方式不是代码输入账号密码而是用浏览器扫码登录后在开发者工具里复制 Cookie。这样能避开滑块验证同时保留手机验证等风险控制机制。把 Cookie 字符串写入 requests.Session 就能复用。session requests.Session() def load_cookie(session: requests.Session, cookie_text: str) - None: for item in cookie_text.split(; ): key, _, value item.partition() if key: session.cookies.set(key, value, domain.damai.cn)这里按;切分partition用来处理没有等号的片段。domain参数很重要部分 Cookie 分成.damai.cn和detail.damai.cn两种域set 错域名会导致请求带不上 Cookie。建议把原始 Cookie 字符串保存成文本下次启动时读进来避免手动重复复制。2.3 反爬机制与合规边界大麦网的风控主要看三样请求频率、账号行为、设备指纹。requests 默认的指纹太明显所以要设置完整的 Headers但不要伪造太夸张的值保持和自己浏览器一致即可。频率方面同一个账号每秒超过 3-5 次请求就容易触发风控一旦触发可能不是报错而是返回“亲活动太火爆了”这类迷惑文案。requests 方案虽快但大麦网详情页的加签参数会变维护成本高。浏览器自动化解法虽然速度慢但页面自己执行 JS签名不会漏。因此很多抢票程序是“查库存用 requests提交订单用 Playwright”的混合模式。合规边界的判断标准不是“我是否用了自动化”而是“是否影响他人公平购票、是否用于牟利”。自己学习用脚本完成一次订单提交和用脚本囤票倒卖是两回事。这代代码都只为演示技术过程接入真实环境前请确认你拥有合法访问权限且不对平台造成压力。3. 用 Playwright 搭建浏览器自动化抢票流程requests 方案虽快但页面加签参数会变维护成本高。Playwright 是目前维护最积极的浏览器自动化库它直接驱动 Chromium/WebKit/Firefox页面内 JS、接口签名都由真实浏览器完成。对抢票场景来说最大的优势是wait_for_selector能处理弹窗和异步加载比 Selenium 写显式等待更顺手。3.1 为什么抢票脚本选 PlaywrightSelenium 年代常见问题是 WebDriver 特征明显、经常被检测而且等待机制要靠轮询。Playwright 提供自动等待选择器等元素可操作才继续执行还支持移动端模拟和网络拦截。下表是两者在抢票场景里的直观对比对比项PlaywrightSelenium安装pip install playwright 后还要装浏览器需要手动下载 driver自动等待内置默认等待元素可操作需要 expected_conditions隐形特征较少可配置真实 UA 等特征较多多页面管理context.pages 直接切换需要 window_handles 字母顺序同步/异步两套 API 都有主要靠线程包装对抢票这种“页面结构会变、时机不可控”的任务自动等待能直接吃掉大部分时序问题。3.2 最小编码打开项目页并点击购买下面代码是一个最小的 Playwright 抢票流程覆盖打开浏览器、跳转详情页、点击购买按钮。注意选择器只是示例真实页面需要按当天 DOM 调整。from playwright.sync_api import sync_playwright DAMAI_URL https://detail.damai.cn/item.htm?id你的项目ID def buy_ticket(): with sync_playwright() as p: browser p.chromium.launch(headlessFalse, slow_mo50) context browser.new_context( viewport{width: 1280, height: 800}, user_agentMozilla/5.0 ..., ) page context.new_page() page.goto(DAMAI_URL, wait_untildomcontentloaded, timeout10000) page.wait_for_selector(text立即购买, timeout5000) page.click(text立即购买) # 选票档后等待提交按钮 page.wait_for_selector(text提交订单, timeout3000) page.click(text提交订单) # 停留几秒手动完成支付或继续进入订单页 page.wait_for_timeout(5000) browser.close()这里的参数含义headlessFalse让浏览器有头运行方便观察抢票节点是否卡住slow_mo50给每一步操作加 50ms 延迟避免动作太快触发风控。生产环境可以把这俩参数改成可配置调试结束后headlessTrue。wait_untildomcontentloaded表示等待 DOM 就绪就开始找按钮比load更快适合抢票场景。点击“立即购买”后页面通常会弹出大麦网的“登录确认”或“观影人”弹窗这时 Playwright 会自动等待page.wait_for_selector。如果弹窗是 iframe还要先page.frame_locator切换到对应 frame。很多时候抢票失败不是网络不行而是“提交订单”按钮被弹窗遮住无法点击所以脚本里必须显式等待按钮可操作而不是页面加载完成。3.3 复用已有登录态跳过每次扫码每次运行都扫码登录很影响速度。Playwright 允许把登录后的 Cookie 或 StorageState 保存成文件下次直接加载。context.storage_state(pathdamai_state.json)第二次启动时context browser.new_context( storage_statedamai_state.json, viewport{width: 1280, height: 800}, )storage_state包含 Cookie 和 LocalStorage比单纯传 Cookie 字符串更适合大麦网这类带本地缓存的站点。注意这个文件保存了登录凭证不要提交到 git也不要分享给任何人。有效期取决于平台一般是数天到数周失效后需要重新扫码生成。如果抢票脚本部署在 Linux 服务器上没有浏览器界面可以先用本机 Playwright 生成damai_state.json再传输到服务器。Linux 服务器本身不需要图形界面但需要安装 Playwright 的 Chromium 依赖常见命令是python -m playwright install chromium --with-deps这行命令在 Debian/Ubuntu 上会同时安装系统库。4. 抢票的定时、并发与重试参数把时机调到准抢票的核心动作不在代码逻辑多复杂而在“什么时候启动”和“失败后怎么办”。前端看到的倒计时和服务器时间存在偏差直接按本地时间启动会错过窗口。4.1 对齐服务器时间误差控制在 50ms 以内常见做法是用 HTTP 响应头里的Date字段估算本地和服务器的时间差。虽然网络往返会带来延迟但在同一城市网络下误差通常在几十毫秒足够抢票使用。import email.utils import time import requests def estimate_time_offset() - float: resp requests.get(https://detail.damai.cn/, timeout3) date_header resp.headers.get(Date) if not date_header: raise RuntimeError(no Date header) server_ts email.utils.mktime_tz(email.utils.parsedate_tz(date_header)) latency resp.elapsed.total_seconds() return server_ts latency / 2 - time.time()说明parsedate_tz把 HTTP date 字符串解析成时间戳resp.elapsed是请求从发出到收到响应头的耗时。把延迟的一半加回去是估算服务器在响应发出那一刻的时间。这个latency不是精确的但足够把时间戳对齐到几十毫秒。若服务器时间与本地差超过几分钟优先检查系统时区和 NTP 服务不要只用脚本纠偏。启动策略统一是先循环等待本地时间到达目标点然后立即执行购票函数。不要在本地时间到点前 200ms 内做复杂计算避免调度延迟吃掉窗口。4.2 并发参数设置并发数和请求频率很多人误以为抢票就是疯狂开线程。大麦网风控会按账号维度限制订单频率同一个账号同时发几十个创建订单请求结果往往是被封数据接口。常见的稳妥做法是同一账号同时只允许一个下单会话配合 2-3 个线程轮询库存接口。参数推荐值说明下单并发1同一个账号并发下单会被风控库存轮询间隔1 秒开票前可放松到 0.5 秒开售后不要更频繁请求超时3 秒超过直接放弃本次请求避免线程堆积单次重试次数3 次超过后进入休息而不是继续发请求如果想彻底异步化也可以用 aiohttp asyncio 替代 requests ThreadPoolExecutor但抢票这种短连接场景收益不大反而增加调试难度。并发可以用ThreadPoolExecutor但要注意 Cookie 对象不是线程安全的最好每个线程持有独立的requests.Session共用同一个 Cookie 字符串去初始化。from concurrent.futures import ThreadPoolExecutor import requests COOKIE_TEXT 复制来的Cookie def make_session() - requests.Session: session requests.Session() for item in COOKIE_TEXT.split(; ): key, _, value item.partition() if key: session.cookies.set(key, value, domain.damai.cn) return session def poll_stock(session: requests.Session, item_id: str) - None: # 查询库存并决定是否执行下单 pass with ThreadPoolExecutor(max_workers3) as pool: sessions [make_session() for _ in range(3)] futures [pool.submit(poll_stock, s, 你的项目ID) for s in sessions]代码里每个线程都重新解析 Cookie 生成 Session避免 Session 对象跨线程复用导致连接池状态混乱。max_workers3是表里的推荐值这不是越高越好超过 5 个会让风控更容易识别。4.3 重试策略指数退避而不是无限重试抢票场景最常见的失败是超时和提交订单冲突。无脑重试会让服务器压力加倍也容易把自己的 IP 拉黑。指数退避是通用可靠方案第一次失败等 0.5 秒第二次等 1 秒第三次等 2 秒并加一点随机抖动。import random import time def retry_submit(submit_fn, max_retries3): for attempt in range(max_retries): try: return submit_fn() except Exception as exc: if attempt max_retries - 1: raise exc wait 0.5 * (2 ** attempt) random.uniform(0, 0.2) time.sleep(wait)max_retries3意味着最多尝试 3 次。等待时间依次约为 0.5-0.7 秒、1.0-1.2 秒第三次是最后一次失败直接抛出。随机抖动是为了避免多台机器在同一个时间点同时重试。注意如果失败原因是“库存不足”而不是网络异常就不应该重试因为重试不会改变结果应该继续轮询库存。只有网络超时、接口 5xx 这类临时错误才适合退避重试。5. 抢票主循环状态机与三个页面卡点前三章分别讲了 requests 链路、Playwright 点击、定时重试。实际抢票中这几部分的衔接才是成败所在。这一章给一个整合后的状态机骨架并且列出最容易导致脚本假死的页面问题。5.1 抢票主循环的最小状态机不用把抢票程序写得像微服务但至少要有一个清晰状态切换否则调试时会分不清是“还没到时间”还是“已经卡住”。我习惯用枚举加 while 循环。import time from enum import Enum, auto class State(Enum): WAIT auto() LOAD auto() SKU auto() SUBMIT auto() DONE auto() FAILED auto() def run_bot(page, start_ts: float): state State.WAIT while state ! State.DONE and state ! State.FAILED: if state State.WAIT: if time.time() start_ts: state State.LOAD elif state State.LOAD: page.reload() if page.locator(text立即购买).count() 0: state State.SKU elif state State.SKU: page.click(text立即购买) page.click(.sku-item) # 选中第一个票档 state State.SUBMIT elif state State.SUBMIT: page.click(text提交订单) state State.DONE return state这段状态机会在WAIT阶段空转到点后刷新页面检查“立即购买”按钮是否存在。没有票时按钮通常是“缺货登记”或“即将开抢”这时count()返回 0循环会一直在LOAD状态重试。page.reload()比反复goto更接近手动操作而且会保留页面内的初始参数。票档选择器.sku-item只是示例真实页面可能是div.sku-item span或带>import winsound if page.locator(text请完成验证).count() 0: winsound.Beep(880, 2000) # Windows 提示音 page.wait_for_timeout(60000) # 留 60 秒给人工拖动滑块这段代码的思路是“自动到验证人工收尾”。winsound 只适用于 Windows部署到 Linux 时换成os.system(printf \\a)或日志轮询。提示音之后脚本不要立即重试否则验证码会刷新成新的。等人工完成后页面状态继续执行提交订单。如果人工没有完成60 秒后可以重新触发验证码检测。6. 加一个限速与熔断抢票脚本才算可维护抢票脚本不是一次性跑完就结束尤其是演唱会开票前需要提前半小时启动期间可能遇到各种异常。我一般会加三样东西请求频率限制、连续失败熔断、结构化日志。它们不增加抢票成功率但能让你在抢票失败后知道问题出在哪一步。先看一个简单限速装饰器它会让被装饰的函数每隔min_interval秒才真正执行一次。这个限速是进程级别的多个线程并发调用时依然生效适合用在对同一 Session 的轮流轮询上。import functools import time def rate_limit(min_interval): last_call [0.0] def decorator(fn): functools.wraps(fn) def wrapper(*args, **kwargs): delta time.time() - last_call[0] if delta min_interval: time.sleep(min_interval - delta) result fn(*args, **kwargs) last_call[0] time.time() return result return wrapper return decoratormin_interval传 0.5 就是说同一个 session 每秒最多调用两次last_call用列表是为了在闭包中可写。再配一个熔断装饰器记录连续异常次数超过阈值就抛出 SystemExit避免脚本在风控期内继续撞墙。import functools import logging def circuit_breaker(max_fail5): failures [0] def decorator(fn): functools.wraps(fn) def wrapper(*args, **kwargs): try: result fn(*args, **kwargs) failures[0] 0 return result except Exception: failures[0] 1 if failures[0] max_fail: logging.error(连续 %s 次失败熔断退出, max_fail) raise SystemExit(1) raise return wrapper return decorator日志建议输出到文件而不是终端因为抢票过程可能持续很久终端滚动会覆盖关键信息。用 Python 的logging.handlers.RotatingFileHandler按大小轮转比如单个文件不超过 5MB保留三个备份。每轮循环打印时间戳、当前状态、接口响应耗时失败时额外打印异常堆栈。最后一个技巧是给整个脚本加一个“总运行时长”上限比如 30 分钟。到时间无论是否抢到都停止避免因网络重试一直占用服务器资源。实现起来只需要记录开始时间在主循环里检查time.time() - start_time max_runtime就退出。退出前把这份运行数据写到同一份日志文件配合环境变量切换限速间隔和熔断阈值脚本才能在不同网络环境下稳定复现。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联 返回资讯列表 →