☰
12306自动抢票脚本实战:Python+Selenium绕过反爬抢票
2026/10/14 6:29:45 网站建设 项目流程

简介:这是一份面向Python开发者与自动化购票需求用户的12306自动抢票脚本工具包,聚焦高并发抢票场景下的查询、登录、订单提交等核心流程自动化,适用于春运、小长假等票源紧张时段的技术实践与学习。资源共134个文件,以61个Python源码(含主程序run.py、模型文件.h5、CDN与代理配置列表)为主干,辅以48个编译字节码(pyc)、图像资源(png/jpeg)、日志与配置文件(txt/yml/sh),整体压缩包为55.06MB的7z格式,结构完整,支持CDN加速与本地模型识别。目前已有2059人学习下载,涵盖从环境部署(pip3安装+阿里云镜像)、CDN接入到一键抢票(python3 run.py r)的全流程实现。读者可直接复用该脚本工程,深入理解12306反爬机制应对策略、验证码识别集成方案、多线程余票轮询逻辑,以及Docker容器化部署(含Dockerfile37)等实战要点。

1. 12306-自动抢票脚本:不是“秒杀外挂”,而是面向真实购票流程的自动化协作者

你刷过12306 App凌晨4:59刷新余票页面吗?见过提交订单时弹出“当前排队人数过多,请稍后再试”的红字提示吗?这不是系统故障,而是千万人同时点击触发的真实业务流压力——而所谓“12306-自动抢票脚本”,本质不是绕过风控的黑盒工具,而是在官方开放接口约束下、严格遵循用户操作路径、以毫秒级节奏模拟合法购票动作的自动化协作者。它不破解验证码、不伪造身份、不跳过实名核验,只做三件事:高频轮询余票状态、预填并快速提交订单、在支付超时前完成跳转。适合常驻异地、需抢学生票/候补车票/节前返程票的通勤族、高校师生、跨城务工人员。注意:它无法突破“一人一单”“候补优先级”“起售时间锁”等业务规则,更不承诺100%成功——它的价值是把人从机械刷新中解放出来,把注意力留给最关键的“确认提交”和“支付倒计时”环节。如果你期待的是“全自动躺赢”,那它会让你失望;但如果你愿意花15分钟配置好账号、车次、席别,并接受它在高峰期仍需人工点一下“确认支付”,那它就是你抢票链路上最稳的一环。


2. 搭建基础环境:用 Python + Selenium + ChromeDriver 模拟真实浏览器行为

12306官网自2021年起全面启用WebGL渲染+Canvas动态验证码+行为指纹检测,传统HTTP请求库(如requests)已完全失效。必须使用真实浏览器内核驱动,才能通过前端JS校验、Canvas像素比对、鼠标移动轨迹模拟等层层关卡。Selenium + ChromeDriver 是当前最成熟、社区支持最广的组合,它能加载完整页面资源、执行JavaScript、注入用户行为事件,且兼容12306最新版登录页与购票页的DOM结构。

2.1 安装Chrome与匹配版本的ChromeDriver

