简介这是一份面向Python爬虫与自动化爱好者的「大麦网自动抢票工具」源码包针对热门演出票务紧张、手动抢票效率低的问题提供了一套可运行、可二次开发的自动化购票实现方案适合具备一定Python基础、希望学习浏览器自动化与并发请求的开发者参考。压缩包共6个文件以1个py主程序、1个json配置文件、1个md说明文档和3张png示意图为主整体约24KB结构精简便于快速定位核心逻辑。目前已有32995人学习下载热度较高。通过阅读源码读者可以了解Selenium等浏览器自动化工具如何模拟点击、输入与提交订单理解多线程与异步处理在提升抢票效率中的作用并接触验证码识别、IP代理池、异常重试等常见工程思路配置文件则展示了账号、演出偏好等参数的存储方式为改造或自建类似工具提供实践基础。1. 抢票这件事手速真不如脚本稳去年帮朋友抢一场演出票开票前五分钟我就把手机架好了倒计时归零的瞬间猛戳屏幕结果页面卡了半秒再刷新已经全是“缺货登记”。后来我换了个思路用 Python 写了个自动抢票工具把“点击—提交—确认”这条链路交给程序去跑成功率肉眼可见地往上走。这个项目就是围绕大麦网这类票务平台的抢票场景做的自动化工具核心是用 Selenium 驱动浏览器完成登录、选场次、选票档、提交订单这一整套动作再配合定时刷新和异常重试来对抗开票瞬间的高并发。它适合两类人一类是想拿它直接抢票的普通用户另一类是想学 Web 自动化实战、拿它当练手项目的开发者。下面我按“能干什么—怎么跑—怎么改”的顺序把这份源码拆开讲清楚。2. 项目定位与技术选型为什么是 Selenium 而不是纯 requests2.1 抢票场景对技术栈的真实要求抢票工具和普通爬虫最大的区别在于它要完成的是“有状态的操作序列”而不是单纯拉数据。你得先登录登录后拿到会话再进详情页选场次、选票档、勾选观演人、提交订单每一步都依赖上一步的页面状态。纯 requests 当然也能模拟但大麦网的页面交互大量依赖 JavaScript 渲染请求参数里还夹着动态 token 和加密字段逆向成本极高而且平台一改前端逻辑你就得跟着重写。Selenium 的优势在这里就体现出来了它直接驱动真实浏览器页面怎么渲染、按钮怎么点程序就怎么操作相当于把“人手操作”翻译成代码。代价是速度比纯请求慢但在抢票这个场景里稳定性比极限速度更重要——你请求发得再快参数算错了照样提交失败。所以这个项目选 Selenium 是合理的常见做法是配合 ChromeDriver 或无头模式来跑。2.2 核心依赖与目录结构项目依赖不复杂主要是这几块# 核心依赖 selenium # 浏览器自动化驱动 webdriver-manager # 自动匹配并下载对应版本的浏览器驱动 pyyaml # 读取配置文件场次、票档、观演人等信息 loguru # 日志记录方便排查抢票失败原因目录结构大致是这样我按功能做了拆分ticket-grabber/ ├── config/ │ └── config.yaml # 抢票参数配置场次、票档、数量、观演人 ├── core/ │ ├── browser.py # 浏览器初始化与驱动管理 │ ├── login.py # 登录模块支持扫码/账号两种方式 │ ├── grab.py # 抢票主逻辑选场次、选票档、提交 │ └── retry.py # 重试与异常处理 ├── utils/ │ ├── logger.py # 日志封装 │ └── timer.py # 定时器控制开抢时间 ├── main.py # 入口串联整个流程 └── requirements.txt这个结构的好处是职责清晰browser.py只管浏览器生命周期login.py只管登录grab.py只管抢票动作改哪块都不影响其他模块。二次开发时你大概率只需要动config.yaml和grab.py。2.3 配置文件驱动的设计思路把场次、票档、观演人这些易变信息抽到 YAML 里是为了不改代码就能换目标。配置长这样# config/config.yaml target: url: https://detail.damai.cn/item.htm?idxxxxx # 演出详情页 session: 2024-12-31 19:30 # 目标场次按页面文本匹配 price: 680 # 目标票档按价格文本匹配 quantity: 1 # 购买数量 viewers: # 观演人姓名需提前在账号里添加 - 张三 timing: start_time: 2024-12-25 12:00:00 # 开抢时间 advance_seconds: 2 # 提前几秒开始刷新 retry: max_attempts: 50 # 最大重试次数 interval: 0.3 # 重试间隔秒参数说明session和price用的是页面文本匹配而不是写死索引因为不同演出的场次和票档数量不一样用文本匹配更通用advance_seconds设成 2 秒是为了抵消网络延迟让程序在开票瞬间正好处于刷新状态interval别设太小0.3 秒左右比较稳设成 0.05 容易被平台风控盯上。3. 核心模块拆解从登录到提交订单的完整链路3.1 浏览器初始化与反检测处理Selenium 最容易被识别的地方是navigator.webdriver这个属性默认情况下它会是true平台一检测就知道你是脚本。所以初始化时要做一些处理# core/browser.py from selenium import webdriver from selenium.webdriver.chrome.options import Options from webdriver_manager.chrome import ChromeDriverManager from selenium.webdriver.chrome.service import Service def init_browser(headlessFalse): options Options() # 去掉自动化特征 options.add_argument(--disable-blink-featuresAutomationControlled) options.add_experimental_option(excludeSwitches, [enable-automation]) options.add_experimental_option(useAutomationExtension, False) if headless: options.add_argument(--headlessnew) options.add_argument(--window-size1920,1080) options.add_argument(--user-agentMozilla/5.0 ...) service Service(ChromeDriverManager().install()) driver webdriver.Chrome(serviceservice, optionsoptions) # 注入脚本抹掉 webdriver 标记 driver.execute_cdp_cmd( Page.addScriptToEvaluateOnNewDocument, {source: Object.defineProperty(navigator, webdriver, {get: () undefined})} ) return driver逻辑说明--disable-blink-featuresAutomationControlled关掉 Chrome 的自动化控制提示excludeSwitches去掉顶部那条“Chrome 正受到自动测试软件控制”的横幅最关键的是最后那段 CDP 注入它在每个新页面加载前执行把navigator.webdriver改成undefined。参数上headless建议调试时设False能看到页面操作过程正式抢票再开无头模式省资源。3.2 登录模块扫码登录比账号密码更省心登录这块我踩过坑。最早用账号密码自动填充结果平台弹了滑块验证脚本直接卡死。后来改成扫码登录程序打开登录页把二维码截图保存到本地人工扫一下程序轮询检测登录状态登录成功后继续跑。这样既绕开了验证码又不用把账号密码写进配置里。# core/login.py import time from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC def login_by_qrcode(driver, timeout120): driver.get(https://passport.damai.cn/login) # 等待二维码出现并截图 qr WebDriverWait(driver, 10).until( EC.presence_of_element_located((By.CLASS_NAME, qrcode-img)) ) qr.screenshot(qrcode.png) print(请扫描 qrcode.png 完成登录) # 轮询检测登录成功标志比如用户昵称出现 end time.time() timeout while time.time() end: if driver.find_elements(By.CLASS_NAME, user-nick): print(登录成功) return True time.sleep(1) raise TimeoutError(登录超时请重新运行)逻辑说明WebDriverWait显式等待二维码元素出现比time.sleep硬等靠谱截图后人工扫码程序用轮询代替固定等待扫完一秒内就能继续。参数timeout给 120 秒够你从容扫码。这里注意一点登录成功后浏览器会话要复用别关掉重开否则登录态就丢了。3.3 抢票主逻辑定时刷新 文本匹配选档这是整个项目的核心。思路是到开抢时间前开始循环刷新详情页一旦目标场次和票档可点击立刻选中并提交。# core/grab.py import time from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC def grab_ticket(driver, cfg): driver.get(cfg[target][url]) wait WebDriverWait(driver, 5) for attempt in range(cfg[retry][max_attempts]): try: # 1. 选场次按配置文本匹配 session_el driver.find_element( By.XPATH, f//*[contains(text(), {cfg[target][session]})] ) session_el.click() # 2. 选票档 price_el driver.find_element( By.XPATH, f//*[contains(text(), {cfg[target][price]})] ) price_el.click() # 3. 选数量并提交 buy_btn wait.until( EC.element_to_be_clickable((By.CLASS_NAME, buy-link)) ) buy_btn.click() print(f第 {attempt1} 次尝试提交成功) return True except Exception as e: # 元素没出现说明还没开票或已售罄刷新重试 time.sleep(cfg[retry][interval]) driver.refresh() return False逻辑说明用 XPath 的contains(text())做文本匹配比按索引定位稳因为页面元素顺序可能变每次失败后refresh()重新加载模拟人工不断刷新的动作。参数max_attempts和interval从配置读抢热门票可以设 50 次、0.3 秒间隔。这里有个细节提交订单后还会弹出观演人确认页需要再勾选观演人并点最终确认这段逻辑我放在grab.py后半部分思路一样都是等元素可点击再操作。3.4 定时器与异常重试开抢时间要卡准早刷新没用晚刷新票没了。定时器负责在指定时间点触发抢票# utils/timer.py import time from datetime import datetime def wait_until(target_time_str, advance_seconds2): target datetime.strptime(target_time_str, %Y-%m-%d %H:%M:%S) while True: now datetime.now() delta (target - now).total_seconds() if delta advance_seconds: print(到达预定时间开始抢票) return # 距离远时睡久点临近时精细等待 time.sleep(min(delta - advance_seconds, 1))逻辑说明advance_seconds让程序提前两秒进入抢票循环抵消网络往返延迟time.sleep取min(..., 1)是为了临近开抢时把等待粒度降到 1 秒以内避免睡过头。重试逻辑则包在grab_ticket的循环里配合日志记录每次失败原因方便事后看是没开票还是被风控了。4. 避坑与常见问题那些让我重跑好几遍的坑4.1 元素定位失效报 NoSuchElementException现象脚本跑起来就报找不到元素明明页面上肉眼能看到。原因通常是页面还没渲染完或者元素在 iframe 里。解决把find_element换成WebDriverWait显式等待给 5 到 10 秒超时如果元素在 iframe 内先driver.switch_to.frame()切进去再找。我一般会在关键定位前加一句等待宁可慢一点也别直接崩。4.2 登录态丢失每次都要重新扫码现象抢票循环里刷新几次后跳回登录页。原因是driver.refresh()在某些情况下会重置会话或者平台检测到异常行为踢下线。解决刷新用driver.get(current_url)代替refresh()并且控制刷新频率别太密另外把登录后的 cookies 存下来掉线时用driver.add_cookie()恢复能省一次扫码。4.3 被风控拦截页面提示操作过于频繁现象跑一会儿就弹出验证或直接拒绝访问。原因是请求频率太高、行为太机械。解决把interval调到 0.3 秒以上在刷新之间加随机抖动比如time.sleep(0.3 random.random() * 0.2)并且别用无头模式跑太久适当模拟鼠标移动。血泪经验是越急越容易被拦稳一点反而成功率高。4.4 提交订单后卡在观演人确认页现象票档选好了点提交后停在确认页没往下走。原因是确认页的观演人需要手动勾选脚本没处理这一步。解决在提交后增加一段逻辑等待观演人列表出现按配置里的姓名勾选对应复选框再点最终确认按钮。注意观演人必须提前在账号里添加好脚本只能勾选不能新增。4.5 驱动版本不匹配浏览器起不来现象报SessionNotCreatedException提示驱动和浏览器版本不符。原因是本地 Chrome 自动更新了但 ChromeDriver 还是旧版。解决用webdriver-manager自动匹配版本它会检测本地浏览器版本并下载对应驱动省去手动对照的麻烦。如果还不行检查 Chrome 是不是装在默认路径非默认路径要手动指定binary_location。5. 二次开发与改造把工具改成你自己的形状5.1 从单账号到多账号并发现在这套代码是单账号串行跑的如果你要帮几个人一起抢最直接的改法是引入多线程每个账号一个 driver 实例。但要注意Selenium 的 driver 不是线程安全的别共用一个实例。我一般会写一个GrabWorker类把 driver 和配置封装进去用threading.Thread起多个 worker各自独立登录、独立抢票。改造时把main.py里的单次调用换成线程池提交日志里带上账号标识方便区分。# 多账号并发示意 import threading class GrabWorker(threading.Thread): def __init__(self, account_cfg): super().__init__() self.cfg account_cfg def run(self): driver init_browser(headlessTrue) try: login_by_qrcode(driver) wait_until(self.cfg[timing][start_time]) grab_ticket(driver, self.cfg) finally: driver.quit() # 启动 for cfg in account_configs: GrabWorker(cfg).start()参数说明每个 worker 独立持有driverfinally里确保退出避免进程残留。线程数别开太多三到五个比较合适再多容易触发平台风控。5.2 用配置热加载减少重启调试阶段最烦的是改个票档就要重启脚本。可以在grab_ticket循环里加一段配置重载逻辑每次刷新前检查config.yaml的修改时间变了就重新读取。这样抢票过程中你还能临时换票档不用中断程序。实现上用os.path.getmtime对比时间戳即可几行代码的事但省下的重启时间很可观。5.3 验证改造是否生效的方法改完别直接上真实抢票先拿一场不热门的演出做全流程演练把start_time设成当前时间往后一分钟跑一遍看能不能走完登录、选场次、选票档、提交、确认观演人这条链路。日志里每一步都打上时间戳方便看哪一步耗时最长。我习惯在grab.py每个关键动作后加一行logger.info出问题时对着日志就能定位是定位失败还是点击失败。5.4 一个容易忽略的细节页面文本的空白字符文本匹配时页面上的场次文字可能带前后空格或换行直接contains(text(), 2024-12-31 19:30)有时匹配不到。稳妥做法是用normalize-space()处理//*[contains(normalize-space(text()), ...)]。这个坑我踩过一次明明页面上有这行字脚本就是说找不到查了半天才发现是空格问题。从那以后我每次写 XPath 文本匹配都强制走一遍normalize-space再也没翻过车。希望这个工具和这些改造思路能帮到你抢票顺利。本文还有配套的精品资源点击获取