1. 项目概述:一个Python老兵的12306抢票工具实战复盘
又到了一年一度的春运返程高峰,看着朋友圈里各种“抢票难”的哀嚎,我想起了几年前自己写的一个小玩意儿。没错,就是那个用Python实现的12306抢票工具。虽然现在官方候补功能已经很完善,但那段研究接口、对抗验证码、优化请求策略的日子,依然是技术人解决实际问题的经典案例。今天,我就把这个项目的核心思路、技术细节和踩过的坑,毫无保留地分享给大家。这不仅仅是一个“脚本”,更是一次对复杂系统进行逆向工程和自动化实践的完整过程。无论你是想学习Python网络编程、了解自动化测试,还是对爬虫与反爬策略感兴趣,相信都能从中获得启发。
这个工具本质上是一个模拟用户购票流程的自动化程序。它的目标很明确:在指定车次、席别有余票的瞬间,快速、准确地完成登录、查询、下单、支付(提示)等一系列操作,其速度远超人工。它适合有一定Python基础,对HTTP协议、Web自动化有一定了解,并愿意折腾的开发者。对于纯粹的小白用户,我更建议你优先使用官方的候补功能,因为维护一个稳定的自动化工具需要持续投入精力。但如果你想了解这背后的技术逻辑,或者想自己动手打造一个个性化的工具,那么请继续往下看。
2. 核心架构与设计思路拆解
2.1 为什么选择Python?技术栈选型背后的考量
首先回答一个根本问题:为什么是Python?在自动化、爬虫领域,Python几乎是首选语言,这得益于其丰富的生态库。对于12306这样的目标,我们需要处理HTTP请求、解析JSON/HTML、处理验证码、管理会话状态等。Python的requests库简洁强大,BeautifulSoup和lxml能轻松应对页面解析,Pillow处理图片验证码,selenium应对复杂的动态交互(如滑动验证)。这些库成熟、稳定,社区支持好,能极大降低开发成本。
当时的设计思路是“模拟”而非“破解”。我们完全尊重12306的官方业务流程,只是用程序代替了人工点击。整个架构围绕以下几个核心模块展开:
- 会话管理模块:核心是维持一个
requests.Session()对象,自动处理Cookies,模拟浏览器保持登录状态。这是所有后续请求的基础。 - 查询模块:定时或条件触发,向12306的余票查询接口发送请求。这里的关键是参数构造和请求频率控制,过于频繁会触发风控。
- 登录模块:最复杂的部分之一。需要处理用户名密码登录、动态二维码登录,以及最棘手的验证码(图片点选、滑动等)。
- 下单模块:当查询到有余票时,立即执行一系列原子操作:检查用户登录状态、提交乘车人信息、检查订单信息、获取排队计数、最终提交订单。
- 通知模块:下单成功或出现异常时,通过邮件、微信等方式通知用户。
- 配置与调度模块:管理车次、日期、乘车人、座位偏好等配置,并调度各个模块协同工作。
我选择了配置文件使用YAML格式,因为它比JSON更易读,支持注释,比INI文件能表达更复杂的层次结构(如列表、嵌套字典),非常适合管理多日期、多车次、多乘车人的复杂配置。
2.2 对抗升级:从简单脚本到持续维护的思考
12306的后端风控策略一直在升级,这意味着抢票工具不是一个一劳永逸的项目。早期可能只需要模拟几个POST请求,后来加入了复杂的验证码,再后来有了设备指纹、行为检测。我的项目迭代也体现了这一点:从最初的纯requests实现,到后来引入selenium来获取更真实的浏览器指纹和环境参数,都是为了更好地“模拟真人”。
这里有一个重要的设计原则:分离稳定部分与易变部分。像查询余票的接口参数、城市车站编码映射,这些相对稳定,可以固化在代码或配置里。而像登录验证逻辑、请求头(User-Agent、各种Token)的生成、接口URL,这些是易变的,需要设计成可灵活替换和更新的结构。我在代码中大量使用了面向对象的设计,将每个功能模块类化,通过继承和组合,可以相对容易地替换某个环节的实现,比如将“打码平台识别”换成“本地深度学习模型识别”。
3. 关键技术与难点实战解析
3.1 登录攻坚战:验证码与会话保持
登录是第一个拦路虎。12306的登录流程经历了多次演变。
1. 密码加密与首次请求:首先,不能明文发送密码。观察浏览器网络请求会发现,密码是经过RSA公钥加密的。我们需要先请求一个获取公钥的接口,然后用JavaScript库(如jsencrypt)或Python的rsa库在本地进行加密。这里有个坑:12306使用的RSA填充模式是PKCS1_v1_5,必须完全匹配,否则服务器端解密会失败。
2. 验证码识别:这是最耗精力的部分。早期的图片点选(“点击图中所有的xxx”)可以通过第三方打码平台(如超级鹰、图鉴)来解决,这需要额外费用,但识别率高。为了学习和控制成本,我也尝试过本地方案:
- 传统图像处理:对验证码图片进行二值化、去噪、分割,然后使用OCR库(如
pytesseract)识别。但对于12306干扰线密集的验证码,效果很差。 - 深度学习:使用TensorFlow或PyTorch训练一个CNN模型。需要大量标注数据,自己标注费时费力,网上能找到一些开源数据集。虽然最终识别率可能达到90%以上,但模型维护和更新也是负担。
实操心得:对于个人项目,初期建议直接使用可靠的打码平台API,把精力集中在核心流程打通上。等整个流程跑通后,再考虑优化识别环节以降低成本。在我的项目配置里,预留了打码平台账号的配置项。
3. 滑动验证码:后来12306加入了滑动验证码。这需要用到selenium自动化浏览器。核心步骤是:
- 用
selenium打开登录页,获取带缺口的背景图和完整的滑块图。 - 计算缺口位置。这里可以用图像处理库(如
OpenCV)的模板匹配功能,找出缺口在背景图中的像素位置。 - 模拟人类滑动轨迹。直接让滑块瞬间移动到目标位置会被识别为机器行为。需要生成一个先加速后减速的移动轨迹,然后用
selenium的ActionChains来拖拽滑块。
注意:滑动轨迹的算法非常关键。一个简单的改进是将总位移分成多段,每段移动后加入极短的随机延迟,模拟人的微操。轨迹函数可以参考
ease_out_quad等缓动函数,让移动更自然。
4. 会话保持与Token管理:登录成功后,服务器会返回一系列Cookie(如RAIL_DEVICEID,RAIL_EXPIRATION)和Token(如uamtk)。这些必须被妥善保存在Session中,并在后续所有请求的请求头里正确携带。特别是RAIL_DEVICEID,它作为设备指纹的一部分,对于维持登录状态至关重要。我的做法是将登录成功后的Session对象序列化保存到文件,下次启动时直接加载,避免频繁登录触发风控。
3.2 智能查询与余票监控策略
查询是抢票的“眼睛”。12306的余票查询接口是公开的,但如何使用大有讲究。
1. 接口分析与参数构造:查询接口通常是一个GET请求,关键参数包括:
leftTicketDTO.train_date: 出发日期(格式:YYYY-MM-DD)leftTicketDTO.from_station: 出发站电报码(如北京西是BXP)leftTicketDTO.to_station: 到达站电报码purpose_codes: 票类型(成人票为ADULT)
最大的难点在于车站电报码。12306内部使用一套独特的编码,不是拼音也不是汉字。我们需要维护一个站名到编码的映射字典。这个映射可以从官网页面里提取,也可以从网络上的开源项目获取。我的工具里内置了一份,但需要定期更新。
2. 查询频率优化与风控规避:无脑高频查询会导致IP被暂时封禁。我的策略是:
- 基础间隔:设置一个合理的查询间隔,如2-5秒。在非预售期(开车前很多天),这个频率足够。
- 预售模式:在车票开售瞬间,采用更激进的策略。我实现了“多线程查询”,但如项目README中所警告,这非常危险,极易被ban。更稳妥的方法是:在开售前1分钟,将间隔逐步缩短至1秒,开售后维持一个较高但不过分的频率(如800毫秒)。
- CDN加速与代理:项目提到了CDN加速,原理是同时向多个12306的CDN节点发送查询请求,取最先返回的结果。但这需要维护一个可用的CDN节点列表,且存在安全风险(如果节点被篡改)。代理IP池是另一个方案,但高质量的代理IP需要成本。
3. 余票解析与决策逻辑:接口返回的是一长串加密数据,需要按固定位置分割解析。解析后,我们得到每个席别(二等座、一等座、无座等)的余票数量。 决策逻辑需要灵活配置:
- 席别优先级:用户可配置
[‘二等座’, ‘一等座’, ‘硬卧’],程序按顺序查找,找到第一个有票的席别就锁定。 - 车次优先级:可以接受多趟车次,按优先级排序。
- 多日期查询:用户可以配置多个出发日期,程序同时监控。
我的代码里,这部分逻辑被封装成一个独立的QueryEngine类,它接收配置,返回符合条件的最优车次、席别信息,供下单模块使用。
3.3 下单与订单确认:毫秒必争的最终环节
当查询模块发现目标票源,瞬间就会触发下单流程。这是一个环环相扣的链式调用,任何一个请求失败或延迟,都会导致失败。
1. 提交预定请求:首先,携带车次、日期、席别、乘车人信息,向提交订单接口发送请求。这个请求会返回一个关键的token和orderID。
2. 检查订单信息:用上一步获取的token去请求检查订单接口,确认票价、乘车人等信息无误。这里服务器会再次校验库存。
3. 获取排队计数:确认信息后,请求获取排队计数接口。在春运等高峰期,这一步可能会返回一个排队人数和预计等待时间。程序需要循环查询这个接口,直到返回“排队成功”的状态。
4. 确认订单:排队成功后,立即发送最终确认订单的请求。至此,票在系统中就算锁定在你的名下了,会进入待支付状态。
整个下单链路的超时和重试策略至关重要。每个请求我都设置了短而合理的超时时间(如3秒),并为整个下单链路设置一个总超时。如果某个步骤失败,会根据错误码决定是重试(如网络超时)还是放弃(如库存不足)。对于“排队计数”环节,需要实现一个轮询,但也要设置最大轮询次数,避免无限等待。
5. 异步通知:订单确认成功后,工具会通过配置的邮件服务器(如QQ邮箱SMTP)发送成功通知到用户邮箱,提醒用户在45分钟内完成支付。这里我用了smtplib和email库。为了不阻塞主程序,通知操作可以放在单独的线程中执行。
4. 配置文件与核心参数详解
一个健壮的工具离不开清晰的配置。我的项目使用config.yaml,下面我拆解几个关键配置项:
# 基础用户信息 user: username: ‘你的12306账号’ password: ‘你的加密密码(部分工具支持运行时输入)’ # 查询任务列表 tasks: - # 任务1 train_date: [‘2024-02-15‘, ‘2024-02-16‘] # 多日期 from_station: ‘上海‘ to_station: ‘北京‘ stations_code: # 站名映射电报码,可自动获取,也可手动指定 ‘上海‘: ‘SHH‘ ‘北京‘: ‘BJP‘ trains: [‘G1‘, ‘G3‘, ‘G5’] # 目标车次,按优先级排序 seats: [‘二等座‘, ‘一等座‘, ‘商务座’] # 座位优先级 passengers: [‘张三‘, ‘李四’] # 乘车人姓名,需与12306账号内一致 allow_less_seat: false # 是否接受无座票 query_interval: 2 # 基础查询间隔(秒) pre_sale_mode: # 预售模式配置 enable: true start_time: ‘2024-01-20 13:30‘ # 开售时间 advance_minutes: 5 # 提前多少分钟开始加速查询 min_interval: 0.8 # 最小查询间隔(秒) # 通知设置 notification: email: enable: true sender: ‘your_email@qq.com‘ password: ‘你的SMTP授权码‘ # 注意不是邮箱密码! receiver: ‘receiver@email.com‘ smtp_server: ‘smtp.qq.com‘ smtp_port: 465 # 验证码设置 captcha: type: ‘manual‘ # manual(手动), third_party(第三方平台) third_party: # 如果type为third_party则需配置 name: ‘chaojiying‘ # 打码平台名称 username: ‘your_username‘ password: ‘your_password‘ soft_id: ‘your_softid‘ # 网络设置 network: cdn_enable: false # 谨慎开启CDN加速 proxy_enable: false proxies: [] # 代理服务器列表 retry_times: 3 # 网络请求失败重试次数 timeout: 10 # 请求超时时间(秒)配置心得:
- 密码安全:不建议将明文密码写在配置文件中。我的做法是,如果检测到配置文件里是明文,则在第一次运行时提示用户输入密码,然后程序内部进行加密处理,或者只保存一个令牌。更好的方式是使用环境变量或密钥管理工具。
- 车站编码:
stations_code可以配置一部分,程序会优先使用。对于未配置的车站,工具内置了一个从网络获取最新编码的备用方案(需联网)。 - 预售模式:
pre_sale_mode是抢票成功的关键。一定要确保服务器时间准确(最好用NTP同步),start_time要精确到秒。
5. 部署运行与常见问题排坑指南
5.1 环境搭建与运行步骤
- 安装Python:确保你的Python版本在3.6以上。推荐使用Anaconda管理环境,避免包冲突。
- 获取代码:
git clone https://github.com/versionzhang/python_12306.git或者直接下载ZIP包。 - 安装依赖:进入项目目录,执行
pip install -r requirements.txt。这里可能会遇到cryptography或pyOpenSSL等库编译失败的问题,通常是因为缺少系统级的开发工具(如Windows下的C++ Build Tools, Linux下的python3-devlibssl-dev)。请根据错误提示搜索解决。 - 配置Chrome Driver:如果启用滑动验证或设备指纹功能,需要安装
selenium和对应版本的Chrome Driver。下载后将其所在目录添加到系统PATH,或者将可执行文件放在项目目录下。 - 修改配置:复制
config.yaml_example为config.yaml,并按照上述说明仔细填写你的信息。 - 运行:在终端执行
python mainloop.py。如果是通过pip安装的,则在配置文件所在目录执行py12306。
5.2 实战中遇到的典型问题与解决方案
下面这个表格是我在开发和长期使用中遇到的一些典型错误及其排查思路:
| 问题现象 | 可能原因 | 排查与解决思路 |
|---|---|---|
| 登录失败,提示“验证码错误” | 1. 打码平台识别错误 2. 本地OCR模型不准 3. 验证码图片未正确下载或处理 | 1. 切换到手动模式 (captcha.type: manual),观察程序弹出的图片,手动输入验证码,测试流程。2. 检查打码平台余额和接口配置是否正确。 3. 增加图片下载后的本地保存和查看逻辑,确认图片是否清晰可用。 |
| 登录成功但很快掉线,后续请求返回未登录 | 1. 设备指纹RAIL_DEVICEID失效或未正确传递。2. 会话 (Session) 未保持,每次请求新建了Session。 3. 请求头 (Headers) 不完整,缺少关键Token。 | 1. 启用selenium获取更真实的设备指纹。检查代码中是否正确从响应中提取并保存了RAIL_DEVICEID。2. 确保全程使用同一个 requests.Session()对象。3. 开启调试模式,打印关键请求的请求头和响应头,与浏览器抓包对比,补全缺失的Header,如 Referer,X-Requested-With等。 |
| 查询余票一直返回“网络错误”或空数据 | 1. IP被12306暂时限制。 2. 查询参数错误,特别是车站电报码不对。 3. 查询接口URL或参数结构已更新。 | 1. 降低查询频率,尤其是pre_sale_mode下的min_interval不要设得太小(建议大于0.5秒)。暂停程序,等待几分钟或更换网络环境再试。2. 打印出发送的完整查询URL,与浏览器开发者工具中捕获的URL进行逐项对比。使用工具内置或手动更新车站编码映射表。 3. 关注项目GitHub的Issue区,看是否有类似问题。可能需要根据新的接口调整代码。 |
| 查到有票,但下单瞬间失败,提示“库存不足” | 1. 真实库存为0,查询结果有缓存或延迟。 2. 下单链路某个步骤太慢,被其他用户或程序抢先。 3. 乘车人信息有误或未通过核验。 | 1. 这是常态,尤其在抢票高峰。程序的速度优势在于概率,而非绝对。 2. 优化下单链路代码,移除不必要的休眠 ( time.sleep),将顺序执行的网络请求尽可能优化(但注意12306有请求顺序依赖)。3. 提前在12306官网或APP中添加好常用乘车人,并在配置文件中填写完全一致的姓名。 |
| 程序运行一段时间后崩溃或卡死 | 1. 内存泄漏(如未关闭的Selenium浏览器实例)。 2. 异常未捕获,导致线程或主进程退出。 3. 网络不稳定导致死锁。 | 1. 确保selenium的driver.quit()在finally块中被执行。使用try...except...finally结构包裹关键代码。2. 在最外层添加全局异常捕获,记录日志,并尝试重启失败的任务模块。 3. 为所有网络请求设置合理的超时 ( timeout) 和重试机制,避免因单个请求hang住而阻塞整个循环。 |
独家避坑技巧:
- 本地时间同步:抢票服务器的开售时间是以北京时间为准。务必确保运行程序的电脑或服务器时间准确,误差最好在1秒以内。可以使用
ntpdate或w32tm命令同步。 - 日志为王:一定要开启详细日志,记录每个关键步骤的请求和响应(可脱敏敏感信息)。当出现问题时,日志是唯一的排查依据。我使用Python内置的
logging模块,将日志同时输出到控制台和文件,方便回溯。 - “小黑屋”策略:如果连续多次请求失败(如返回特定错误码),程序应自动进入“小黑屋”状态,即暂停所有请求10-30分钟,模拟真人被限制后的行为,避免IP被进一步封禁。
- 支付环节:本工具只负责锁定订单。支付仍需用户在手机APP或网站上手动完成。工具的成功通知里应明确提示支付时限(通常是45分钟)。
6. 法律、道德与替代方案思考
在分享技术细节的同时,我必须强调一点:任何自动化工具在访问公共服务时,都应遵守其服务条款。12306的《用户协议》通常禁止任何形式的自动化程序访问。使用此类工具可能存在账号被暂时或永久限制购票的风险。
从技术伦理角度看,我们开发和学习这类工具,应侧重于技术原理的探索和解决思路的锻炼,而非用于大规模、恶意的资源抢占。这和个人为了回家使用工具抢一张票,在性质上有所不同,但风险依然存在。
更推荐的替代方案:
- 官方候补功能:这是目前最公平、最有效的“抢票”方式。当所需车次/席别无票时,提交候补订单,系统会根据退票、改签情况自动按顺序分配。成功率很高,且完全合规。
- 多平台监控:除了12306,可以关注一些合规的旅行平台,它们有时会有不同的票源渠道或放票时间提醒。
- 灵活出行:尝试购买目标区间前后几站的车票(买长乘短或买短补长),或者选择中转方案,往往能提高成行概率。
我这个开源项目,正如其README所说,在候补功能普及后已基本停止功能更新,仅修复重大bug。它更像是一个时代的技术标本,记录了开发者与一个复杂系统交互的完整过程。对于学习者而言,其价值在于HTTP协议分析、自动化流程设计、异常处理和反反爬策略的实践,这些知识在任何Web自动化和爬虫场景下都是通用的。
如果你决定运行它,请抱着学习和研究的目的,谨慎配置,温柔请求,并做好随时需要根据官网变化而调整代码的心理准备。技术是工具,如何使用它,取决于我们自己的选择。