首页 / 资讯中心 / 文章详情

从零实现Python自动抢票脚本:12306查票、下单与避坑全解析

从零实现Python自动抢票脚本:12306查票、下单与避坑全解析 ★ FEATURED ARTICLE
简介这是一份面向12306购票场景的Python自动化抢票脚本资源主要解决春节、中秋、国庆等出行高峰期间手动刷新购票页面效率低、容易错过余票的问题适合需要抢购回家车票的个人用户也适合想了解Python自动化操作的学习者。脚本基于电脑端12306购票流程运行通过自动化的查询与提交方式减轻用户反复操作的负担提升抢到车票的成功概率。RAR压缩包体积仅6KB共8个文件核心为1个Python脚本另有4个XML工程配置文件、1个iml模块文件以及gitignore等辅助文件整体结构简洁便于导入开发环境阅读和修改。目前已有2169人学习关注说明该脚本在假期出行抢票场景中具有较高的实用参考价值。用户下载后可直接查看脚本主逻辑理解自动抢票的实现思路再结合自己的车次、日期与乘车人信息进行适当调整适合具有一定Python基础、希望自行搭建自动抢票工具的人群。1. 为什么一定要自己写一个抢票脚本某开发者春节前一周每天盯着抢票软件的加速包页面显示有票点进去就没了。他不信邪花了一个周末把 12306 的查票、下单流程摸了一遍写出了一个不到 500 行的 Python 自动抢火车票脚本。这个项目不是黑科技插件而是直接调用 12306 官网的公开接口按你配置的车次、日期、乘车人自动循环查票、锁票、提交订单。它适合两类人一类是需要一个稳定抢票入口的 Python 开发者另一类是想研究会话保持和接口签名逻辑的初学者。没有图形界面全靠命令行输出但胜在逻辑透明、可控性强。2. 技术拆解登录态、车站编码和余票查询背后的接口设计2.1 项目结构轻量工程的几个核心文件这个项目不是一个框架解压后就是一份很克制的目录结构12306_ticket/ ├── station.py # 车站编码映射由脚本自动生成 ├── ticket.py # 主脚本登录、查票、下单 ├── cookie.txt # 登录后保存的会话 cookie ├── requirements.txt # 依赖清单 └── README.md # 使用说明核心就两个 py 文件。station.py 负责把所有车站的汉字名称和电报码做映射ticket.py 跑整个抢票流程。依赖只有三样requests、prettytable、Pillow。requests 负责所有 HTTP 请求prettytable 用来在命令行里把余票信息格式化成表格Pillow 用来处理登录时的验证码图片。选 requests 而不是 selenium是刻意为之。查票和下单本质上都是 HTTP 请求用浏览器自动化反而又慢又容易被风控盯上。requests 直接模拟浏览器发出的 JSON 请求速度能快一个数量级而且 12306 的接口本身就是公开的不需要处理浏览器渲染和 JS 执行这类黑匣子问题。缺点也有——你得自己维护会话、手动处理验证码这些恰好就是本项目花力气解决的地方。2.2 登录态保持cookie 和 session 的完整链路抢票的第一步不是查票是登录。12306 的登录流程比普通网站多一道坎验证码。而且它后期上线的点选式验证码让自动识别变得极其痛苦所以这个项目在登录环节做了一个很务实的取舍——支持两种模式要么手动输入验证码坐标要么调用打码平台 API。先看登录模块的核心代码import requests def login(session, username, password): # 第一次请求拿到 cookie 和验证码图片 init_resp session.get(https://kyfw.12306.cn/passport/web/init) # 请求验证码图片交给用户或OCR识别 captcha_img session.get(https://kyfw.12306.cn/passport/captcha/image64) # 将图片保存到本地识别后输入坐标 captcha input(请输入验证码坐标格式如 100,50: ) # 校验验证码答案格式是坐标的x,y拼接 check_resp session.post( https://kyfw.12306.cn/passport/captcha/check, data{answer: captcha} ) if check_resp.json()[result] ! 1: print(验证码校验失败重新获取验证码) return False # 验证码通过后再提交账号密码 login_resp session.post( https://kyfw.12306.cn/passport/web/login, data{username: username, password: password, appid: otn} ) return login_resp.json()[result] 1这段代码有几个关键点。首先所有请求都用同一个 session 对象这很重要——12306 会把登录态和 cookie 强绑定你在别的 session 里发起请求服务端根本不认。其次验证码的 answer 参数是图片上点选位置的横纵坐标拼接比如点击位置在 (100, 50)就传 100,50。最后登录成功后服务端会返回一个 token 和用户信息这些会被自动写进 session 的 cookie 里后续查票下单全靠它。登录成功后的另一个动作是保存 cookie 到本地文件import json def save_cookie(session, pathcookie.txt): cookie_dict session.cookies.get_dict() with open(path, w) as f: json.dump(cookie_dict, f) def load_cookie(session, pathcookie.txt): import os if not os.path.exists(path): return False with open(path, r) as f: cookie_dict json.load(f) session.cookies.update(cookie_dict) return True把 cookie 落盘之后再跑脚本就能直接跳过登录环节。12306 的 cookie 有效期一般是一个月过期后脚本会提示重新登录。我一般会加一个校验函数用 cookie 请求一次用户信息接口如果返回正常就继续跑否则强制重登避免带着过期 cookie 跑一晚上结果一次都没抢到。2.3 车站编码和余票查询接口的参数拆解12306 的查票接口和别的网站不太一样它不认识汉字。你在网页上搜“北京南”后端实际用的是“VNP”这个电报码搜“上海虹桥”接口认的是“AOH”。所以抢票脚本的准备工作里必须有一份车站名称和电报码的对照表。这个项目没有手动维护这个表而是写了一个自动抓取脚本。# station.py import re import requests def init_station(): url https://kyfw.12306.cn/otn/resources/js/framework/station_name.js text requests.get(url, timeout10).text # station_name.js 的格式是 b|北京北|VAP|beijingbei|bjb|... 每5个字段一条记录 stations re.findall(r([\u4e00-\u9fa5])\|([A-Z]{3}), text) return {name: code for name, code in stations}运行一次 init_station就能把所有车站的汉字名和电报码抓下来存成字典。有了这个映射配置里写“北京南”脚本就知道是“VNP”写“上海虹桥”就知道是“AOH”。如果某天新增了车站重新执行一遍 init_station 就能同步不用手动维护。查票接口的参数也在这里一并定义def query_ticket(session, from_station, to_station, date, purposeADULT): url https://kyfw.12306.cn/otn/leftTicket/queryG params { leftTicketDTO.train_date: date, # 出发日期格式 2025-01-28 leftTicketDTO.from_station: from_station, # 出发站电报码 leftTicketDTO.to_station: to_station, # 到达站电报码 purpose_codes: purpose, # ADULT成人票0X00学生票 } resp session.get(url, paramsparams, timeout10) return resp.json()[data][result]leftTicketDTO 是 12306 接口的固定参数名train_date 传日期格式必须是 YYYY-MM-DDfrom_station 和 to_station 传刚才生成的车站电报码purpose_codes 一般用 ADULT。返回的 result 是一个字符串列表每个字符串对应一个车次里面用竖线分隔了车次号、出发时间、到达时间、余票状态等几十个字段。解析这些字段就是下一章的事。这里补充一个细节这个接口偶尔会返回 None 的 data原因是请求频率过高被限流或者车站编码不存在。脚本里要加一个空值判断否则resp.json()[data]直接抛 TypeError整个循环就挂了。3. 核心模块实现从查票到下单的完整链路3.1 余票查询模块车次筛选和余票解析查票接口返回的 result 字符串长这样0|G1|VNP|AOH|北京南|上海虹桥|...|有|有|无|...竖线分隔的字段里索引 2 是车次号索引 3 是出发站编码索引 4 是到达站编码索引 6 和 7 分别是始发站和终到站。后半段的“有”“无”或数字就是各席别的余票状态。解析的代码写成这样def parse_tickets(results, train_numbers): tickets [] for item in results: fields item.split(|) if len(fields) 35: continue train_no fields[2] if train_no not in train_numbers: continue # 二、三、四等座余票这里以二等座为例字段位置会变务必动态判断 seat_map { 二等座: fields[35], 一等座: fields[36], 商务座: fields[37], } available {k: v for k, v in seat_map.items() if v not in (, 无)} if available: tickets.append({ train_no: train_no, from: fields[6], to: fields[7], depart_time: fields[8], arrive_time: fields[9], seat: available, }) return tickets这里最容易翻车的是字段下标。12306 调整过几次接口返回格式不同车次返回的字段长度可能不一样尤其是停运车次和临客。我见过有人写死第 35 位是二等座结果某天接口加了两个字段整个脚本就废了。稳妥做法是先在本地打印一条完整的 result 字符串数清楚下标位置再写解析逻辑。这是血泪经验别问我怎么知道的。解析完成后脚本会在命令行用 prettytable 输出一张表------------------------------------------------------------------- | 车次 | 出发站 | 到达站 | 出发时间 | 到达时间 | 余票 | ------------------------------------------------------------------- | G1 | 北京南 | 上海虹桥 | 07:00 | 11:29 | 二等座: 3 | -------------------------------------------------------------------看到有余票脚本就自动进入下单流程。3.2 提交订单从勾选乘车人到预下单下单环节的核心接口是 submitOrderRequest。这里有个关键点查票接口返回的数据里有一个加过密的 secretStr 字段下单时必须原样传回去。它包含了车次、日期、出发站、到达站的加密信息不能自己拼也不能改动任何字符。def submit_order(session, train_info, date, passengers): url https://kyfw.12306.cn/otn/leftTicket/submitOrderRequest data { secretStr: train_info[secret_str], # 查票接口返回的加密串 train_date: date, fromStationTelecode: train_info[from_code], toStationTelecode: train_info[to_code], purpose_codes: ADULT, passengerTicketStr: build_passenger_str(passengers), } resp session.post(url, datadata) result resp.json() if result[status] and result[data] N: print(订单预提交成功进入排队队列) return True # 常见失败原因余票被抢完、乘车人信息格式不对 print(提交失败 result[messages][0] if result.get(messages) else 未知错误) return Falsebuild_passenger_str 是一个容易被低估的函数。乘车人信息要拼成一个大字符串格式是“席别编码,乘车人类型,姓名,身份证号,手机号”多个乘车人用下划线拼接。席别编码对应关系是1 二等座2 一等座3 商务座4 硬卧5 软卧。类型编码 1 是成人2 是儿童。拼接代码如下def build_passenger_str(passengers): parts [] for seat_type, passenger_type, name, id_card, phone in passengers: parts.append(f{seat_type},{passenger_type},{name},{id_card},{phone}) return _.join(parts)参数说明第一个字段是席别编码第二个是乘客类型第三个是姓名第四个是身份证号第五个是手机号。这个顺序不能乱特别是身份证号不能带空格否则服务端校验直接返回“乘车人信息有误”。3.3 排队下单和订单确认处理异步队列预提交成功不代表锁票成功12306 在下单和确认之间还有一道异步队列。服务端会先返回一个 queue 信息客户端要轮询队列状态等排到队首之后再发确认请求。这个设计是为了避免高并发下直接把订单库打爆。import time def confirm_order(session, queue_info): # 轮询排队状态一般 3-10 秒内会出结果 while True: try: resp session.post( https://kyfw.12306.cn/otn/confirmPassenger/getQueueCount, dataqueue_info, timeout5 ) result resp.json() if result[status] and result[data][ticket]: print(排队成功正在确认订单...) break except Exception as e: print(f轮询异常{e}) time.sleep(3) # 队列确认后发送最终确认请求 confirm_resp session.post( https://kyfw.12306.cn/otn/confirmPassenger/confirmSingleForQueue, dataqueue_info, timeout5 ) data confirm_resp.json() if data[status]: print(订单确认成功请在 30 分钟内完成支付) return True print(确认失败 data.get(messages, 未知错误)) return False这段代码有两个经验值。轮询间隔 3 秒是经过实测的太频繁会被风控太慢又容易超时。整个排队到确认的过程一般控制在 10 秒内如果超过 30 秒还没结果基本就是没票了脚本会自动退出循环回到查票流程继续监控下一个车次。另外确认订单接口的 queue_info 是从 submitOrderRequest 的返回里拿到的里面包含一个 token 和一个 train_date 参数。这个 token 是一次性的确认完成后立即失效下一次抢票必须重新走一遍预提交流程。所以整个抢票循环的最小单位是“查票 → 预提交 → 排队确认”一次只能为一张订单服务。4. 避坑指南cookie 失效、异步队列和风控的实战排查4.1 cookie 失效查询正常但提交订单永远失败现象查票接口返回 200数据也正常但一到 submitOrderRequest 就报“用户未登录”或“登录状态失效”。原因登录态维护依赖两个 cookieJSESSIONID 和当前用户 token。车次高峰期服务端会主动把过期的 JSESSIONID 刷掉还有一种更隐蔽的情况是你在手机上登录了同一个账号把脚本保存的 cookie 顶掉了。解决每次运行前先读一次 cookie再调一次用户信息接口确认登录态失败就强制重新登录。同时给 cookie 文件加一个修改时间判断修改时间超过 3 天就主动重登。我在实际使用中还会把 cookie 文件的权限设成 600避免工作站上别人能看到。4.2 车站编码写死导致查不到票现象配置里写了正确的车站中文名但脚本一直提示“未找到该车站编码”查票永远返回空。原因很多人在初版脚本里自己维护了一份车站编码表但 12306 每年都会新增车站比如新开通的高铁站老表里没有。解决删掉写死的编码表改用本章第一节的 init_station 动态抓取。运行一次所有车站映射自动生成。注意station.js 的编码格式里既有三字母的电报码也有拼音缩写字正则只抓三字母的编码否则会把拼音码也抓进来传到接口里直接报错。4.3 余票字段错位把“无”当成“有”现象脚本明明检测到有余票下单后却提示“无票”或者反过来有票却显示无。原因12306 的余票接口在某些场景下会多出“列车停运”或“预售”等状态位导致字段下标偏移。尤其是高峰期有些车次的字段长度和平时不一样。解决不要用固定下标先判断车次状态字段。一般第 28 位左右有一个“列车停运”标志如果是 1 就跳过该车次。然后动态筛选余票字段把“有”“数字”都视为有票把“无”“空字符串”视为无票。解析前先打印一条完整字段列表核对一下比写死下标靠谱得多。4.4 乘车人信息格式错误导致下单失败现象submitOrderRequest 正常但 confirm 接口报“乘车人信息有误”。原因乘车人拼串里的身份证号、手机号、席别编码、座位类型编码必须严格对应。席别编码和座位类型是两个不同的概念很多人在这里搞混。比如商务座的席别编码是 3但座位类型编码又是 P二等座的席别编码是 1座位类型编码又是 O。拼串用的是席别编码不是座位类型编码。解决把席别编码和乘车人信息分开配置拼串前做一次正则校验。身份证 18 位、手机号 11 位不合法直接终止运行不要等提交后才报错。4.5 验证码识别的选择OCR 还是打码平台现象自动登录时卡在验证码环节OCR 识别率不稳定验证码经常输入错误。原因12306 的验证码是点选式偶尔还会出现滑动验证码普通的 OCR 识别不了。很多人用打码平台但识别率也只能做到 70% 左右。解决本项目默认提供手动输入验证码坐标的交互方式适合本地跑的场景登录一次 cookie 管一个月。想全自动的话可以接打码平台的 API但要考虑识别失败重试的流程设计设置最大重试次数否则会死循环。我个人的习惯是工作日白天手动登录一次然后设置定时任务让脚本挂着跑。5. 把脚本跑起来环境配置、参数声明和运行效果验证5.1 环境依赖安装这个脚本依赖 Python 3.8 以上环境安装依赖就一条命令pip install -r requirements.txtrequirements.txt 内容requests2.31.0 prettytable3.9.0 Pillow10.1.0建议用虚拟环境避免污染系统 Python。Windows 和 macOS 都能跑但要注意控制台编码。Windows 下建议执行set PYTHONIOENCODINGutf-8否则打印车次信息时会乱码。另外如果你在 Linux 服务器上跑没有显示器的情况下输入验证码会比较麻烦建议先在本地跑通登录流程生成 cookie 文件再传到服务器上运行。5.2 配置文件与参数声明运行前在脚本头部或 config 区块里配置抢票参数# ticket.py 顶部配置区 USERNAME your_account # 12306 账号 PASSWORD your_password # 密码 TRAIN_DATE 2025-01-28 # 出行日期注意不要超过预售期 FROM_STATION 北京南 # 出发站支持中文 TO_STATION 上海虹桥 # 到达站 PASSENGERS [ # 格式(席别编码, 乘车人类型, 姓名, 身份证号, 手机号) (1, 1, 张三, 110101199001011234, 13812345678), ] TRAIN_NUMBERS [G1, G3, G11] # 优先车次白名单FROM_STATION 和 TO_STATION 填中文即可脚本启动时会自动查 station 模块的映射。TRAIN_NUMBERS 是你要盯的车次白名单脚本会按列表顺序轮询。PASSENGERS 里的第一个参数是席别编码1 二等座2 一等座3 商务座。第二个参数是乘车人类型1 成人2 儿童。5.3 运行命令和日志解读运行python ticket.py脚本启动后会先加载车站编码表然后进入查票循环。日志大概长这样[09:00:01] 车站编码表加载完成共 3456 个车站 [09:00:02] 登录成功, 开始监控 G1 北京南 - 上海虹桥 [09:00:05] 无票, 当前余票: 二等座 无, 一等座 无 [09:00:08] 发现余票! 二等座剩余 3 张, 正在提交订单... [09:00:10] 预提交成功, 排队中... [09:00:13] 订单确认成功, 请登录 12306 完成支付看到“订单确认成功”就说明核心链路已经跑通接下来打开 12306 App 或网页端完成支付即可。整个过程从登录到下单控制在 15 秒内比手动刷新要高效得多。5.4 验证运行效果的方法跑通之后不要急着等放票先做两件事验证脚本的准确性。第一用一个已经确认无票的冷门车次跑一轮确认脚本不会误报“有票”第二用一个有余票的短途车次验证下单链路能走到支付页。我一般会选一个早上七点发车的短途车次做测试那类票通常不会秒光足够验证完整流程。验证完再切回真正的目标车次和日期这样心里有底。6. 二次开发从单次抢票到多车次自动监控这个项目最大的改造空间在于把“手动指定一个日期和一个车次”变成“多车次多日期的自动监控任务”。# 改造思路用任务队列替代单次查询 tasks [ {date: 2025-01-28, trains: [G1, G3], seat_type: 1}, {date: 2025-01-29, trains: [G5, G7], seat_type: 2}, ] def run_scheduler(tasks, session): while True: for task in tasks: for train_no in task[trains]: result check_and_submit(session, task, train_no) if result: notify_user(result) # 接入钉钉或 Server酱通知 time.sleep(random.uniform(1, 3))再把查票、下单、通知拆成独立函数配合系统 crontab 或定时任务就能做到早上放票时自动循环监控所有备选车次。通知模块可以用requests.post往钉钉机器人 Webhook 发消息把车次、余票、下单状态拼成一条文本推送到手机上比盯着命令行守着实在得多。验证方法很简单先用一个已经无票的车次跑一轮确认脚本不会误报再找一个有余票的冷门车次验证下单链路能走到支付页。我从那次春运之后每次改脚本都会先跑一遍“无票车次”用例再上真战场因为有一年我改坏了字段映射脚本高高兴兴“抢到”了十几次根本不存在的票人还在电脑前傻等支付链接。从那以后任何改动都强制走一遍这套验证流程花不了十分钟但能省下一整晚的焦虑。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站