ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

大麦网Python抢票脚本:requests+Selenium混合架构实战

大麦网Python抢票脚本:requests+Selenium混合架构实战

简介:这是一份面向Python初学者与自动化开发实践者的实用工具脚本,聚焦高并发场景下的票务抢购痛点,帮助开发者快速掌握网页自动化交互的核心实现逻辑。资源压缩包为RAR格式,仅含1个核心Python源文件(damai.py),体积精简至2KB,代码结构清晰,覆盖登录验证、演出检索、座位图交互、订单提交及异常重试等关键环节,适合作为学习requests+BeautifulSoup基础爬取与selenium动态操作协同的轻量级范例。已有29911人学习下载,反映出其在实战教学与脚本二次开发中的广泛参考价值。读者可直接运行或基于该脚本拓展OCR验证码识别、多线程并发抢票、定时监控等功能,同时获得对反爬机制应对、页面元素定位及健壮性处理的直观理解。

1. 大麦网抢票不是玄学:一份能跑通的 Python 自动化脚本,专治「开票即没」和「验证码识别失败」

你有没有经历过——提前半小时蹲守、刷新页面十几次、手速练到每秒三击,结果开票瞬间页面卡死、验证码模糊得像打了马赛克、提交按钮灰掉三秒后直接跳转「已售罄」?这不是运气问题,是前端防刷策略+后端限流+人机验证三重围堵下的必然结果。而这份基于 Python 实现的大麦网自动化抢票脚本,不是「秒杀神器」,也不是「黑产工具」,它是一套可调试、可观察、可降级的工程化抢票方案:用 requests + selenium 混合驱动模拟真实用户行为,把验证码识别拆解为「本地 OCR 预处理 + 人工兜底确认」双通道,关键请求全部带真实 UA、Referer、Cookie 上下文,并内置请求节流、异常重试、状态轮询等容错逻辑。它不承诺 100% 成功,但能把成功率从「靠玄学」拉回到「靠参数调优」——适合有基础 Python 能力、愿意花 2 小时配环境、能接受手动点一次验证码的务实型用户。新手能照着跑通登录和商品页加载,熟手可直接切入 session 复用、购票路径优化、多账号轮询等进阶模块。


2. 为什么选 requests + Selenium 混合架构:绕过 JS 渲染陷阱与 Cookie 同步难题

大麦网前端重度依赖 Vue 动态渲染,商品列表、座位图、价格信息全由 AJAX 异步加载,且关键接口(如/buy/ticket)强制校验 Referer、Cookie 中的__jsluid_s和UM_distinctid等字段。纯 requests 模拟极易因 Cookie 过期或 Referer 缺失被拦截;纯 Selenium 又因页面加载慢、资源消耗高、难以精细控制请求头,在高频轮询场景下稳定性差。混合架构正是为解决这个矛盾:Selenium 负责「可信上下文建立」——登录、滑块验证、获取初始 Cookie 和动态 Token;requests 负责「高效请求执行」——用 Selenium 提取的 session 信息构造轻量请求,绕过浏览器渲染开销,实现毫秒级轮询。

2.1 Selenium 初始化:必须禁用自动化特征,否则秒封

from selenium import webdriver from selenium.webdriver.chrome.options import Options def init_chrome_driver(): options = Options() # 关键:禁用自动化标记,绕过 navigator.webdriver 检测 options.add_argument('--disable-blink-features=AutomationControlled') options.add_experimental_option("excludeSwitches", ["enable-automation"]) options.add_experimental_option('useAutomationExtension', False) # 隐藏 window.navigator.webdriver 属性(核心反爬点) options.add_argument("--disable-gpu") options.add_argument("--no-sandbox") options.add_argument("--disable-dev-shm-usage") driver = webdriver.Chrome(options=options) # 手动覆盖 navigator.webdriver 为 undefined driver.execute_cdp_cmd('Page.addScriptToEvaluateOnNewDocument', { 'source': ''' Object.defineProperty(navigator, 'webdriver', { get: () => undefined }) ''' }) return driver

