简介:这是一份面向Python爬虫与自动化爱好者的「大麦网自动抢票工具」源码包,针对热门演出票务紧张、手动抢票效率低的问题,提供了一套可运行、可二次开发的自动化购票实现方案,适合具备一定Python基础、希望学习浏览器自动化与并发请求的开发者参考。压缩包共6个文件,以1个py主程序、1个json配置文件、1个md说明文档和3张png示意图为主,整体约24KB,结构精简,便于快速定位核心逻辑。目前已有32995人学习下载,热度较高。通过阅读源码,读者可以了解Selenium等浏览器自动化工具如何模拟点击、输入与提交订单,理解多线程与异步处理在提升抢票效率中的作用,并接触验证码识别、IP代理池、异常重试等常见工程思路;配置文件则展示了账号、演出偏好等参数的存储方式,为改造或自建类似工具提供实践基础。
1. 抢票这件事,手速真不如脚本稳
去年帮朋友抢一场演出票,开票前五分钟我就把手机架好了,倒计时归零的瞬间猛戳屏幕,结果页面卡了半秒,再刷新已经全是“缺货登记”。后来我换了个思路,用 Python 写了个自动抢票工具,把“点击—提交—确认”这条链路交给程序去跑,成功率肉眼可见地往上走。这个项目就是围绕大麦网这类票务平台的抢票场景做的自动化工具,核心是用 Selenium 驱动浏览器完成登录、选场次、选票档、提交订单这一整套动作,再配合定时刷新和异常重试来对抗开票瞬间的高并发。它适合两类人:一类是想拿它直接抢票的普通用户,另一类是想学 Web 自动化实战、拿它当练手项目的开发者。下面我按“能干什么—怎么跑—怎么改”的顺序,把这份源码拆开讲清楚。
2. 项目定位与技术选型:为什么是 Selenium 而不是纯 requests
2.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?id=xxxxx" # 演出详情页 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(headless=False): options = Options() # 去掉自动化特征 options.add_argument("--disable-blink-features=AutomationControlled") options.add_experimental_option("excludeSwitches", ["enable-automation"]) options.add_experimental_option("useAutomationExtension", False) if headless: options.add_argument("--headless=new") options.add_argument("--window-size=1920,1080") options.add_argument("--user-agent=Mozilla/5.0 ...") service = Service(ChromeDriverManager().install()) driver = webdriver.Chrome(service=service, options=options) # 注入脚本,抹掉 webdriver 标记 driver.execute_cdp_cmd( "Page.addScriptToEvaluateOnNewDocument", {"source": "Object.defineProperty(navigator, 'webdriver', {get: () => undefined})"} ) return driver逻辑说明:--disable-blink-features=AutomationControlled关掉 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, timeout=120): 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"第 {attempt+1} 次尝试提交成功") 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_seconds=2): 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(headless=True) 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 独立持有driver,finally里确保退出,避免进程残留。线程数别开太多,三到五个比较合适,再多容易触发平台风控。
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,再也没翻过车。希望这个工具和这些改造思路能帮到你,抢票顺利。
本文还有配套的精品资源,点击获取