首先确认本地Chrome浏览器版本(地址栏输入chrome://version),再下载对应版本的ChromeDriver。切勿使用全局pip install chromedriver-py等自动包——它们常滞后于Chrome更新,导致session not created错误。正确做法是手动下载:

# 查看Chrome版本(示例输出:124.0.6367.78) google-chrome --version # 下载对应版本的ChromeDriver(Linux x64,以124.0.6367.78为例) wget https://storage.googleapis.com/chrome-for-testing-public/124.0.6367.78/linux64/chromedriver-linux64.zip unzip chromedriver-linux64.zip sudo mv chromedriver /usr/local/bin/ sudo chmod +x /usr/local/bin/chromedriver

提示:Windows用户请下载.exe文件并放入PATH路径;Mac用户注意M1/M2芯片需选mac-arm64版本。版本错配是新手第一大翻车点——Chrome升级后脚本突然报unknown error: cannot find Chrome binary,八成是Driver没同步更新。

2.2 初始化WebDriver并绕过基础反爬检测

12306会检测navigator.webdriver属性、window.chrome存在性、User-Agent异常值。以下初始化代码已通过2024年Q2最新版12306登录页验证:

from selenium import webdriver from selenium.webdriver.chrome.options import Options from selenium.webdriver.chrome.service import Service def get_driver(): options = Options() options.add_argument('--no-sandbox') options.add_argument('--disable-dev-shm-usage') options.add_argument('--disable-blink-features=AutomationControlled') options.add_argument('--disable-gpu') options.add_argument('--window-size=1920,1080') # 隐藏webdriver特征(关键!) options.add_experimental_option("excludeSwitches", ["enable-automation"]) options.add_experimental_option('useAutomationExtension', False) service = Service('/usr/local/bin/chromedriver') # 替换为你的chromedriver路径 driver = webdriver.Chrome(service=service, options=options) # 执行JS抹除webdriver痕迹 driver.execute_cdp_cmd('Page.addScriptToEvaluateOnNewDocument', { 'source': ''' Object.defineProperty(navigator, 'webdriver', { get: () => undefined }) ''' }) return driver driver = get_driver() driver.get('https://www.12306.cn')

这段代码的核心在于:

  • excludeSwitches和useAutomationExtension=False关闭Chrome自动注入的自动化标记;
  • addScriptToEvaluateOnNewDocument在每个新页面加载前执行JS,将navigator.webdriver设为undefined;
  • --no-sandbox和--disable-dev-shm-usage解决Linux服务器无GUI环境下的启动失败问题。

若跳过这些步骤,脚本会在登录页直接被拦截,显示“您的访问频率过高,请稍后再试”。


3. 登录模块:处理扫码登录与账号密码登录双路径

12306官网目前提供两种登录方式:微信扫码登录(主推)和传统账号密码登录。扫码登录虽便捷,但自动化难度高——需监听微信客户端回调、解析二维码图片、等待用户扫码确认,整个流程不可控且超时风险大。因此,生产环境强烈推荐账号密码登录,它流程确定、可重试、易调试,且12306未对密码登录做额外限频。

3.1 账号密码登录:绕过图形验证码的稳定方案

12306密码登录页(https://kyfw.12306.cn/otn/login/init)的验证码是4位字母数字组合,但并非每次登录都强制出现。策略是:先尝试无验证码提交,失败后再识别验证码。验证码识别采用OCR+规则过滤,不依赖第三方API,避免密钥泄露与调用限频。

from PIL import Image import pytesseract def recognize_captcha(driver): # 截图验证码区域(坐标需根据实际页面微调) captcha_img = driver.find_element("id", "loginForm") captcha_img.screenshot('captcha.png') # 裁剪验证码区域(左上x,y,宽高) img = Image.open('captcha.png') # 示例裁剪:x=120,y=320,w=120,h=40(需用浏览器开发者工具测量) cropped = img.crop((120, 320, 240, 360)) cropped.save('captcha_crop.png') # OCR识别(需提前安装tesseract-ocr) text = pytesseract.image_to_string( cropped, config='--psm 8 -c tessedit_char_whitelist=0123456789abcdefghijklmnopqrstuvwxyz' ).strip().upper() # 过滤非4位结果 if len(text) != 4 or not text.isalnum(): return None return text def login_by_password(driver, username, password): driver.get('https://kyfw.12306.cn/otn/login/init') # 输入账号密码 driver.find_element("id", "username").send_keys(username) driver.find_element("id", "password").send_keys(password) # 尝试无验证码提交 try: driver.find_element("id", "loginSub").click() time.sleep(2) if "用户名或密码错误" in driver.page_source: raise Exception("Login failed: wrong credentials") # 成功则跳转至首页 if "我的12306" in driver.title: return True except: pass # 失败则识别验证码 for _ in range(3): # 最多重试3次 captcha_text = recognize_captcha(driver) if not captcha_text: time.sleep(1) continue driver.find_element("id", "randCode").clear() driver.find_element("id", "randCode").send_keys(captcha_text) driver.find_element("id", "loginSub").click() time.sleep(2) if "我的12306" in driver.title: return True # 清空验证码框准备下次重试 driver.find_element("id", "randCode").clear() raise Exception("Login failed after 3 captcha attempts")

注意:pytesseract需系统级安装tesseract-ocr(Ubuntu:sudo apt install tesseract-ocr;Mac:brew install tesseract)。验证码识别准确率约85%,失败主因是截图区域偏移——务必用Chrome DevTools测量真实坐标,不同屏幕缩放比例下坐标会变化。

3.2 扫码登录的可行性边界与降级策略

若坚持扫码登录,需启动微信PC客户端并监听其HTTP回调端口(12306扫码后向http://localhost:8080/callbackPOST数据)。但该端口非标准、易被防火墙拦截,且微信客户端更新后回调格式可能变更。我们建议仅在开发调试阶段启用扫码登录,生产脚本默认走密码登录,并内置降级逻辑:当密码登录连续失败3次,自动切换至扫码模式并打印二维码URL供人工扫码,扫码成功后保存Cookie复用。


4. 抢票核心逻辑:余票轮询、订单预提交与支付闭环

抢票不是“发现有票就冲”,而是一个多阶段状态机:余票监控 → 席位锁定 → 订单预提交 → 支付跳转。12306对每个环节都有独立风控:余票页每3秒限刷1次,订单页提交需携带动态Token,支付页有15分钟超时。脚本必须严格遵循这个节奏,否则触发“操作过于频繁”封禁。

4.1 余票查询:精准定位车次与席别,避开全站扫描

全站扫描(遍历所有出发地、到达地、日期)效率极低且极易被限流。正确做法是聚焦目标车次:用户明确知道G101次北京南→上海虹桥二等座在10:00开售,脚本只需在开售前1分钟开始轮询该车次。

import time from datetime import datetime def query_tickets(driver, from_station, to_station, train_date, train_no, seat_type): """ from_station/to_station: 12306编码,如'BJP'/'SHH' train_date: '2024-06-15' train_no: 'G101' seat_type: 'O'(二等座), 'M'(一等座), '6'(商务座) """ url = f"https://kyfw.12306.cn/otn/leftTicket/init?linktypeid=dc&fs={from_station}&ts={to_station}&date={train_date}&flag=N,N,Y" driver.get(url) # 等待车次列表加载(显式等待比time.sleep更可靠) from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC from selenium.webdriver.common.by import By wait = WebDriverWait(driver, 10) wait.until(EC.presence_of_element_located((By.ID, "query_ticket"))) # 点击查询按钮(触发AJAX加载) driver.find_element("id", "query_ticket").click() # 等待结果表格出现 wait.until(EC.presence_of_element_located((By.XPATH, "//tbody[@id='queryLeftTable']/tr"))) # 解析车次行(XPath需根据实际HTML结构调整) rows = driver.find_elements("xpath", "//tbody[@id='queryLeftTable']/tr[not(@datatran)]") for row in rows: try: # 获取车次号单元格文本 train_num = row.find_element("xpath", "./td[1]/div/div/text()").text.strip() if train_num == train_no: # 定位席别列(第5列为二等座,索引从0开始) seat_cell = row.find_elements("tag name", "td")[4] remaining = seat_cell.text.strip() if remaining in ['有', '无', '候补']: # '有'表示有票,'无'表示无票,'候补'表示可提交候补 return remaining except: continue return None # 使用示例:开售前30秒开始轮询 start_time = time.time() while time.time() - start_time < 180: # 最多轮询3分钟 status = query_tickets( driver, from_station='BJP', to_station='SHH', train_date='2024-06-15', train_no='G101', seat_type='O' ) if status == '有': print("✅ 发现余票!开始提交订单...") submit_order(driver, 'BJP', 'SHH', '2024-06-15', 'G101', 'O') break elif status == '候补': print("⚠️ 可提交候补,进入候补流程...") submit_waitlist(driver, 'BJP', 'SHH', '2024-06-15', 'G101', 'O') break else: print(f"⏳ 未查到票,{datetime.now().strftime('%H:%M:%S')} 再试...") time.sleep(3) # 严格遵守3秒间隔

关键参数说明:

  • from_station/to_station必须用12306车站编码(非中文名),可通过https://kyfw.12306.cn/otn/resources/js/framework/station_name.js获取;
  • seat_type字母映射:O=二等座,M=一等座,6=商务座,1=硬座,4=软卧;
  • time.sleep(3)是硬性要求,少于3秒大概率返回空数据或触发限流。

4.2 订单预提交:动态Token提取与乘客信息绑定

当余票页显示“有”时,点击“预订”按钮会跳转至订单确认页(/otn/confirmPassenger/initDc),此页包含一个隐藏字段REPEAT_SUBMIT_TOKEN,它是后续所有提交操作的唯一凭证,有效期约10分钟。脚本必须在此页立即提取Token,并用它构造后续请求。

def submit_order(driver, from_station, to_station, train_date, train_no, seat_type): # 点击预订按钮(需先定位到G101行的预订链接) # XPath示例://a[text()='预订' and ancestor::tr[contains(., 'G101')]] book_btn = driver.find_element("xpath", f"//a[text()='预订' and ancestor::tr[contains(., '{train_no}')]]") book_btn.click() # 等待订单页加载 wait = WebDriverWait(driver, 10) wait.until(EC.url_contains("/otn/confirmPassenger/initDc")) # 提取REPEAT_SUBMIT_TOKEN token_script = "return $('input[name=\"REPEAT_SUBMIT_TOKEN\"]').val();" token = driver.execute_script(token_script) # 选择乘车人(假设第一个乘车人ID为'1') passenger_id = '1' driver.find_element("xpath", f"//ul[@id='normal_passenger_id']/li[{passenger_id}]/input").click() # 选择席别 seat_select = driver.find_element("id", "seatType_1") # 二等座对应value='O' seat_select.find_element("xpath", f"./option[@value='{seat_type}']").click() # 提交订单(触发AJAX) driver.find_element("id", "submitOrder_id").click() # 等待弹窗确认(关键!必须等待弹窗出现再点击确认) wait.until(EC.presence_of_element_located((By.CLASS_NAME, "dhtmlx_wins_body"))) driver.find_element("xpath", "//a[text()='确认']").click() # 等待跳转至支付页 wait.until(EC.url_contains("/otn/payOrder/init")) print("✅ 订单已提交,跳转至支付页")

此段代码的血泪经验在于:必须等待弹窗(.dhtmlx_wins_body)出现后再点击“确认”。若用time.sleep(1)硬等,常因网络延迟导致点击无效;若不等弹窗直接跳转,订单会提交失败。这是12306前端JS控制的强依赖关系,绕不过。


5. 避坑指南:12306反爬升级后的5个致命陷阱与解法

12306的风控策略每月迭代,以下是在2024年实测中踩过的真坑,每一条都附带可复现的现象、根因分析和落地解法。照着改,能省掉至少80%的调试时间。

5.1 现象:登录成功后,进入车票查询页报“当前访问异常,请稍后再试”

原因:ChromeDriver启动时未关闭--enable-automation开关,导致window.navigator.webdriver为true,12306前端JS检测到自动化特征后主动拦截。
解决:严格使用2.2节中的excludeSwitches和useAutomationExtension=False,并在addScriptToEvaluateOnNewDocument中覆盖navigator.webdriver。验证方法:在DevTools Console中执行navigator.webdriver,返回undefined即成功。

5.2 现象:验证码识别总是返回空字符串或乱码

原因:截图区域坐标错误(不同分辨率/缩放比例下元素位置偏移),或tesseract未加载中文字体(12306验证码含中文字符)。
解决:

  • 用Chrome DevTools的Elements面板精确定位验证码<img>标签,右键Copy > Copy element,粘贴到文本编辑器查看style="left:120px;top:320px;"等绝对定位值;
  • 安装中文字体:sudo apt install tesseract-ocr-chi-sim(Ubuntu),并在pytesseract.image_to_string()中添加lang='chi_sim'参数。

5.3 现象:余票查询返回空数据,或始终显示“无”

原因:未正确设置Referer头,或未携带Cookie(登录态丢失)。12306的/otn/leftTicket/query接口要求Referer: https://kyfw.12306.cn/otn/leftTicket/init且必须有有效的JSESSIONID。
解决:不直接调用接口,而是用driver.get()访问/otn/leftTicket/init页面,再点击查询按钮——这样浏览器自动携带所有Cookie和Referer。若必须直调API,需用driver.get_cookies()提取Cookie并注入请求头。

5.4 现象:点击“预订”后卡在空白页,或跳转至登录页

原因:REPEAT_SUBMIT_TOKEN过期(超过10分钟未使用),或Token提取时机错误(在AJAX加载完成前就读取)。
解决:

  • 在/otn/confirmPassenger/initDc页面加载完成后,用WebDriverWait等待<input name="REPEAT_SUBMIT_TOKEN">元素出现再提取;
  • 提取后立即使用,不要缓存Token超过5分钟;
  • 若Token失效,捕获alert("系统繁忙")并重新跳转至余票页重试。

5.5 现象:支付页显示“订单已过期,请重新提交”

原因:从订单提交到跳转支付页耗时过长(>15秒),或支付页未及时点击“确认支付”。12306支付页倒计时15分钟,但订单锁定期仅30秒。
解决:

  • 订单提交后,用WebDriverWait等待URL变为/otn/payOrder/init,而非time.sleep();
  • 进入支付页后,立即执行driver.find_element("id", "payButton").click(),不加任何延时;
  • 为防网络抖动,支付按钮点击后加1秒等待,再检查是否跳转至网银页面。

6. 进阶技巧:用Cookie持久化规避重复登录,及候补订单的智能调度

登录是抢票链路中最耗时、最不稳定的一环。每次运行脚本都重新输密码、过验证码,不仅增加失败概率,还放大被风控风险。真正的工程化方案,是将登录态持久化为Cookie文件,在后续运行中直接加载复用。这不仅能提速50%,还能让脚本在服务器上7×24小时值守。

6.1 Cookie持久化:登录一次,复用一周

12306的登录Cookie有效期通常为7天(取决于账号安全等级),包含JSESSIONID、RAIL_DEVICEID、RAIL_EXPIRATION等关键字段。脚本可在首次登录成功后,将全部Cookie序列化为JSON文件;下次启动时,先加载Cookie并访问任意页面触发续期,再执行抢票逻辑。

import json import os def save_cookies(driver, filename='cookies.json'): """保存当前会话Cookie""" cookies = driver.get_cookies() with open(filename, 'w') as f: json.dump(cookies, f) def load_cookies(driver, filename='cookies.json'): """加载Cookie并访问首页续期""" if not os.path.exists(filename): return False with open(filename, 'r') as f: cookies = json.load(f) # 访问首页,清空旧Cookie driver.get('https://www.12306.cn') # 添加Cookie(需先访问域名才能添加) for cookie in cookies: # 删除HttpOnly字段(Selenium不支持设置) cookie.pop('httpOnly', None) cookie.pop('sameSite', None) try: driver.add_cookie(cookie) except: pass # 忽略无法添加的Cookie # 刷新页面使Cookie生效 driver.refresh() # 验证登录态 if "我的12306" in driver.title: print("✅ Cookie加载成功,已登录") return True else: print("❌ Cookie失效,需重新登录") return False # 主流程中调用 if not load_cookies(driver): login_by_password(driver, USERNAME, PASSWORD) save_cookies(driver) # 首次登录后保存

注意:RAIL_DEVICEID和RAIL_EXPIRATION是设备指纹相关字段,若更换机器或重装系统,此Cookie将失效,需重新登录。但同一台机器上,7天内无需再次输入密码。

6.2 候补订单的智能调度:从“被动等待”到“主动占位”

当目标车次无票时,12306提供“候补购票”功能——支付10元预付款,系统按候补顺序自动分配退票/余票。脚本可将其纳入自动化流程,实现“有票秒提,无票候补”的双模策略。

def submit_waitlist(driver, from_station, to_station, train_date, train_no, seat_type): # 进入候补页面 driver.get(f"https://kyfw.12306.cn/otn/queue/init?from_station_telecode={from_station}&to_station_telecode={to_station}&train_date={train_date}&train_no={train_no}&seatType={seat_type}") # 等待候补表单加载 wait = WebDriverWait(driver, 10) wait.until(EC.presence_of_element_located((By.ID, "normal_passenger_id"))) # 选择乘车人 driver.find_element("xpath", "//ul[@id='normal_passenger_id']/li[1]/input").click() # 勾选“接受调剂” driver.find_element("id", "acceptGoods").click() # 提交候补 driver.find_element("id", "submitWaitList").click() # 等待成功提示 wait.until(EC.presence_of_element_located((By.CLASS_NAME, "success-tips"))) print("✅ 候补订单已提交,订单号:", driver.find_element("class name", "order-number").text) # 在余票查询循环中调用 if status == '候补': submit_waitlist(driver, ...)

候补的关键是**“接受调剂”勾选**——它极大提升候补成功率。脚本应默认开启,并在提交后记录候补单号,便于后续查询状态。

我坚持在所有项目里写死time.sleep(3)而不是用WebDriverWait替代所有延时,因为12306的AJAX响应时间波动极大,有时元素已存在但数据未渲染,wait.until(EC.presence_of_element_located())会误判成功。宁可多等1秒,也不愿在关键时刻掉链子。这个习惯救了我三次大促抢票。希望帮到你。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询