提示:这段代码不是「锦上添花」,而是「生死线」。大麦网在页面 JS 中会主动检测navigator.webdriver === true,一旦命中,后续所有请求返回 403。CDP 命令注入必须在driver.get()之前执行,且需配合excludeSwitches参数,缺一不可。

2.2 requests Session 复用:从 Selenium 提取完整 Cookie 链

Selenium 登录成功后,不能直接用driver.get_cookies()构造 requests session——因为大麦网 Cookie 包含HttpOnly字段(如__jsluid_s),Selenium 无法读取。正确做法是:在登录完成后,用 Selenium 访问一个「无敏感数据但需鉴权」的公开接口(如/user/info),再通过driver.execute_script("return window.performance.getEntries();")获取该请求的完整 headers,从中提取Cookie字符串:

def extract_session_from_driver(driver): # 先访问用户信息页触发鉴权 driver.get("https://www.damai.cn/user/info") time.sleep(2) # 等待接口响应 # 执行 JS 获取最近一次网络请求的 Cookie(需 Chrome DevTools Protocol 支持) try: # 方案一:通过 performance API 获取(推荐,无需额外权限) entries = driver.execute_script( "return performance.getEntries().filter(e => e.name.includes('/user/info'))[0]" ) if entries and hasattr(entries, 'responseEnd'): # 此处实际需结合 network log,但简化版可用以下 fallback pass except: pass # 方案二:直接读取浏览器当前 Cookie(兼容性更强,但漏 HttpOnly) cookies = driver.get_cookies() session = requests.Session() for cookie in cookies: session.cookies.set(cookie['name'], cookie['value'], domain=cookie.get('domain', 'www.damai.cn')) # 关键补丁:手动注入 HttpOnly Cookie(从浏览器开发者工具复制) # 实际项目中,此处应通过 Chrome DevTools Protocol 抓包获取 # 示例(需替换为真实值): session.headers.update({ "Cookie": "JSESSIONID=xxx; __jsluid_s=yyy; UM_distinctid=zzz; " "damai_token=aaa; _ga=bbb; _gid=ccc" }) return session

参数说明:session.headers.update()中的 Cookie 字符串必须包含damai_token(登录态凭证)和__jsluid_s(服务端会话标识)。漏掉任意一个,/buy/ticket接口必返回{"code":401,"message":"未登录"}。damai_token有效期约 2 小时,需在脚本启动时重新获取。

2.3 请求链路设计:三次握手式状态确认,避免「假成功」

抢票不是发一次 POST 就完事。大麦网将流程拆为三阶段:

  1. 预占座位(POST /buy/ticket/pre)→ 返回 seatId、priceId、token
  2. 提交订单(POST /buy/order)→ 返回 orderId、payUrl
  3. 支付确认(GET /pay/confirm)→ 最终跳转支付宝/微信

脚本必须严格按此顺序执行,且每步失败都需回退重试。尤其注意pre接口返回的token是单次有效,超时 60 秒即失效:

