
简介针对12306购票高峰期的自动抢票脚本面向有Python基础、希望提升购票效率或研究自动化抢票实现的开发者。资源以Python源码为主包含61个py脚本、48个pyc编译文件另附5张图片、2个H5模型文件及Dockerfile可用于验证码识别、CDN资源筛选与容器化部署。全套共134个文件压缩包62.84MB已有440人学习下载。内容涵盖网络请求处理、登录验证、票务查询到下单支付等环节模型文件支持验证码识别Docker配置便于快速搭建运行环境适合作为自动化脚本设计与反爬对抗的实战参考。使用时需注意与12306服务条款的合规性。 年前回家那趟车我盯着12306的“余票0张”盯了三天手动刷新到手指发酸最后还是靠候补才捡到一张凌晨的站票。也是从那次之后我决定自己写一个辅助购票的工具也就是这个项目qiangpiao12306。简单来说qiangpiao12306是一个围绕12306官网查询、购票流程开发的小脚本核心能力是自动登录、余票查询、车次筛选和订单提交把反复刷新页面、盯余票、选车次这些机械操作交给程序人只需要在关键的验证码环节出手确认。这篇文章把我从零开始拆解、实现、调试这个脚本的过程整理出来包括接口交互思路、核心代码逻辑、参数含义以及几个典型坑适合对Python爬虫、接口调试有一定基础同时经常为抢票发愁的朋友参考。1. 项目定位与需求拆解1.1 12306购票的痛点在哪里先说痛点。春运、小长假这些高峰期的票放票那一刻基本是秒空的原因也不难理解热门线路的票额是固定的而同一时间涌入的请求量太大。12306官方的售票策略偏向于“先到先得”所以手速和网络延迟就成了决定性因素。手动购票最大的问题是效率低。打开APP、查车次、切换日期、看余票、点进去提交再填乘客、排队一套流程走下来最少也要一两分钟。等你操作完热门车次的票早就没了。还有一个隐性问题是疲劳操作容易出错比如选错日期、选错乘车人甚至眼睁睁看到余票从“有”变“无”的一瞬间手还没点下去。脚本解决的就是这个“刷新-查询-筛选-提交”的低效循环。它可以在放票前几秒开始高频轮询余票接口一旦检测到目标车次有票就自动进入提交流程比人手快得多。1.2 抢票脚本的核心能力边界不过先给这个脚本的能力边界划清楚。它不是一个“万能抢票机”也做不到在没票的情况下凭空变出票来。它做的是两件事一是把查询和检测余票这件事做到高频、自动化二是把从“发现有票”到“订单提交成功”这段流程尽量缩短。我设计的思路是“半自动”。登录需要人工扫码或处理验证码提交订单前需要人工看一眼确认乘车人信息剩下的步骤交给代码。这样做既规避了验证码识别的灰色地带也避免脚本滥用导致账号异常。这里要说句实在话12306官方现在有候补购票功能而且候补的成功率其实很高高峰期很多票额在放票瞬间就会被候补队列消化掉留给普通购票窗口的票并不多。所以qiangpiao12306的定位是“候补之外的辅助工具”适合盯着余票、等退票、放票后再捡漏的场景单纯指望它绕过候补机制不现实。2. 技术路线与原理分析2.1 为什么选择Python加requests方案最初我考虑过两个技术路线一是用Selenium这样的浏览器自动化框架直接驱动真实浏览器去操作12306页面二是用requests按接口调用。Selenium方案的优点是还原了真实用户操作网页上的JS逻辑、验证码、动态加载通通不用操心浏览器会自己处理。但缺点也很明显启动浏览器实例非常占内存高并发模拟不现实而且操作速度受页面渲染速度限制放票高峰期反而可能因为页面加载延迟错失机会。requests方案的优点是轻量、快、可控。直接调用12306的HTTPS接口请求体小、响应快轮询频率可以做到很高。缺点是需要自己维护会话状态、处理加密参数、解析复杂响应开发成本更高。实测下来在正常网络环境下requests直接请求查询接口的响应时间可以控制在几百毫秒内比浏览器方案快一个量级。所以最终我选了requests。还有一个考虑是12306的接口虽然没公开文档但实际上有大量前端运行时依赖的接口结构相对稳定只要跟着网页端的请求行为模拟就行。2.2 与12306服务端交互的基本流程整个脚本的交互流程可以拆成这么几条主线初始化会话。创建requests.Session保持Cookie和会话状态。登录。这个过程涉及用户名密码接口、验证码校验接口当前版本大多支持扫码登录我选择保留手动扫码入口。跳转购票页并获取用户信息。登录成功后请求购票页相关接口拿到当前登录用户信息、常用联系人列表。查询余票。调用余票查询接口传入日期、出发站、到达站编码返回车次数组解析后筛选目标车次。提交订单。选中车次后调用订单预提交接口携带车次加密串、乘客信息、席别信息等待排队接口返回队列状态。获取订单号并完成支付引导。订单确认后脚本返回一个待支付订单号由用户到App或官网完成支付。这几个步骤里最关键的链路是“查询余票”和“提交订单”。前者决定能不能实时发现票后者决定能不能在发现票之后顺利抢下来。2.3 数据交互的关键字段与参数说明做脚本绕不开参数解析。这里把几个核心字段列出来理解这些字段的意思后面调试代码能少走很多弯路。字段名来源接口含义实际用途station_train_code余票查询车次号如G1234车次筛选leftTicket余票查询余票综合信息串包含各席别分布判断指定席别是否有票train_location余票查询车次定位串提交订单时回传校验车次唯一性secretStr余票查询车次加密串用于订单提交提交订单必传参数key_check_isChange订单预提交会话校验串防止票据串号REPEAT_SUBMIT_TOKEN前端页面购票令牌表单防重提交订单时带上purpose_codes余票查询购票类型ADULT为成人票查询成人票余票第一次接触这些字段的人可能会觉得混乱但只要用浏览器开发者工具抓一次包把这些参数和请求对应上逻辑就通了。余票查询接口返回的是一个压缩过的JSON字符串需要用urllib.parse.unquote先解码再按|分隔来解析这部分代码后面会细讲。3. 核心模块实现与实操指南3.1 开发环境准备开发环境很简单Python 3.8以上版本就可以。依赖库只有三个安装命令如下pip install requests prettytable Pillow为什么选这几个库requests核心HTTP请求库处理会话、Cookie、SSL。prettytable把查到的车次余票信息格式化输出到终端纯为了看着舒服。Pillow用于展示验证码图片。虽然现在我只保留扫码登录但备着没坏处。项目结构我用的是单文件加配置的方式config.py放账号、出行参数main.py放主流程逻辑。小工具不建议一上来就拆一堆模块脚本本身没复杂到那个程度拆得太散反而影响迭代效率。3.2 登录与会话保持的实现登录这一块现在12306做得比较严密码登录经常要过滑块验证所以我最终保留了扫码登录方案。扫码这一步本质上是轮询一个二维码状态接口检测到扫码并确认后会话自动建立。会话保持的核心就是requests.Session。登录成功后Cookie存在Session对象里后续所有操作都会带上登录态。核心逻辑示意如下import requests import re session requests.Session() session.headers.update({ User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 }) def login_by_qrcode(): # 获取二维码UUID uuid_resp session.post(https://kyfw.12306.cn/passport/web/getQRCodeUUID) data uuid_resp.json() if data[result_code] ! 0: raise RuntimeError(获取二维码UUID失败) uuid data[uuid] # 这里将二维码图片链接展示给用户扫码 qr_url fhttps://kyfw.12306.cn/passport/web/getQRCodeImg?uuid{uuid} # 展示二维码等待用户扫码并确认 # 轮询扫码状态 while True: check_resp session.post( https://kyfw.12306.cn/passport/web/checkQRCode, data{uuid: uuid}, ) status check_resp.json() # 二维码状态码 2: 已扫码未确认3: 已确认 if status[result_code] 2: print(已扫码请在手机上确认) elif status[result_code] 3: print(扫码确认成功) break time.sleep(2) # 扫码确认后还需要调用一次登录确认接口 login_resp session.post( https://kyfw.12306.cn/passport/web/loginByQrCode, data{uuid: uuid}, ) token login_resp.json()[uamtk] # 校验并绑定会话 session.post(https://kyfw.12306.cn/otn/uamauthclient, data{tk: token})注意一个细节完整扫码登录之后还要把uamtk绑定到otn的会话里这一步漏了的话后面请求购票接口会提示未登录。3.3 余票查询与车次筛选余票查询是脚本的核心模块。12306的查询接口路径大致如下GET https://kyfw.12306.cn/otn/leftTicket/queryG?leftTicketDTO.train_date2025-01-28leftTicketDTO.from_stationGZQleftTicketDTO.to_stationBJPpurpose_codesADULT这里from_station和to_station都是电报码不是中文站名。电报码可以从12306的station_name.js里获取这个文件是公开资源里面维护了站名和电报码的映射关系。解析逻辑很简单按分割后提取。我自己写了个简单函数做缓存import re import requests def load_station_map(): resp requests.get(https://kyfw.12306.cn/otn/resources/js/framework/station_name.js) text resp.text station_pattern re.compile(r\|([\u4e00-\u9fa5])\|([A-Z])) station_map dict(station_pattern.findall(text)) return station_map查询结果的解析需要一点耐心。接口返回的data.result是一个字符串列表每个元素对应一趟车次格式是把多个字段用|拼接。其中secretStr和leftTicket这两个字段在拆包后是URL编码状态需要解码后才能用。下面是余票解析的核心代码from urllib.parse import unquote def parse_trains(result_list): trains [] for raw in result_list: parts raw.split(|) # 各字段索引以实际为准这里列出关键位置 train_no parts[3] # 车次号 from_station parts[6] to_station parts[7] start_time parts[8] arrive_time parts[9] left_ticket parts[30] # 需要 URL 解码 secret_str parts[0] # 需要 URL 解码 left_ticket unquote(left_ticket) secret_str unquote(secret_str) trains.append({ train_no: train_no, start_time: start_time, arrive_time: arrive_time, left_ticket: left_ticket, secret_str: secret_str, }) return trainsleftTicket解码后的字符串类似1P0O9000这种格式不同席别的余票数量就分布在这些数字里。具体哪个索引对应二等座、一等座、硬卧需要对着12306网页端实际显示情况逐一确认这是一个排查耐心活但做一次之后就能稳定复用。在筛选车次这一块我建议不要只盯一趟车而是把同线路的所有车次都拉下来按照“有票优先、时间优先”的规则排序一旦目标车次有票立即触发后续提交逻辑。3.4 订单提交与购票流程订单提交是整个脚本里最复杂的一环。12306在这方面做了很多防机器人机制比如需要带REPEAT_SUBMIT_TOKEN、需要回传secretStr、train_location等。为了让流程更稳我在提交订单前把流程拆成两步先预提交再正式提交。预提交接口大致是def check_order_info(train_data, passenger_info): submit_url https://kyfw.12306.cn/otn/leftTicket/submitOrderRequest payload { secretStr: train_data[secret_str], train_date: train_data[train_date], back_train_date: train_data[train_date], tour_flag: dc, purpose_codes: ADULT, query_from_station_name: train_data[from_station_name], query_to_station_name: train_data[to_station_name], undefined: , } resp session.post(submit_url, datapayload) return resp.json()预提交成功后会进入一个排队流程此时需要请求检查订单信息的接口把乘客信息、席别都确认一遍拿到key_check_isChange和REPEAT_SUBMIT_TOKEN。真正提交订单时需要构造一个比较复杂的表单里面包含乘客ID、席别编码、票种编码等。不同席别对应的编码不同二等座是M一等座是O硬卧是3软卧是4这个可以从12306的字典接口里拿到。代码写起来不复杂但字段多、容易错漏我建议在实际开发时对照12306官网提交订单时抓包的数据来一一比对。提交订单后接口会返回一个排队信息隔几秒轮询一次队列状态等队列结束后能拿到一个orderId到这一步脚本的任务就算完成了后续支付直接交给用户在手机App上完成。4. 关键细节与常见问题排查4.1 高频问题排查列表开发调试过程中踩了不少坑这里整理一个速查表基本覆盖了最常见的几类问题。问题现象可能原因解决方案登录成功后接口仍提示未登录uamtk未绑定到otn会话确保调用了uamauthclient接口查询接口返回HTTP 302会话过期或Cookie异常重新走扫码登录流程leftTicket解析出来全是数字席别对不上索引位判断错误对照官网车票展示窗口逐一确认字段位提交订单提示secretStr过期查询到提交的时间间隔太长把提交逻辑和查询逻辑尽量串行化减少间隔频繁查询被限制请求频率过高触发风控每次查询间隔不低于2秒加随机抖动提示REPEAT_SUBMIT_TOKEN缺失未先请求购票页获取令牌提交前先GET一次购票页提取表单令牌4.2 实测中的参数调优建议说一下我实测后的几个调优心得。轮询间隔是第一个要调的参数。我一开始设成0.5秒一次跑了几分钟后接口就开始超时后来改成2到3秒一次加一个0到1秒的随机延时稳定了很多。抢票本质上是拼概率和时间窗口不是请求越频繁越好频率过高反而容易被风控封掉。第二个是车次筛选策略。不要只盯一辆车建议把同一条线路的车次都拉下来按优先级排序。我的排序逻辑是先看有没有直达车次有票有票就提交没有直达就看换乘方案如果目的地是大站可以把终点放宽到同城或周边站再查一次这个捡漏概率不小。第三个是启动时机。如果目标车次是放票当天发售脚本应该在放票时间点之前启动提前完成登录、查询一次车次列表、预热会话然后在放票时间点前10秒开始高频轮询。如果是捡漏场景不需要高频每5到10秒查一次就够重点在于能持续跑几个小时。4.3 合规与性能的平衡建议最后强调一个原则性问题。这类脚本可以用于个人学习、辅助自己购票但我不建议把它做成商业化工具更不要用它批量注册账号、恶意占票、倒卖车票这既违反12306用户协议也可能涉及法律风险。在实际使用中我会把查询频率控制在低频范围避免对服务器造成压力。同时12306官方在很多热门线路上有候补购票功能高峰期会优先处理候补队列脚本能挤进去的概率其实没有想象中那么高所以理性用法是候补队列照常排队脚本作为额外的余票监控工具两边同时进行谁先成功算谁的。开发这个脚本的过程中最深的体会是稳定比速度更重要。一个能平稳跑上12个小时的监控脚本远比只追求放票那一秒的极速请求更有实用价值。网上流传的“12306截图生成器”这类工具最多只能做出看起来像车票的图片拿去当购票凭证或者传播都毫无意义建议别浪费时间。回到项目本身qiangpiao12306目前还在小范围自用后面如果有精力我计划把它改造成一个命令行工具只需要通过配置文件指定出发站、到达站和日期段就能在后台自动监控余票等验证码识别方案成熟了再考虑把登录也完全自动化。如果你准备自己动手写一个建议从余票查询接口开始先把数据拿到手后面每一步都是在这个基础上叠加遇到问题欢迎随时交流。本文还有配套的精品资源点击获取