Python实战:模拟HTTP请求与多线程监控实现12306自动化抢票
1. 项目概述为什么我们需要自己动手写一个12306抢票程序又到了一年一度的春运返程高峰相信很多朋友都经历过在12306官网或APP上反复刷新却眼睁睁看着车票“秒没”的绝望。那种感觉就像在双十一零点抢购热门商品网络稍微卡顿一下购物车就空了。作为一个常年需要跨省通勤的程序员我对此深有体会。市面上确实有不少抢票软件从浏览器插件到独立的桌面应用但它们要么需要付费加速要么存在隐私泄露的风险更关键的是在票源极度紧张的时候这些工具的“公平性”和“可控性”往往不尽如人意。于是自己动手用Python写一个抢票程序就成了一个非常实际且富有极客精神的选择。这不仅仅是为了抢到一张回家的票更是一个深入理解网络请求、自动化流程和反爬策略的绝佳实战项目。今天分享的这个“12306抢票源程序Python版”其核心思路就是模拟一个真实用户的购票行为通过程序自动化地完成查询、登录、提交订单等一系列操作并且通过“分流”策略提高抢票的成功率。所谓“分流”可以理解为多线程或分布式查询避免单一请求被限制或错过放票时机。这个项目适合谁呢首先当然是需要抢票的朋友尤其是对技术有好奇心、愿意折腾的开发者。其次对于正在学习Python网络爬虫、自动化测试Selenium/Requests的同学来说这是一个复杂度适中、综合性极强的练手项目。你将接触到Cookie管理、验证码识别或绕过、JSON数据解析、多线程并发等核心技能。当然我必须强调任何自动化工具的使用都应遵守相关服务条款本分享旨在技术交流与学习请合理使用。2. 核心思路与技术选型从浏览器操作到直接API请求在动手之前我们需要明确技术路线。抢票程序本质上是一个“客户端”它需要与12306的服务器进行交互。通常有两条主流路径路径一模拟浏览器操作如使用Selenium这种方法相对“笨重”但直观。它通过自动化工具如Selenium WebDriver控制一个真实的浏览器Chrome、Firefox模拟人的点击、输入等操作。优点是不需要深入分析复杂的网络请求绕过一些前端加密逻辑对于初学者理解整个流程很有帮助。但缺点也非常明显速度慢、资源占用高每个实例都是一个完整的浏览器进程在需要高频查询和快速提交的抢票场景中性能是致命短板。路径二直接模拟HTTP API请求如使用Requests库这是高性能抢票程序的必然选择。其原理是直接分析12306网站或APP在购票过程中发生的所有网络请求通过浏览器开发者工具的Network面板找到关键的查询、登录、下单等接口然后用Python的Requests库直接发送这些HTTP请求并处理返回的JSON数据。这种方式完全剥离了浏览器渲染的 overhead速度极快可以轻松实现多线程、分布式并发。我们本次分享的“源程序”正是采用这种方案。技术栈的选型基于这条高效路径核心请求库Requests。这是Python中处理HTTP请求的事实标准简单易用功能强大。并发处理concurrent.futures 或 threading。为了同时监控多个车次、多个日期的余票必须使用多线程或线程池。concurrent.futures.ThreadPoolExecutor提供了高级的线程池接口管理起来更方便。数据解析JSON / re。服务器返回的数据基本都是JSON格式直接用Python内置的json模块解析即可。部分页面内容可能用到正则表达式re。持久化与配置JSON文件 / Pickle。用户配置如出发地、目的地、乘车人信息和登录状态Cookie需要保存到本地文件通常使用JSON格式存储配置用Pickle或直接保存CookieJar来维持登录会话。验证码处理第三方OCR服务或手动。12306的登录验证码是著名的“八选二”选择图中所有xx物体。完全自动识别难度大、成本高。在开源项目中常见的做法是1) 弹出图片让用户手动识别并点击2) 集成打码平台如超级鹰、图鉴的API进行自动识别。为了稳定性和学习目的我们的程序可以先采用手动识别方式。注意直接请求API的方式需要时刻关注12306的接口更新。铁路部门会不定期调整接口参数或加密方式因此程序可能需要维护。这也是理解网络协议动态性的好机会。3. 环境准备与依赖安装搭建稳定的运行基础工欲善其事必先利其器。一个独立的、干净的Python环境是项目稳定的前提。我强烈推荐使用conda或venv创建虚拟环境避免包版本冲突。# 使用 conda 创建环境假设你安装了Anaconda或Miniconda conda create -n 12306_ticket python3.8 conda activate 12306_ticket # 或者使用 Python 内置的 venv python -m venv venv_12306 # Windows 激活 venv_12306\Scripts\activate # Linux/Mac 激活 source venv_12306/bin/activate环境激活后安装核心依赖库。除了Requests我们可能还需要一些辅助库。pip install requests # 如果需要更友好的命令行交互可以安装 rich 或 colorama pip install rich # 如果需要将Cookie保存为文件可能会用到 browsercookie谨慎使用学习原理为主但更推荐手动管理。 # pip install browsercookie这里特别说明一下requests库的版本。建议使用较新的稳定版如2.28但并非越新越好。关键是要确保你的代码与库版本兼容。一个常见的“坑”是某些教程里使用的requests旧版API在新版中可能已被弃用。例如session的某些参数设置方式。因此在开始编码前最好在交互式环境里简单测试一下import requests; print(requests.__version__)。接下来我们需要一个代码编辑器。VSCode或PyCharm都是绝佳选择。以VSCode为例确保安装了Python扩展并将解释器路径设置为刚才创建的虚拟环境中的Python。这样你在编辑器里运行和调试代码时使用的就是隔离环境中的包。实操心得我习惯在项目根目录下创建一个requirements.txt文件记录所有依赖及其版本。这样在另一台机器上部署时一句pip install -r requirements.txt就能还原环境。对于这个项目初始的requirements.txt可以很简单requests2.28.0 rich13.0.04. 关键环节一登录与会话维持Cookie管理登录是抢票程序的第一道也是最复杂的一道关卡。12306的登录流程经历了多次升级安全性越来越高。其核心步骤包括获取登录页面初始化Cookie首先用GET请求访问登录页面服务器会返回一个包含关键Cookie如RAIL_DEVICEID,RAIL_EXPIRATION的响应。我们的程序需要保存这个session。获取验证码Captcha请求验证码图片接口。目前12306的验证码通常是“八选二”的图片点选形式。接口会返回一个图片和一组坐标对应的答案但答案需要我们自己识别。校验验证码将用户识别出的坐标例如[‘40,40’, ‘120,80’]提交到验证码校验接口。校验通过后会返回一个result_code如’0’表示成功和一个用于后续登录的randCode。执行登录带着用户名、密码、上一步得到的randCode以及其他一些隐藏参数如appid可以从页面源码中解析提交到登录接口。成功后会返回一个包含uamtk等令牌的响应。权限认证UAMTK拿着登录返回的uamtk再去请求一个auth/uamtk接口获取新的tk。客户端权限验证最后用这个tk去访问otn/login/conf等接口完成最终的客户端鉴权。至此登录流程才全部走完session里包含了完整的、已登录状态的Cookie。整个过程涉及多个接口跳转和状态传递任何一个环节的参数缺失或错误都会导致失败。在编程实现时我们必须严格遵循浏览器中观察到的请求顺序和参数格式。核心代码结构示例简化版import requests from PIL import Image import json class TicketGrabber: def __init__(self): self.session requests.Session() # 设置请求头模拟浏览器 self.session.headers.update({ User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ..., Referer: https://kyfw.12306.cn/otn/resources/login.html, }) self.username 你的用户名 self.password 你的密码 def get_captcha(self): 获取并显示验证码图片 captcha_url https://kyfw.12306.cn/passport/captcha/captcha-image64 # 这里可能需要添加特定的查询参数如login_siteEmoduleloginrandsjrand_{时间戳} resp self.session.get(captcha_url) if resp.status_code 200: # 假设接口返回的是base64编码的图片 img_data resp.json().get(image) # 解码并保存图片然后展示给用户 # ... (省略图片处理代码) # 提示用户输入坐标 coords_input input(请根据提示点击图片坐标格式如40,40,120,80) return coords_input.split(,) return None def check_captcha(self, coords): 校验验证码坐标 check_url https://kyfw.12306.cn/passport/captcha/captcha-check # 坐标需要转换为40,40,120,80这样的字符串 answer ,.join(coords) data { answer: answer, rand: sjrand, login_site: E } resp self.session.post(check_url, datadata) result resp.json() if result.get(result_code) 0: print(验证码校验成功) return True else: print(f验证码校验失败{result}) return False def login(self): 执行登录流程 # 1. 获取登录页初始化Cookie (通常在第一次请求时自动完成) # 2. 获取并校验验证码 coords self.get_captcha() if not coords or not self.check_captcha(coords): print(验证码处理失败登录终止) return False # 3. 提交登录表单 login_url https://kyfw.12306.cn/passport/web/login login_data { username: self.username, password: self.password, appid: otn, # 这个参数很重要需要从页面中获取 # 可能还有其他动态参数 } resp self.session.post(login_url, datalogin_data) login_result resp.json() if login_result.get(result_code) ! 0: print(f登录失败{login_result}) return False print(登录表单提交成功) # 4. 后续的uamtk和客户端验证步骤略 # ... return True if __name__ __main__: grabber TicketGrabber() if grabber.login(): print(登录成功会话已建立。) # 可以将会话Cookie保存到文件下次直接加载避免重复登录 # import pickle # with open(session.pkl, wb) as f: # pickle.dump(grabber.session.cookies, f)注意事项参数动态性上述代码中的URL和参数如appid可能随12306前端更新而变化。最可靠的方法是打开浏览器开发者工具F12在Network标签页下监控登录过程中的每一个请求逐一记录其URL、Method、Headers和Form Data。Cookie管理requests.Session()对象会自动处理Cookie。登录成功后这个session在后续的查询、下单请求中可以直接使用。建议将登录成功的session.cookies通过pickle保存到本地文件下次运行程序时直接加载可以避免频繁登录触发风控。验证码处理自动识别验证码成功率有限且可能违反规则。对于个人使用的脚本手动识别一次登录后长期保存Cookie是更稳妥的方案。如果必须自动化请研究12306的滑动验证码逻辑或使用可靠的打码平台但这会增加复杂度和成本。5. 关键环节二车票查询与余票监控登录成功后就进入了核心的查询环节。12306的余票查询接口是公开的但参数构造有一定讲究。我们需要构造一个查询请求指定出发站、到达站、日期、车次类型等。查询接口分析 通常查询接口URL类似于https://kyfw.12306.cn/otn/leftTicket/queryZ?leftTicketDTO.train_date2024-01-20leftTicketDTO.from_stationBJPleftTicketDTO.to_stationSHHpurpose_codesADULTtrain_date: 出发日期格式YYYY-MM-DD。from_station/to_station: 出发站和到达站的电报码不是中文站名这是一个关键点。例如北京南是BJP上海虹桥是AOH。我们需要一个站名到电报码的映射表。purpose_codes: 票种成人票为ADULT学生票为0X00。获取车站电报码 12306提供了一个车站信息接口https://kyfw.12306.cn/otn/resources/js/framework/station_name.js?station_version1.9235。返回的数据是一个大字符串格式如bjb|北京北|VAP|...。我们可以解析这个字符串构建一个{站名: 电报码}的字典并缓存到本地。解析查询结果 查询接口返回的是一个复杂的JSON。其中data字段下的result列表包含了所有车次的信息。每条信息是一个用|竖线分割的字符串各个位置代表了不同的信息如车次号、出发时间、到达时间、历时、各席别余票等。这个字符串的格式定义可以在网络找到例如索引3是车次8是出发时间9是到达时间31是商务座30是一等座29是二等座26是无座23是硬卧28是软卧等。但这个索引位置可能会变所以更稳健的方法是同时解析返回的JSON中的另一个字段map它定义了result中每个位置的名称。实现持续监控多线程/轮询 抢票的关键在于“快”和“持续”。我们需要写一个循环每隔很短的时间如0.5秒或1秒查询一次。同时为了监控多个日期或多个车次我们需要引入多线程。import time import threading from concurrent.futures import ThreadPoolExecutor, as_completed class QueryEngine: def __init__(self, session): self.session session self.station_map self.load_stations() # 加载站名-电报码映射 def query_tickets(self, from_city, to_city, train_date, purposeADULT): 单次查询 from_station self.station_map.get(from_city) to_station self.station_map.get(to_city) if not from_station or not to_station: print(f车站名错误{from_city}-{from_station}, {to_city}-{to_station}) return [] url https://kyfw.12306.cn/otn/leftTicket/queryZ params { leftTicketDTO.train_date: train_date, leftTicketDTO.from_station: from_station, leftTicketDTO.to_station: to_station, purpose_codes: purpose } try: resp self.session.get(url, paramsparams, timeout5) resp.raise_for_status() data resp.json() if data.get(status) and data.get(data): return self.parse_ticket_result(data[data]) else: print(f查询失败或返回数据异常{data.get(messages)}) except Exception as e: print(f查询请求发生异常{e}) return [] def parse_ticket_result(self, data): 解析查询结果提取关键信息 tickets [] # result_list 是竖线分割的字符串列表 result_list data.get(result, []) # station_map 是字段映射关系 field_map data.get(map, {}) # 这里需要根据field_map解析result_list中每个位置的字段名 # 假设我们通过分析确定了索引位置以下为示例实际需验证 for item in result_list: fields item.split(|) if len(fields) 30: # 确保有足够字段 train_info { 车次: fields[3], 出发站: field_map.get(fields[6], fields[6]), 到达站: field_map.get(fields[7], fields[7]), 出发时间: fields[8], 到达时间: fields[9], 历时: fields[10], 商务座/特等座: fields[32] or fields[31], # 可能为空 一等座: fields[31], 二等座: fields[30], 软卧: fields[28], 硬卧: fields[29], 硬座: fields[26], 无座: fields[25], } # 过滤掉“列车停运”等情况 if train_info[车次] and not train_info[车次].startswith(列车停运): tickets.append(train_info) return tickets def monitor_tickets(self, query_list, interval1, callbackNone): 监控多个查询条件 query_list: 列表每个元素是 (from_city, to_city, train_date) interval: 查询间隔秒数 callback: 发现有余票时的回调函数 while True: for query_args in query_list: tickets self.query_tickets(*query_args) for ticket in tickets: # 检查目标席别是否有票非空且不是‘无’或‘*’ if ticket.get(二等座) and ticket[二等座] not in [无, *, ]: print(f[{time.strftime(%H:%M:%S)}] 发现余票{ticket[车次]} {ticket[出发时间]}-{ticket[到达时间]} 二等座{ticket[二等座]}) if callback: callback(ticket, query_args) # 触发下单回调 time.sleep(interval) # 使用线程池监控多个日期 def start_multi_date_monitor(session, from_city, to_city, date_list, seat_type二等座): query_engine QueryEngine(session) query_list [(from_city, to_city, d) for d in date_list] def on_ticket_found(ticket_info, query_args): print(f准备为 {query_args} 下单车次 {ticket_info[车次]}...) # 这里触发下单流程 # submit_order(ticket_info, query_args) # 在一个单独的线程中启动监控 monitor_thread threading.Thread(targetquery_engine.monitor_tickets, args(query_list, 0.8, on_ticket_found), daemonTrue) monitor_thread.start() return monitor_thread实操心得查询频率控制过于频繁的查询如小于0.5秒可能导致IP被暂时限制。建议间隔设置在1秒左右并且可以加入随机延迟time.sleep(1 random.random())来模拟人工操作。席别判断逻辑余票字段可能是数字如‘10’也可能是字符如‘有’无票时可能是‘无’、‘*’或空字符串。解析时需要做好兼容判断。多维度监控除了多日期还可以监控多车次类型如高铁、动车、多席别二等座无票则尝试一等座。这可以通过构造不同的query_list来实现。异常处理网络请求可能超时或返回错误。一定要用try...except包裹并记录日志。当连续多次请求失败时可能是IP被风控应暂停一段时间或更换代理如果需要。6. 关键环节三提交订单与座位选择当监控程序发现有余票时需要立即触发下单流程。这是整个程序中最需要争分夺秒的部分因为一张票可能同时被成千上万个请求争夺。下单流程大致分为以下几步检查登录状态下单前必须确认session中的Cookie依然有效。可以通过访问一个用户中心接口来验证。提交订单请求Check Order使用查询到的车次、日期、席别等信息向submitOrderRequest接口提交一个预订单请求。这个请求需要包含一个关键参数secretStr它隐藏在查询结果的result字符串中通常是第一个字段经过URL解码后的字符串。此外还需要train_date,back_train_date,tour_flag,purpose_codes,query_from_station_name,query_to_station_name,undefined等参数。成功后会返回一个data字段其中包含submitStatus为true。获取令牌Token并初始化订单预订单提交成功后需要请求initDc接口获取一个关键的globalRepeatSubmitToken和key_check_isChange。这两个令牌是后续所有订单相关请求的凭证。获取乘客信息请求getPassengerDTOs接口获取当前账户已保存的常用联系人列表。程序需要从中选择指定的乘车人。检查订单信息带着令牌、车次信息、乘客信息等请求checkOrderInfo接口验证订单信息的有效性。这里需要提交passengerTicketStr和oldPassengerStr两个格式复杂的字符串它们编码了乘客、席别、票价等信息。构造这两个字符串是此环节的难点。获取队列计数检查通过后请求getQueueCount接口获取当前排队情况。确认订单最后请求confirmSingleForQueue接口提交最终的确认请求。如果成功会返回data字段中的submitStatus为true并且订单进入“未完成订单”列表。难点解析passengerTicketStr的构造这个字符串的格式类似于席别代码,乘客类型代码,乘客姓名,证件类型,证件号码,手机号码,保存状态,N。对于多个乘客用_下划线连接。例如1,0,张三,1,身份证号,手机号,1,N_1,0,李四,1,身份证号,手机号,1,N。席别代码1-硬座O-二等座M-一等座4-软卧3-硬卧9-商务座。乘客类型代码1-成人2-儿童3-学生4-残军。证件类型1-身份证。oldPassengerStr格式类似乘客姓名,证件类型,证件号码,乘客类型代码_。座位选择选座 在提交订单的某个环节通常在checkOrderInfo之后可以请求getExtraInfo接口获取可选座位信息如‘A,B,C,D,F’代表一排的座位号。然后通过confirmPassengerOptions接口提交选择的座位如‘1A,1B’。但请注意在票源极度紧张时选座可能会降低成功率因为系统需要同时满足“有余票”和“指定座位可用”两个条件。很多抢票策略会选择不指定座位以最快速度提交订单。代码结构示意关键步骤def submit_order(self, secret_str, train_date, train_no, from_station_code, to_station_code, seat_type, passenger_tickets): secret_str: 从查询结果中提取的加密字符串 passenger_tickets: 列表每个元素是一个乘客信息字典 # 1. 提交订单请求 submit_url https://kyfw.12306.cn/otn/leftTicket/submitOrderRequest data { secretStr: secret_str, # URL解码后的 train_date: train_date.strftime(%Y-%m-%d), back_train_date: datetime.now().strftime(%Y-%m-%d), tour_flag: dc, # 单程 purpose_codes: ADULT, query_from_station_name: self.station_map.get_name_by_code(from_station_code), query_to_station_name: self.station_map.get_name_by_code(to_station_code), undefined: } resp self.session.post(submit_url, datadata) # ... 检查响应 ... # 2. 初始化订单获取token init_url https://kyfw.12306.cn/otn/confirmPassenger/initDc # 通常需要GET请求并携带_json_att等参数 resp self.session.post(init_url) # 注意这里可能是POST需根据实际抓包确定 # 从响应HTML或后续的JSONP请求中提取 globalRepeatSubmitToken 和 key_check_isChange # 这步通常需要解析HTML或执行一个额外的JSONP请求比较复杂。 token, key_check self.extract_tokens(resp.text) # 3. 获取乘客信息 (通常可以缓存不必每次获取) passengers self.get_passengers(token) # 4. 检查订单信息 check_url https://kyfw.12306.cn/otn/confirmPassenger/checkOrderInfo passenger_ticket_str self.build_passenger_ticket_str(passenger_tickets, seat_type) old_passenger_str self.build_old_passenger_str(passenger_tickets) check_data { cancel_flag: 2, bed_level_order_num: 000000000000000000000000000000, passengerTicketStr: passenger_ticket_str, oldPassengerStr: old_passenger_str, tour_flag: dc, randCode: , whatsSelect: 1, _json_att: , REPEAT_SUBMIT_TOKEN: token } resp self.session.post(check_url, datacheck_data) # ... 检查响应确保 data 中的 submitStatus 为 true ... # 5. 获取队列计数 queue_url https://kyfw.12306.cn/otn/confirmPassenger/getQueueCount queue_data { train_date: datetime.now().strftime(%Y %b %d %H:%M:%S GMT0800 (中国标准时间)), # 注意格式 train_no: train_no, stationTrainCode: train_code, seatType: seat_type_code, fromStationTelecode: from_station_code, toStationTelecode: to_station_code, leftTicket: left_ticket_str, # 从之前响应中获取 purpose_codes: 00, train_location: train_location, # 从之前响应中获取 _json_att: , REPEAT_SUBMIT_TOKEN: token } # ... 发送请求 ... # 6. 最终确认 confirm_url https://kyfw.12306.cn/otn/confirmPassenger/confirmSingleForQueue confirm_data { passengerTicketStr: passenger_ticket_str, oldPassengerStr: old_passenger_str, randCode: , purpose_codes: 00, key_check_isChange: key_check, leftTicketStr: left_ticket_str, train_location: train_location, choose_seats: , # 选座信息如1A,1B seatDetailType: 000, whatsSelect: 1, roomType: 00, dwAll: N, _json_att: , REPEAT_SUBMIT_TOKEN: token } resp self.session.post(confirm_url, dataconfirm_data) result resp.json() if result.get(data) and result[data].get(submitStatus): print(订单提交成功请尽快在30分钟内完成支付。) return True else: print(f订单提交失败{result}) return False避坑指南参数格式与编码所有时间、字符串参数必须与浏览器发送的完全一致包括空格和大小写。例如日期格式‘2024-01-20’和‘Sat Jan 20 2024 00:00:00 GMT0800 (中国标准时间)’用在不同的接口。secretStr需要URL解码urllib.parse.unquote。令牌Token的传递globalRepeatSubmitToken和key_check_isChange是贯穿下单流程的生命线每一个后续请求几乎都需要它们。必须从正确的响应中准确提取。乘客信息字符串passengerTicketStr和oldPassengerStr的构造必须精确一个多余的逗号或下划线都可能导致失败。建议先手动在浏览器下单抓包查看这两个字符串的格式然后编写函数严格按照格式组装。错误处理与重试下单流程中的任何一个步骤失败都应记录日志并分析原因。如果是网络超时或可重试的错误如“当前排队人数过多”可以立即重试该步骤。但如果是参数错误或令牌失效则需要重新走整个流程。支付环节程序成功提交订单后订单会进入“未完成订单”列表通常有30分钟支付时间。自动支付涉及与银行或第三方支付平台的交互复杂度高且风险大强烈不建议在个人脚本中实现。最佳实践是程序成功下单后通过声音、弹窗、邮件、微信消息等方式强烈提醒用户手动完成支付。7. 高级策略与优化从“能抢”到“好抢”一个基础的抢票程序能跑通流程但要想在真正的春运高峰中脱颖而出还需要一些策略和优化。1. 分布式与多节点单机单IP的请求频率有限容易被限制。可以部署多个抢票节点例如在几台不同的云服务器或家庭电脑上运行同时监控和抢票。这需要设计一个简单的中心协调机制比如用一个共享数据库或消息队列来分配任务和去重避免多个节点重复提交同一张订单。2. 智能重试与退避策略当请求失败如网络错误、服务器返回“系统忙”时不要立即以固定频率重试。应采用指数退避策略第一次失败等1秒第二次等2秒第三次等4秒……并在随机时间点进行重试避免所有客户端同步。3. 缓存与预热车站编码缓存首次运行后将车站编码字典保存为本地JSON文件下次直接加载避免每次启动都去请求。乘客信息缓存登录后获取的乘客列表也可以缓存。会话Cookie持久化成功登录后将requests.Session的CookieJar用pickle序列化保存。下次启动直接加载可以跳过登录和验证码极大提高启动速度。注意Cookie有有效期。4. 多任务与优先级队列程序应支持同时监控多个任务如北京-上海、北京-广州不同日期不同车次。为每个任务设置优先级如优先抢高铁二等座若无则抢动车二等座再考虑一等座。使用优先级队列queue.PriorityQueue来管理待处理的下单请求确保高优先级任务最先被处理。5. 日志与监控完善的日志系统至关重要。应记录每个关键步骤查询、发现余票、提交订单每一步的结果和时间戳。可以使用Python的logging模块将日志输出到文件和控制台方便事后排查问题。例如可以设置INFO级别记录正常流程DEBUG级别记录详细的请求和响应数据注意屏蔽密码等敏感信息。6. 资源友好与遵守规则尽管是抢票但程序行为应尽量模拟真人避免对12306服务器造成过大压力。设置合理的查询间隔如1-2秒避免在深夜等非售票时间进行高频查询。尊重购票规则不使用异常手段如大量虚拟身份信息。8. 常见问题排查与实战心得在实际开发和运行中你肯定会遇到各种各样的问题。下面是一些典型问题及其排查思路问题现象可能原因排查与解决思路登录时验证码一直错误1. 坐标计算或格式错误。2. 验证码已过期。3. 网络问题导致图片加载不全。1. 打印出提交的answer参数与手动在网页上点击的坐标对比。2. 每次获取验证码后应立即使用长时间未操作需重新获取。3. 检查获取验证码的请求是否成功图片是否能正常显示。查询接口返回status: false或messages: [“系统忙”]1. 请求参数错误如车站码不对。2. 请求过于频繁IP被临时限制。3. 服务器真的繁忙。1. 核对from_station和to_station的电报码是否正确。2. 大幅降低查询频率如改为5秒一次观察是否恢复。3. 等待几分钟再试或更换网络环境/IP。提交订单时提示“非法请求”或“Token无效”1. 下单流程中的令牌REPEAT_SUBMIT_TOKEN,key_check_isChange缺失或错误。2. 请求顺序错误跳过了某个必要的步骤。3. Cookie失效登录状态丢失。1. 用抓包工具如Fiddler、Charles完整录制一次浏览器手动下单流程对比你的程序每一步请求的URL、Headers和Data。2. 确保严格按照initDc-getPassengerDTOs-checkOrderInfo-getQueueCount-confirmSingleForQueue的顺序发起请求。3. 重新登录获取新的Cookie。订单提交后返回“排队人数过多”1. 票确实已被抢光。2. 服务器负载高。1. 这是一个正常反馈程序应继续监控等待其他人未支付取消后的回票。2. 可以尝试立即重试几次但不要无限重试。程序运行一段时间后突然所有请求都失败1. IP地址被12306封禁。2. 账号因异常操作被暂时锁定。1. 暂停程序等待一段时间如半小时到几小时。考虑使用代理IP池但需谨慎公开代理质量差且可能违法。2. 尝试在浏览器手动登录12306看是否需要图片验证或短信验证。账号锁定通常一段时间后会自动解除。我个人在实际操作中的几点深刻体会抓包是王道所有逻辑和参数都必须以当前时刻浏览器抓包的数据为准。网上的教程和代码片段可能已经过时。学会使用Chrome/Firefox的开发者工具特别是Network标签页的XHR/Fetch过滤器是开发这类项目的必备技能。模块化与配置化不要把出发地、目的地、日期、乘客等信息硬编码在代码里。应该设计一个配置文件如config.json让用户方便地修改。程序结构也要清晰将登录、查询、下单分别写成独立的类或模块便于调试和维护。稳定大于一切在抢票高峰你的程序可能要和几十万甚至上百万个请求竞争。这时程序的稳定性和容错性比极限的请求速度更重要。一个因为一个小异常就崩溃的程序远不如一个能稳定运行、持续监控的程序有效。务必做好每一个网络请求的异常捕获和日志记录。理解“分流”的本质所谓的“分流”在程序层面主要体现为多线程/多进程查询和多任务多日期、多车次监控。它的目的不是“攻击”服务器而是扩大监控覆盖面避免因为只查一个车次而错过其他车次的放票或退票。合理设置查询间隔和任务优先级才是提高成功率的关键。保持敬畏合法使用这个项目是绝佳的学习材料但请务必在合法合规的范围内使用。不要用于商业代抢不要干扰12306系统的正常运行。它的价值在于让你掌握一套完整的自动化、反爬、网络编程的技术栈这份能力远比抢到一张票更有意义。