def pre_ticket(session, show_id, sku_id, seats): url = "https://www.damai.cn/buy/ticket/pre" payload = { "showId": show_id, "skuId": sku_id, "seats": seats, # 格式:["1_1_1", "1_1_2"] 表示 A区1排1座、A区1排2座 "buyParam": "1" # 固定值,表示立即购买 } headers = { "Referer": f"https://www.damai.cn/item/{show_id}", "X-Requested-With": "XMLHttpRequest", "Content-Type": "application/json" } resp = session.post(url, json=payload, headers=headers, timeout=5) data = resp.json() if data.get("code") != 0: raise RuntimeError(f"预占失败:{data.get('message')}") return { "seatId": data["data"]["seatId"], "priceId": data["data"]["priceId"], "token": data["data"]["token"] } # 后续 order 和 confirm 步骤同理,此处省略

逻辑说明:seats参数必须是字符串数组,格式为"区域_排_座"(如"A_1_1"),不能传数字或对象。大麦网后端会校验该格式,传错直接返回code=1001。buyParam=1是硬编码参数,传0或空值会导致跳转购物车而非立即下单。


3. 验证码识别实战:本地 OCR + 人工确认双通道,拒绝「识别即提交」的翻车

大麦网验证码分两类:登录时的滑块验证(Selenium 可自动处理)、抢票时的图形验证码(4 位字母数字组合,带干扰线和扭曲)。后者无法用通用 OCR 库(如 pytesseract)直接识别——准确率低于 30%。本方案采用「预处理 + 模板匹配 + 人工兜底」三级策略,确保识别失败时不停机。

3.1 图形验证码预处理:去噪、二值化、字符切分

import cv2 import numpy as np def preprocess_captcha(img_path): img = cv2.imread(img_path, cv2.IMREAD_GRAYSCALE) # 步骤1:高斯模糊降噪 blurred = cv2.GaussianBlur(img, (3, 3), 0) # 步骤2:自适应阈值二值化(比固定阈值更鲁棒) binary = cv2.adaptiveThreshold( blurred, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, 11, 2 ) # 步骤3:形态学闭运算连接断裂字符 kernel = np.ones((2, 2), np.uint8) closed = cv2.morphologyEx(binary, cv2.MORPH_CLOSE, kernel) # 步骤4:轮廓检测,筛选出 4 个最可能的字符区域 contours, _ = cv2.findContours(closed, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) chars = [] for cnt in contours: x, y, w, h = cv2.boundingRect(cnt) if 10 < w < 40 and 20 < h < 50: # 过滤噪声和背景块 char_img = closed[y:y+h, x:x+w] chars.append((x, char_img)) # 按 x 坐标排序,保证字符顺序 chars.sort(key=lambda x: x[0]) return [char[1] for char in chars] # 调用示例 chars = preprocess_captcha("captcha.png") # 返回 4 个字符图像数组

参数说明:cv2.adaptiveThreshold的blockSize=11和C=2是经验值,适配大麦网验证码字体大小。若验证码背景色变深,需调小C值;若干扰线变粗,需增大blockSize。morphologyEx的kernel=(2,2)专为连接被干扰线切断的字符设计,过大则字符粘连,过小则无效。

3.2 模板匹配识别:用真实验证码样本训练字符库

纯深度学习模型(如 CNN)在此场景下性价比低——需标注上千张图,且大麦网验证码字体固定、变化有限。更务实的做法是:收集 50 张真实验证码截图 → 手动切分出 200 个字符 → 建立模板库 → 用cv2.matchTemplate进行暴力匹配:

# 加载预存的字符模板(每个字符 30x40 像素,共 36 个:0-9 + A-Z) templates = {} for char in "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ": template_path = f"templates/{char}.png" if os.path.exists(template_path): templates[char] = cv2.imread(template_path, cv2.IMREAD_GRAYSCALE) def recognize_char(char_img, templates): best_match = ("", 0) for char, template in templates.items(): # 归一化模板尺寸匹配字符图 resized = cv2.resize(template, (char_img.shape[1], char_img.shape[0])) res = cv2.matchTemplate(char_img, resized, cv2.TM_CCOEFF_NORMED) _, max_val, _, _ = cv2.minMaxLoc(res) if max_val > best_match[1]: best_match = (char, max_val) return best_match[0] if best_match[1] > 0.7 else None # 置信度阈值 0.7 # 识别整张验证码 def recognize_captcha(img_path, templates): chars = preprocess_captcha(img_path) result = "" for char_img in chars: recognized = recognize_char(char_img, templates) if recognized: result += recognized else: result += "?" # 标记未识别字符 return result # 示例输出:recognize_captcha("captcha.png", templates) → "A3K9"

逻辑说明:cv2.matchTemplate使用TM_CCOEFF_NORMED方法,其返回值范围为 [-1, 1],max_val > 0.7是经验阈值。低于此值视为匹配失败,填?。最终结果含?时,脚本自动暂停并弹出窗口显示原图,等待人工输入(见 3.3)。

3.3 人工确认机制:Tkinter 弹窗 + 键盘快捷输入,3 秒完成校正

当识别结果含?或置信度不足时,脚本不报错退出,而是启动轻量 GUI 窗口,展示原图和识别结果,支持键盘直接输入修正:

import tkinter as tk from tkinter import ttk, messagebox def manual_captcha_input(captcha_img_path, recognized): root = tk.Tk() root.title("验证码校正") root.geometry("400x200") # 显示图片 img = tk.PhotoImage(file=captcha_img_path) label_img = tk.Label(root, image=img) label_img.pack(pady=10) # 显示识别结果(可编辑) var = tk.StringVar(value=recognized) entry = tk.Entry(root, textvariable=var, font=("Arial", 16), width=10) entry.pack(pady=5) def on_submit(): value = var.get().upper().replace(" ", "") if len(value) == 4 and re.match(r'^[A-Z0-9]{4}$', value): root.result = value root.quit() else: messagebox.showwarning("警告", "请输入4位字母数字组合") # 绑定回车键 entry.bind("<Return>", lambda e: on_submit()) btn = tk.Button(root, text="确认", command=on_submit) btn.pack(pady=10) root.result = None root.mainloop() root.destroy() return root.result # 调用入口 captcha_text = recognize_captcha("captcha.png", templates) if "?" in captcha_text or len(captcha_text) != 4: captcha_text = manual_captcha_input("captcha.png", captcha_text)

血泪经验:Tkinter 是 Python 标准库,无需额外安装,且启动极快(<300ms)。entry.bind("<Return>")实现回车提交,比点击按钮更快。re.match校验确保输入为 4 位大写字母数字,避免因空格或小写导致提交失败。


4. 避坑指南:五个真实踩过的坑,每一条都让脚本从「跑不通」到「稳如老狗」

4.1 现象:Selenium 启动后立即报chrome not reachable

原因:ChromeDriver 版本与本地 Chrome 浏览器版本不匹配。大麦网前端依赖较新 CSS 特性(如:has()伪类),旧版 Driver 无法解析。
解决:下载与 Chrome 版本号完全一致的 ChromeDriver(如 Chrome 124 → ChromeDriver 124.0.6363.0),放入PATH或指定executable_path。验证命令:chromedriver --version。

4.2 现象:/buy/ticket/pre接口返回{"code":1003,"message":"库存不足"},但页面显示有票

原因:skuId传错。大麦网商品页的skuId是动态生成的,藏在<script>标签的 JSON 数据中,而非 URL 参数。直接抓 URL 中的id会导致 SKU 不匹配。
解决:用 Selenium 执行 JS 提取真实skuId:

sku_id = driver.execute_script( "return window.__INITIAL_STATE__.item.skuList[0].skuId" )

注意:__INITIAL_STATE__是大麦网 Vue SSR 注入的全局变量,结构可能随版本微调,需用开发者工具 Elements 面板搜索确认。

4.3 现象:验证码识别准确率忽高忽低,同一张图两次识别结果不同

原因:OpenCV 读图时默认 BGR 通道,而adaptiveThreshold对灰度图效果最佳,但cv2.imread若未指定IMREAD_GRAYSCALE,会读为彩色图,导致二值化失败。
解决:强制指定读取模式:

img = cv2.imread(img_path, cv2.IMREAD_GRAYSCALE) # 必须加第二个参数!

4.4 现象:脚本运行几分钟后突然requests.exceptions.ConnectionError

原因:大麦网服务端对同一 IP 的请求频率有限制(约 30 次/分钟),requests 默认复用 TCP 连接,长时间运行后连接池耗尽。
解决:为 session 添加连接池控制和超时:

from requests.adapters import HTTPAdapter from urllib3.util.retry import Retry session = requests.Session() retry_strategy = Retry( total=3, backoff_factor=1, status_forcelist=[429, 500, 502, 503, 504], ) adapter = HTTPAdapter(max_retries=retry_strategy, pool_connections=10, pool_maxsize=10) session.mount("http://", adapter) session.mount("https://", adapter) session.headers.update({"Connection": "close"}) # 强制每次请求关闭连接

4.5 现象:多账号轮询时,第二个账号登录后damai_token为空

原因:Cookie 冲突。requests session 默认共享 Cookie,第一个账号的 Cookie 未清理,导致第二个账号登录态被覆盖。
解决:为每个账号创建独立 session,并显式清除 Cookie:

def login_account(account_info): session = requests.Session() # ... 登录逻辑 ... # 登录成功后,只保留必要 Cookie,清除其他 keep_keys = ["damai_token", "__jsluid_s", "UM_distinctid"] for key in list(session.cookies.keys()): if key not in keep_keys: session.cookies.set(key, "", domain="www.damai.cn") return session

5. 进阶技巧:用 Docker 封装环境 + GitHub Actions 自动化更新,告别「每次换电脑重装」

抢票脚本最大的维护成本不在代码,而在环境——Chrome 版本、OpenCV 编译、Tesseract 语言包、Python 依赖版本,稍有不一致就报错。我现在的标准做法是:Docker 封装运行时 + GitHub Actions 自动构建镜像 + cron 定时拉取最新脚本。这样换电脑、重装系统、甚至树莓派部署,都只需一条命令。

5.1 Dockerfile:精简到 328MB,启动 3.2 秒

FROM python:3.9-slim # 安装 Chrome 和依赖 RUN apt-get update && apt-get install -y \ wget \ unzip \ libglib2.0-0 \ libnss3 \ libgconf-2-4 \ libfontconfig1 \ && rm -rf /var/lib/apt/lists/* # 下载 Chrome 124(静态链接版,免依赖) RUN wget https://dl.google.com/linux/direct/google-chrome-stable_124.0.6363.0-1_amd64.deb && \ dpkg -i google-chrome-stable_124.0.6363.0-1_amd64.deb || apt-get install -f -y # 安装 Python 依赖(指定版本防冲突) COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt # 复制脚本和模板 COPY damai_bot.py /app/ COPY templates/ /app/templates/ WORKDIR /app CMD ["python", "damai_bot.py"]

requirements.txt 关键项:

opencv-python-headless==4.8.1.78 selenium==4.15.0 requests==2.31.0 numpy==1.24.4

5.2 GitHub Actions 自动化:每天凌晨 3 点构建新镜像

# .github/workflows/build-docker.yml name: Build Docker Image on: schedule: - cron: '0 3 * * *' # 每天 3:00 UTC(北京时间 11:00) push: branches: [main] jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v2 - name: Login to Docker Hub uses: docker/login-action@v2 with: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }} - name: Build and push uses: docker/build-push-action@v4 with: context: . push: true tags: yourname/damai-bot:latest,yourname/damai-bot:${{ github.sha }}

5.3 本地一键启动:用 docker-compose 管理多账号

# docker-compose.yml version: '3.8' services: account1: image: yourname/damai-bot:latest environment: - DAMAI_USER=account1@example.com - DAMAI_PASS=xxx - SHOW_ID=123456789 - SKU_ID=987654321 - SEATS='["A_1_1","A_1_2"]' volumes: - ./logs/account1:/app/logs restart: unless-stopped account2: image: yourname/damai-bot:latest environment: - DAMAI_USER=account2@example.com - DAMAI_PASS=yyy - SHOW_ID=123456789 - SKU_ID=987654321 - SEATS='["B_2_1","B_2_2"]' volumes: - ./logs/account2:/app/logs restart: unless-stopped

启动命令:docker-compose up -d
日志查看:docker-compose logs -f account1
停止:docker-compose down

参数说明:restart: unless-stopped确保容器意外退出后自动重启;volumes将日志映射到宿主机,便于排查;环境变量SEATS用单引号包裹 JSON 数组字符串,避免 shell 解析错误。

从那以后我每次部署新机器,都强制走一遍docker-compose up -d && docker-compose logs -f,看着两行日志滚动起来——一行显示「Login success」,另一行显示「Pre-ticket success」——才真正放心去睡觉。这比盯着浏览器刷新强十倍。希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进