ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Selenium 实现 12306 自动抢票:从配置到下单通知的完整指南

Selenium 实现 12306 自动抢票:从配置到下单通知的完整指南 简介一套基于 Selenium 的 12306 自动抢票脚本资料包面向计算机相关专业学生、开发者及自动化测试入门者针对购票流程自动化、模拟登录、余票查询等场景提供可运行的完整方案。包内共 18 个文件包含 7 个 Python 源文件、5 个 XML 配置、3 个 TXT 说明以及 Markdown 文档、YAML 配置和工程模块文件合计仅 64KB目录围绕车站信息、参数配置、购票核心逻辑和邮件通知等模块组织结构清晰适合快速定位代码。目前已有 129 人学习下载可用于课程设计、毕业设计或 Selenium 自动化学习进阶。该套项目源自高分作品功能经测试通过附带详细文档便于在此基础上扩展多账号监控、验证码处理或余票提醒等功能是一份轻量实用的自动化实战参考。1. 用 Selenium 写 12306 抢票脚本先搞清楚这套资料能干什么拿 Selenium 做 12306 自动抢票这个思路被讨论了很多年但真正能一路跑到下单、通知的完整资料并不多。大多数教程停在“查询余票”这一步再往后登录会话怎么保持、车次怎么定位、乘客怎么勾选、验证码怎么处理就断掉了。这套资源好就好在它是一条完整链路配置驱动、车站信息自动加载、Selenium 维持会话、下单成功后走 SMTP 发邮件通知你。适合两类人——一类是拿它当课设、毕设底座的在校生另一类是手里有票务监控需求、想改成自动工具的从业者。先说实话脚本不保证 100% 抢到票它干的是把“反复刷新查票、手动提交订单”变成“自动监控、自动下单、自动通知”。2. 项目结构与配置准备六个模块先对号入座拿到压缩包先别急着跑先把目录结构读明白。这份资料里的代码不是单文件脚本而是按职责拆成了几个包好处是改一处不会牵连别处坏处是不熟悉 Python 包结构的人容易在 import 和路径上卡住。2.1 文件清单每个目录到底管什么先看整体布局压缩包解压后是一个12306-master项目根目录核心文件和模块如下表文件/目录职责说明main.py程序入口组装配置、车站表、购票流程串起整个链路config.yaml全局配置出发地、目的地、日期、车次、乘客、SMTP 通知等requirements.txt依赖清单记录 selenium、PyYAML、requests 等库直接 pip 安装common/get_station.py车站信息拉取请求 12306 官方车站接口生成station.txt映射表common/get_config.py配置读取加载config.yaml统一返回字典对象common/station.txt车站码表中文站名与电报码的映射get_station.py的产物buyTicket/ticket.py核心购票逻辑WebDriver 初始化、登录、查询、下单、提交订单inform/send_email.py邮件通知下单成功后通过 SMTP 发送结果邮件项目授权码.txt授权码说明主要交代邮箱 SMTP 授权码从哪拿、填在哪我拆解这类项目的第一习惯是先把配置文件找出来读一遍因为脚本的运行参数全集中在config.yaml里代码本身反而不用大改。station.txt不需要手动维护脚本启动时会自动拉取并生成这点后面细说。2.2 config.yaml这些参数填错一个就白跑配置是整个脚本最核心的输入填错一项脚本就会在某个环节静默失败。以常见的配置结构为例account: # 登录方式建议使用 qrcode账号密码登录容易触发滑块 login_type: qrcode ticket: from_station: 北京南 to_station: 上海虹桥 train_date: 2025-06-10 trains: [G101, G105] seats: [二等座] passengers: [张三] purpose_codes: ADULT scheduler: retry_interval: 3 max_retry: 50 notify: smtp_host: smtp.qq.com smtp_port: 465 sender: yournameqq.com auth_code: your-smtp-auth-code receiver: yournameqq.com这段配置里最容易翻车的三个点第一train_date必须写成字符串带不带引号决定了 YAML 会不会把它当时间类型转掉第二passengers里的姓名必须和 12306 乘车人列表里的姓名完全一致多一个空格都匹配不上第三auth_code不是 QQ 邮箱的登录密码是开启 SMTP 服务后生成的授权码这个码在项目授权码.txt里一般也会有说明。purpose_codes固定填ADULT这是 12306 查询接口要求的成人票参数。2.3 依赖安装与入口逻辑先跑通最小闭环依赖安装没有玄学直接读requirements.txt装就行pip install -r requirements.txtrequirements.txt里通常包含selenium、PyYAML、requests这几个基础库。selenium 版本建议 4.x 起步3.x 和 4.x 在webdriver.Chrome()的写法上有细微差别项目里如果标注了版本就按它的来没标注就装最新版。装完后先确认 Chrome 浏览器和 chromedriver 的版本能对上这事后面避坑章单独说。入口逻辑在main.py结构大概是这样的from buyTicket.ticket import TicketBuyer from common.get_station import fetch_station_map from common.get_config import load_config def main(): cfg load_config() # 启动时拉取最新车站表生成 {中文站名: 电报码} 的映射 station_map fetch_station_map() buyer TicketBuyer(cfg, station_map) buyer.run() if __name__ __main__: main()main.py的职责就是把三样东西组装起来配置字典、车站映射表、购票执行器。fetch_station_map()会先请求一次 12306 的车站接口拿到映射后传给TicketBuyer。这么设计的好处是车站信息是动态的12306 偶尔会调整线路或站点名启动时拉一次比写死在代码里稳妥得多。3. 车站信息与查询链路station.txt 里的黑匣子12306 的查询接口不认中文站名它认的是车站电报码。比如北京南对应VNP上海虹桥对应AOH。这套对应关系维护起来很麻烦项目直接用common/get_station.py自动拉取这个设计是整份资料里比较聪明的部分。3.1 get_station.py从官方接口拉车站表12306 有一个公开的车站名称接口返回一段特殊格式的文本。get_station.py的核心逻辑就是请求这个接口、解析文本、生成映射import requests import re STATION_URL https://kyfw.12306.cn/otn/resources/js/framework/station_name.js def fetch_station_map(): # 12306 的车站文件是动态生成的直接 GET 就能拿到 resp requests.get(STATION_URL, verifyFalse, timeout10) # 返回格式形如bjb|北京北|BJP|beijingbei|bjb|0bjn|北京南|VNP|beijingnan|bjn|1 # 每段用 分隔字段用 | 分隔中文站名在第二段电报码在第三段 stations re.findall(r([a-z])\|([\u4e00-\u9fa5])\|([A-Z])\|, resp.text) return {name: code for _, name, code in stations}这段代码里值得注意的参数有两个verifyFalse是关闭 SSL 证书校验12306 的证书链在部分 Python 环境里会校验失败不关会直接抛异常timeout10是请求超时网络波动时不至于让脚本卡死。正则表达式里[\u4e00-\u9fa5]匹配中文字符这要求响应文本的编码解析正确requests会从响应头推断编码如果发现中文乱码需要手动指定resp.encoding utf-8。3.2 get_config.py路径与编码的细节get_config.py的代码很短但有一个细节很关键——配置文件路径不能写死相对路径import yaml from pathlib import Path def load_config(): # 不管在哪个目录下启动脚本都定位到项目根目录下的 config.yaml cfg_path Path(__file__).parents[1] / config.yaml with open(cfg_path, encodingutf-8) as f: return yaml.safe_load(f)用Path(__file__).parents[1]而不是config.yaml是因为如果你在buyTicket子目录里运行某个测试脚本相对路径就会指向错误的位置。parents[1]表示当前文件上一级目录的上一级也就是项目根目录。encodingutf-8也要显式指定Windows 默认编码是 gbk不指定的话配置文件里的中文注释和站名全会解析失败。3.3 查询参数对齐日期、席别与接口字段车站映射拿到后下一步是把配置里的中文转成接口参数。查询页面的 URL 拼接是这么做的# 把 北京南/上海虹桥 映射成 VNP/AOH直接拼到 leftTicket 查询接口 base (https://kyfw.12306.cn/otn/leftTicket/init?linktypeiddc fs{from_code}ts{to_code}date{date}) url base.format( from_codestation_map.get(cfg[from_station]), to_codestation_map.get(cfg[to_station]), datecfg[train_date], )这里的fs和ts参数要求的是电报码而不是中文名。很多人直接在接口里传中文名返回结果要么全空要么报错。日期格式必须是2025-06-10这种带横线的格式2025/06/10或2025年6月10日接口都不认。席别参数在 12306 的接口里有自己的编码体系二等座对应M一等座对应M之外的另一套编码第三方的查票脚本各有各的映射方式资料里的config.yaml如果只维护了中文名那ticket.py里一定有一份席别编码表改配置时优先改这份表而不是去改代码逻辑。4. 核心购票流程ticket.py 里的状态机与等待玄学buyTicket/ticket.py是整份资料的核心做的事情可以拆成四段初始化浏览器、登录保持会话、查询并定位车次、提交订单。每一段都踩过不少坑。4.1 WebDriver 初始化与扫码登录Selenium 的自动化痕迹很明显用账号密码登录 12306 大概率会触发滑块验证所以项目普遍采用扫码登录方案人工用 App 扫码让浏览器保持登录态from selenium import webdriver from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC from selenium.webdriver.common.by import By class TicketBuyer: def __init__(self, cfg, station_map): self.cfg cfg self.station_map station_map # 本地需安装 chrome 和匹配版本的 chromedriver options webdriver.ChromeOptions() options.add_argument(--disable-blink-featuresAutomationControlled) self.driver webdriver.Chrome(optionsoptions) def login_by_qrcode(self): self.driver.get(https://kyfw.12306.cn/otn/resources/login.html) # 登录成功会跳回首页轮询等待这个跳转超时120秒抛异常 WebDriverWait(self.driver, 120).until( EC.url_contains(otn/view/index.html) )--disable-blink-featuresAutomationControlled这个参数可以去掉部分自动化标记但不要幻想了它就完全隐身。WebDriverWait每 0.5 秒探测一次条件120 秒是扫码的时间上限。如果经常出现超时说明二维码界面弹出来后没有及时扫或者网络环境加载登录页太慢可以先手动打开登录页确认能正常显示。4.2 查询、定位车次与点击预订登录后直接跳转到余票查询页比在页面上重新输入城市和日期要快得多也稳定得多。查询之后要定位目标车次那一行的预订按钮def query_and_locate(self): # 直达查询页参数用电报码 self.driver.get(self._build_query_url()) # 等待车次列表渲染完成列表为空时页面会显示 无票 提示 WebDriverWait(self.driver, 30).until( EC.presence_of_element_located((By.XPATH, //tbody[idqueryLeftTable]/tr)) ) # 遍历配置里的优先车次找到就点那一行的预订按钮 for train_no in self.cfg[trains]: # 车次行的 id 通常包含车次号比全文匹配稳 row self.driver.find_element( By.XPATH, f//tr[contains(id,{train_no})] ) book_btn row.find_element(By.CLASS_NAME, btn72) book_btn.click() return train_no等待车次列表渲染完是个关键动作。queryLeftTable里即使没票也会渲染行只是预订按钮的 class 不同有的显示“预订”有的显示“无票”或“候补”。脚本判断有没有票本质上是判断按钮的可用状态而不是判断有没有行。配置里的trains建议按优先级排列比如[G101, G105]这样脚本会优先点 G101点不成就点 G105。如果你不想限定车次留空数组表示匹配所有车次但那样每次点击的坐标定位需要重写不建议新手改这个逻辑。4.3 乘客勾选、验证码与提交订单点击预订后会进入确认订单页这里有两个环节勾选乘客、处理验证码。乘客勾选用姓名定位是不错的做法def submit_order(self): WebDriverWait(self.driver, 30).until( EC.presence_of_element_located((By.ID, normalPassenger)) ) # 每个乘客姓名对应一个 labelcheckbox 在 label 前面 for name in self.cfg[passengers]: cb self.driver.find_element( By.XPATH, f//label[contains(text(),{name})]/preceding-sibling::input ) if not cb.is_selected(): cb.click()验证码是整条链路里不确定性最大的一环。网上常有人问 12306 验证码有没有现成的 Java 实现其实验证码识别跟语言关系不大关键在方案选择。常见做法是接入打码平台把验证码图片提交过去拿坐标再在页面上逐个点击也有脚本干脆把浏览器窗口置顶验证码弹出后人工手点脚本用 WebDriverWait 等提交按钮变得可点击。资料里如果写死了某一种方案不建议替换因为验证码的图片样式和点选逻辑经常调整换方案的成本很高。提交订单之后还有一步“确认”确认按钮和验证码提交按钮是同一个还是两个要看当时 12306 的页面结构建议在本地跑通一次完整流程后再改选择器。5. 避坑排查五个高频翻车点逐一拆解这套脚本我跑下来的体感是配置对、依赖对、登录过了大概率能跑通真正翻车的地方高度集中在这五个每个都是血泪经验。5.1 chromedriver 版本不匹配导致启动即崩现象运行webdriver.Chrome()时抛出SessionNotCreatedException提示This version of ChromeDriver only supports Chrome version XX。 原因chromedriver 和本机 Chrome 浏览器版本必须严格对应版本差一位都启动不了。 解决先在浏览器地址栏输入chrome://version看 Chrome 大版本号然后去 chromedriver 的下载页找对应版本解压后放到 Python 环境目录或系统 PATH 里。Windows 下常见做法是丢进C:\Windows\System32更规范的是放到虚拟环境目录下的Scripts文件夹。5.2 车站名匹配失败现象脚本报KeyError: 北京南或者查询接口返回空列表。 原因station.txt是启动时拉取的如果拉取时机不对或接口变了映射表里没有配置里写的站名。 解决先检查get_station.py的响应内容是否正常手动请求一次station_name.js看有没有中文乱码。另一个容易忽略的点是站名里的空格比如“北京 南”和“北京南”是两回事配置文件里别多打空格。5.3 登录弹出滑块验证现象扫码登录成功后页面不跳转反而出现滑块拼图。 原因Selenium 的自动化特征还是被识别了浏览器指纹、cookies、执行环境都被标记。 解决第一优先用无痕模式启动浏览器options.add_argument(--incognito)能减少部分 cookie 污染第二在扫码登录前不要做任何页面操作直接打开登录页等扫码第三如果还触发滑块只能放弃脚本自动填密码坚持扫码方案12306 的滑块验证强度和当时网络环境相关有时候多试两次就过了。5.4 邮件通知发不出去现象下单成功但send_email.py报smtplib.SMTPAuthenticationError。 原因auth_code填成了邮箱登录密码或者 SMTP 服务没在邮箱后台开启。 解决QQ 邮箱在“设置-账户”里开启 SMTP 服务后会生成一串授权码这串码才是auth_code的正确值。另外注意smtp_portQQ 邮箱 SSL 是 465非 SSL 是 25465 端口配合SMTP_SSL是最稳的组合。测试时先用一段独立脚本单独发一封邮件通了再跑全流程不要等到抢票成功才发现通知链路是断的。5.5 查询频率过高被临时限制现象跑一段时间后查询接口开始返回空数据或者页面弹出验证码。 原因12306 对高频查询有限流策略retry_interval填 1 秒就是自找麻烦。 解决retry_interval不低于 2 秒我一般取 3 到 5 秒加随机浮动time.sleep(retry_interval random.uniform(0, 2))这样的写法比固定间隔更不容易被识别。max_retry也建议设置上限跑完一轮就退出而不是无限循环既避免封禁也省资源。6. 把脚本变成工具邮件通知验证与定时调度到这步脚本能跑通接着就要解决“能用”的问题下单成功了怎么让我知道以及怎么在放票时间自动启动。邮件通知的验证有个技巧不要等到真正下单成功才测。在main.py里加一个启动通知登录一过就立刻发一封“脚本已启动”的邮件这样整条 SMTP 链路在抢票前就被验证过了。看send_email.py的结构import smtplib from email.mime.text import MIMEText def send_notify(cfg, subject, content): msg MIMEText(content, plain, utf-8) msg[Subject] subject msg[From] cfg[sender] msg[To] cfg[receiver] # 使用 SSL 连接465 端口不需要 starttls with smtplib.SMTP_SSL(cfg[smtp_host], cfg[smtp_port]) as server: server.login(cfg[sender], cfg[auth_code]) server.send_message(msg)定时调度优先用系统自带的任务计划不必在脚本里做循环。Windows 用任务计划程序每天固定时间启动python main.pyLinux 用 crontab写一行30 7 * * * cd /path/to/project /usr/bin/python3 main.py就能在每天早上 7 点半启动。再配合日志落盘logging.basicConfig(filenamerun.log, levellogging.INFO)把每次查询和下单结果写进文件第二天早上看日志比盯着控制台实用了。还有个细节差点忘了说脚本跑完一轮不管有没有票记得driver.quit()关掉浏览器。我经历过浏览器进程残留十几个、内存被吃光的状况从那以后每次在主流程的finally块里强制闭浏览器。抢票这件事本身没那么多玄学把配置填准、通知打通、日志留好剩下的交给脚本循环跑。希望帮到你。本文还有配套的精品资源点击获取
返回列表