
简介这是一份面向Python初学者与自动化爱好者的12306火车票购票自动化脚本针对中秋、国庆、春节等出行高峰抢票难的问题提供可在电脑端运行的抢票辅助方案。资源以Python源码为核心配合IDE工程配置与项目元数据文件方便直接导入开发环境阅读与调试。压缩包为rar格式共8个文件整体约6KB包含1个py主脚本、4个xml工程配置、1个iml模块描述、1个name标识及1个gitignore忽略规则结构精简适合快速理解脚本逻辑与二次修改。目前已有2168人学习下载说明该方案在抢票场景中具有一定参考价值。读者可从中获取完整的抢票脚本源码、工程目录组织方式以及自动化购票的基本实现思路便于结合自身需求调整参数、排查运行问题并在此基础上扩展验证码识别、多车次监控等功能。1. 抢票脚本的真实边界为什么你写的 Python 脚本总在提交订单那一步翻车每年春运前后后台总有人问我同一个问题用 Python 写个自动抢火车票脚本到底行不行我的回答从来不是“行”或“不行”而是先反问一句——你卡在哪一步绝大多数人的答案惊人地一致查询车次没问题提交订单就报错或者干脆被踢回登录页。这不是代码写得烂而是没搞懂这套系统的真实交互模型。所谓“12306 自动抢火车票脚本”本质是用程序模拟浏览器完成「登录 → 查票 → 提交订单 → 排队 → 支付」这条链路。它解决的是人工刷新慢、手速不够、余票一闪而过的问题适合懂一点 Python、愿意折腾请求库和会话保持的开发者。但它的天花板也很明显你面对的不是一个静态网页而是一套带风控、带时间戳校验、带动态令牌的分布式系统。脚本能帮你把查询频率拉到秒级但能不能抢到取决于你对会话、令牌和提交时机的理解深度。下面我把这套东西拆开讲能复现的部分给代码不能碰的边界说清楚。2. 从登录到查票会话保持与请求构造的最小闭环2.1 为什么直接用 requests 裸请求会被识别很多人第一步就翻车用requests.get直接怼查询接口返回一堆乱码或者直接 403。原因在于这套系统不是靠单一 Cookie 认人而是靠一整套会话上下文——包括但不限于初始页面下发的会话标识、动态生成的令牌、以及请求头里的浏览器指纹字段。你少带一个风控就把你当机器人。常见做法是先用一个Session对象访问初始页面把服务端下发的 Cookie 全部收进会话罐子里后续所有请求都复用这个会话。这一步不做后面全是白搭。import requests # 创建会话对象后续所有请求共用这套 Cookie session requests.Session() # 模拟浏览器的基础请求头缺一不可 headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36, Accept: text/html,application/xhtmlxml,application/xml;q0.9,*/*;q0.8, Accept-Language: zh-CN,zh;q0.9, Connection: keep-alive, } # 第一步访问初始页面让服务端把会话 Cookie 种进来 init_resp session.get( https://example-railway-init-page, headersheaders, timeout10 ) print(初始页面状态码:, init_resp.status_code) print(当前会话 Cookie:, session.cookies.get_dict())这段代码的逻辑很简单先建立一个有状态的会话再用接近真实浏览器的请求头去访问入口页面。参数上User-Agent必须和后续请求保持一致不能这次用 Chrome 下次用 Firefox否则会话上下文对不上。timeout建议设 10 秒以内抢票场景下等太久没意义。2.2 查询接口的参数怎么拼才不会被拒登录之后的核心动作是查票。查票接口通常需要几个关键参数出发站、到达站、出发日期、以及一个从页面里提取出来的动态令牌。这个令牌有时藏在 HTML 的 script 标签里有时是单独的接口下发不同时期形态不一样但思路固定——先拿令牌再带令牌查票。import re # 假设登录已完成现在从某个页面提取动态令牌 page_resp session.get( https://example-railway-ticket-page, headersheaders, timeout10 ) # 用正则从页面源码里抠出令牌具体字段名以实际页面为准 token_match re.search(rglobalRepeatSubmitToken\s*\s*([^]), page_resp.text) if not token_match: raise RuntimeError(没找到动态令牌页面结构可能变了) repeat_token token_match.group(1) print(提取到的令牌:, repeat_token[:16], ...) # 构造查票请求参数 query_params { leftTicketDTO.train_date: 2025-01-20, # 出发日期 leftTicketDTO.from_station: BJP, # 出发站电报码 leftTicketDTO.to_station: SHH, # 到达站电报码 purpose_codes: ADULT, # 成人票 } # 带上令牌和会话去查票 query_resp session.get( https://example-railway-query-api, headersheaders, paramsquery_params, cookies{RAIL_EXPIRATION: repeat_token}, # 令牌有时放 Cookie 有时放参数 timeout10 ) print(查票响应长度:, len(query_resp.text))这里的关键参数是车站电报码不是中文站名。很多人直接传“北京”“上海”接口不认。电报码需要提前从车站列表接口拉一份映射表存本地查的时候做一次转换。令牌的提取正则只是示例实际字段名会变思路是「先抓页面、再抠字段、后带请求」。如果查票返回空或者报错优先检查令牌是否过期、Cookie 是否完整、请求头是否和登录时一致。2.3 余票数据的解析与车次筛选查票接口返回的通常是一段用特殊符号分隔的字符串不是标准 JSON。你需要按分隔符切开再按固定下标取字段。这一步没有捷径只能对着返回样本数位置。# 假设 query_resp.text 里有一个 result 数组每个元素是竖线分隔的字符串 raw_result query_resp.json().get(data, {}).get(result, []) for item in raw_result: fields item.split(|) # 下标位置以实际返回为准这里只是示意 train_no fields[3] # 车次编号 from_code fields[6] # 出发站电报码 to_code fields[7] # 到达站电报码 depart_time fields[8] # 出发时间 arrive_time fields[9] # 到达时间 secret_str fields[0] # 提交订单时要用的密钥串 # 筛选条件出发时间在早上六点到晚上十点之间 if 06:00 depart_time 22:00: print(f车次 {train_no} {depart_time}-{arrive_time} 密钥 {secret_str[:12]}...)解析逻辑说明每个字段的下标是固定的但不同时期接口可能增删字段所以拿到返回后先用一条样本打印全部下标确认位置再写死。secret_str是提交订单时的关键凭证查到了先存下来后面提交要用。筛选条件按自己需求改比如只看高铁、只看卧铺都是在解析后加判断。提示查票频率不要拉到毫秒级常见做法是 3 到 5 秒一次带随机抖动。频率过高会触发风控反而什么都查不到。3. 提交订单与排队令牌、时间戳与提交时机的配合3.1 提交订单需要凑齐哪几个凭证查到票只是第一步真正难的是提交订单。这一步需要凑齐几个东西查票时拿到的密钥串、登录后下发的令牌、以及一个和当前时间绑定的时间戳。少一个都会被拒。import time # 假设前面已经拿到了 secret_str 和 repeat_token # 提交订单的请求体 order_payload { secretStr: secret_str, # 查票时拿到的密钥串 train_date: 2025-01-20, # 出发日期 backTrainDate: , # 返程日期单程留空 tour_flag: dc, # 单程标识 purpose_codes: ADULT, # 成人票 queryTrainInfo: , # 查询信息通常留空 REPEAT_SUBMIT_TOKEN: repeat_token, # 登录后下发的令牌 } # 提交订单 order_resp session.post( https://example-railway-submit-api, headers{**headers, Content-Type: application/x-www-form-urlencoded}, dataorder_payload, timeout10 ) print(提交订单响应:, order_resp.text[:200])参数说明secretStr是查票结果里带的每次查询都会变不能复用。REPEAT_SUBMIT_TOKEN是登录后拿到的整个会话周期内有效。tour_flag单程是dc往返是wc别填错。提交后如果返回里带订单号说明进入排队了如果返回错误码看错误信息是令牌过期还是余票不足。3.2 排队与确认为什么提交成功不等于抢到提交订单成功只是进入排队队列不代表票已经锁定。系统会返回一个排队状态你需要轮询确认接口直到状态变成「订单已确认」或者「排队失败」。import time # 假设提交后拿到了一个 queue_id queue_id example-queue-id for attempt in range(20): confirm_resp session.get( https://example-railway-confirm-api, headersheaders, params{queueId: queue_id}, timeout10 ) status confirm_resp.json().get(status) print(f第 {attempt1} 次轮询状态: {status}) if status CONFIRMED: print(订单已确认去支付) break elif status FAILED: print(排队失败余票被别人抢了) break time.sleep(1.5) # 轮询间隔别太短1 到 2 秒比较稳轮询逻辑说明间隔太短会给服务端压力也容易触发风控太长又可能错过确认窗口。1 到 2 秒是常见做法。状态字段名以实际返回为准有的叫status有的叫orderStatus。如果轮询多次仍是「排队中」说明队列拥堵继续等或者重新查票。3.3 多车次并发查询的线程池写法单线程查一个车次太慢常见做法是用线程池并发查多个车次谁先有票就提交谁。但要注意提交订单本身最好串行避免多个线程同时提交导致令牌冲突。from concurrent.futures import ThreadPoolExecutor, as_completed def query_train(train_code): 查询单个车次的余票返回有票的车次信息 params { leftTicketDTO.train_date: 2025-01-20, leftTicketDTO.from_station: BJP, leftTicketDTO.to_station: SHH, purpose_codes: ADULT, } resp session.get( https://example-railway-query-api, headersheaders, paramsparams, timeout10 ) # 解析逻辑省略返回有票的车次 return parse_ticket(resp.text, train_code) # 用线程池并发查询最多 5 个线程 train_list [G101, G102, G103, G104, G105] with ThreadPoolExecutor(max_workers5) as executor: futures {executor.submit(query_train, t): t for t in train_list} for future in as_completed(futures): result future.result() if result: print(发现余票:, result) # 这里触发提交订单注意加锁或串行处理线程池参数说明max_workers建议不超过 5再多容易触发风控。每个线程共用同一个session对象Cookie 是共享的但要注意requests.Session本身不是完全线程安全的高并发下可能出现 Cookie 错乱。稳妥做法是每个线程独立会话或者用锁保护提交动作。注意并发查询可以并发提交订单是大忌。多个线程同时提交同一个令牌服务端会判定为异常请求直接拒绝。4. 避坑与排查抢票脚本最常见的 5 个翻车现场4.1 登录后查票一直返回未登录现象登录接口返回成功但紧接着查票提示「请先登录」。 原因登录成功后服务端会重定向或者下发新的 Cookie如果Session没有自动跟随重定向或者手动覆盖了 Cookie会话就断了。 解决登录请求加allow_redirectsTrue登录后打印session.cookies.get_dict()确认关键 Cookie 存在。不要手动设置Cookie请求头交给Session管理。4.2 令牌提取正则突然匹配不到现象昨天还能跑的脚本今天报「没找到动态令牌」。 原因页面结构变了令牌字段名或者所在位置调整了。 解决不要写死正则先请求页面把 HTML 存下来用浏览器开发者工具搜关键字确认新字段名后再改正则。更稳的做法是找令牌下发的独立接口直接拿 JSON比抠 HTML 可靠。4.3 提交订单返回「操作过于频繁」现象查票正常一提交就提示频繁。 原因提交间隔太短或者同一会话短时间内多次提交。 解决提交失败后至少等 3 到 5 秒再重试重试前重新查一次票拿新的密钥串。不要用同一个密钥串反复提交它是一次性的。4.4 余票显示有但提交时提示「余票不足」现象查票结果里明明有票提交却说没了。 原因查票和提交之间有时间差票被抢走了。或者查到的票是「候补」状态不是直接可购。 解决查票后立刻提交中间不要做多余操作。解析时区分「有票」和「候补」只对真正有票的车次提交。候补是另一套接口不要混用。4.5 脚本跑一晚上什么都没抢到现象日志显示一直在查但从来没提交成功。 原因查询频率太低或者筛选条件太窄或者提交时机总是慢半拍。 解决把查询间隔压到 3 秒左右带随机抖动筛选条件放宽一档提交动作放在查到票后的第一个循环里立刻执行。另外确认系统时间是否准确时间戳偏差太大会导致请求被拒。5. 把脚本跑稳的几个进阶习惯5.1 用配置文件管理车站码和日期硬编码车站码和日期是新手最容易犯的错。改一次日期要翻遍代码容易漏改。常见做法是抽一个config.yaml或者config.json把出发站、到达站、日期、车次列表、查询间隔全放进去。import json with open(config.json, r, encodingutf-8) as f: config json.load(f) train_date config[train_date] from_station config[from_station] to_station config[to_station] query_interval config.get(query_interval, 3)配置文件里还可以放车站码映射表查票前做一次转换。这样换日期、换车站只改配置不动代码。5.2 日志要记到文件别只 print抢票脚本往往跑在后台print 到终端看不到。用logging模块把关键节点记到文件登录成功、令牌提取、查票结果、提交响应、排队状态。出问题时翻日志比猜快得多。import logging logging.basicConfig( levellogging.INFO, format%(asctime)s [%(levelname)s] %(message)s, handlers[ logging.FileHandler(ticket.log, encodingutf-8), logging.StreamHandler() ] ) logging.info(开始查询车次 %s, train_code) logging.warning(令牌提取失败准备重试)日志级别用INFO记正常流程WARNING记异常但可恢复的情况ERROR记致命错误。文件按天切分更好避免单个日志文件过大。5.3 失败重试要带退避不要死循环网络抖动、令牌过期、服务端限流都会导致请求失败。无脑while True重试只会让情况更糟。常见做法是带指数退避的重试第一次等 1 秒第二次等 2 秒第三次等 4 秒最多重试 3 到 5 次。import time def retry_request(func, max_retries3, base_delay1): 带指数退避的请求重试 for attempt in range(max_retries): try: return func() except Exception as e: if attempt max_retries - 1: raise delay base_delay * (2 ** attempt) print(f请求失败: {e}{delay} 秒后重试) time.sleep(delay)这个重试函数可以包住查票、提交、轮询各个动作。注意提交订单的重试要重新拿密钥串不能直接重发原请求。5.4 验证脚本是否真的在工作跑起来不等于在工作。验证方法很简单手动在浏览器里查一次票记下有余票的车次和时间然后看脚本日志里有没有查到同样的结果。如果浏览器有票脚本查不到说明请求参数或者令牌有问题如果脚本查到了但提交失败说明提交环节的凭证不对。逐段对比比盲目改代码有效。我自己的习惯是每次改完脚本先用一个已知有票的车次跑一遍完整流程确认从查询到提交都能走通再换成目标车次。这个习惯帮我省了很多「跑了一晚上发现根本没提交」的后悔药。抢票脚本这东西稳定比快更重要能跑通一条完整链路比并发一百个车次但每个都半路翻车强得多。希望帮到你。本文还有配套的精品资源点击获取