ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

12306火车票查询系统教学实践:Requests+Session会话管理与API解析

12306火车票查询系统教学实践:Requests+Session会话管理与API解析 简介本资源是一套基于Python开发的12306火车票查询系统完整源码专为计算机类专业学生设计适用于课程设计、期末大作业及毕业设计等教学实践场景帮助学习者掌握HTTP请求、HTML解析、用户交互与模块化编程等核心技能。压缩包共8个文件含4个核心Python脚本main.py为主程序入口ticket.py与station.py分别负责车票查询与车站编码处理log.py用于日志记录辅以README.md说明文档、LICENSE授权文件及Git配置文件整体仅18KB轻量易读、结构清晰。目前已有233人学习下载适合作为Python网络爬虫与GUI开发的入门进阶项目。代码已通过功能验证运行稳定支持二次开发与功能拓展可直接部署演示或作为毕设基础框架配套说明详实便于快速理解模块分工与调用逻辑。1. 这不是抢票脚本而是一套可调试、可验证、能跑通的12306火车票查询教学系统很多学生交课程大作业时把“12306火车票查询系统”当成一个黑盒功能来糊弄——复制粘贴一段带登录的爬虫代码运行时报错就改UA或加sleep最后截图几张控制台输出就算完成。但真正有价值的课程实践是理解12306接口如何被设计、如何被调用、如何被反爬识别以及在不触发风控的前提下稳定获取车次、余票、座位类型等结构化数据。这个基于Python实现的12306火车票查询系统源码核心目标不是“秒抢”而是构建一个可复现、可断点、可替换模块、可对接真实购票流程的教学级查询框架。它覆盖从会话维持、验证码识别模拟逻辑、车次检索、余票解析到结果本地缓存的完整链路适合计算机专业高年级学生或刚入门的后端开发者用于理解HTTP状态管理、JSON API解析、异步请求调度与Web安全边界。所有代码均避开敏感操作如自动提交订单、绕过图形验证码真实识别聚焦在“查得准、查得稳、查得清”三个教学维度上。2. 为什么必须用RequestsSession手动Cookie管理而不是Selenium或Playwright2.1 12306前端架构决定静态资源分离 动态API驱动12306官网采用典型的前后端分离架构HTML页面由CDN静态分发所有车票数据通过https://kyfw.12306.cn/otn/leftTicket/queryZ等RESTful接口返回JSON。这意味着只要能构造合法的HTTP请求头、携带有效会话凭证Cookie中的JSESSIONID、RAIL_DEVICEID、RAIL_EXPIRATION就不需要渲染整个浏览器环境。Selenium虽能绕过部分JS检测但启动慢、内存占用高、难以调试网络层细节且极易因页面DOM微调导致XPath失效——这在课程作业中属于“不可控故障源”。而RequestsSession组合能精准控制每个请求的生命周期便于在requests.Session()对象中统一维护Cookie、User-Agent、Referer并支持手动注入X-Requested-With: XMLHttpRequest等关键头字段完全匹配12306后端对AJAX请求的校验逻辑。提示12306服务端会校验Origin、Referer和User-Agent三者一致性。若使用SeleniumOrigin常为null需额外注入--disable-web-security参数反而增加被拦截概率而Requests可精确设置headers{Origin: https://kyfw.12306.cn, Referer: https://kyfw.12306.cn/otn/leftTicket/init}更贴近真实用户行为。2.2 Session管理的关键三要素RAIL_DEVICEID、RAIL_EXPIRATION与JSESSIONID12306会话并非仅靠JSESSIONID维持而是依赖一组强关联CookieCookie名作用是否必需获取方式RAIL_DEVICEID设备指纹标识Base64编码的16字节随机串是首次访问https://kyfw.12306.cn/otn/login/init时由JS生成并写入RAIL_EXPIRATIONRAIL_DEVICEID有效期时间戳毫秒是同上与RAIL_DEVICEID成对出现JSESSIONIDTomcat会话ID是登录成功后由服务端下发课程源码中session.py模块通过以下步骤初始化合法会话import requests import re import time def init_session(): s requests.Session() # Step 1: 访问登录页触发RAIL_DEVICEID生成 login_init_url https://kyfw.12306.cn/otn/login/init headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36, Accept: text/html,application/xhtmlxml,application/xml;q0.9,*/*;q0.8 } s.get(login_init_url, headersheaders, timeout10) # Step 2: 提取RAIL_DEVICEID和RAIL_EXPIRATION从响应HTML中正则提取 # 注意实际课程代码中此处应解析返回HTML的script标签内JS变量 # 示例伪代码rail_deviceid re.search(rRAIL_DEVICEID([^]), html).group(1) # Step 3: 手动注入设备指纹Cookie避免每次请求都丢失 s.cookies.set(RAIL_DEVICEID, xxx, domainkyfw.12306.cn) s.cookies.set(RAIL_EXPIRATION, str(int(time.time() * 1000) 3600000), domainkyfw.12306.cn) return s这段代码的关键在于RAIL_DEVICEID必须在首次请求时由服务端生成并返回不能硬编码后续所有请求必须携带该值及其对应的有效期。课程作业中常见错误是直接复制浏览器Cookie导致RAIL_EXPIRATION过期后请求全部返回{httpstatus:403,status:false,messages:[非法请求]}。正确做法是将init_session()封装为独立函数在主程序入口处调用一次后续所有查询均复用该Session实例。2.3 为什么放弃自动验证码识别教学场景下的合理取舍当前12306图形验证码已升级为滑块文字点选混合模式OCR准确率低于60%且调用第三方识别API涉及密钥管理与费用偏离课程作业目标。本源码采用“人工介入本地缓存”策略第一次运行时程序自动打开验证码图片保存至./captcha.jpg暂停等待用户手动输入输入后将验证码文本与对应RAIL_DEVICEID哈希值存入./captcha_cache.json后续相同设备ID请求时优先查缓存命中避免重复人工输入。这种设计既满足“可运行”要求又保留了验证码环节的教学意义——让学生理解https://kyfw.12306.cn/otn/passcodeNew/getPassCodeNew?login_siteEmoduleloginrandsjrand0.123456789接口的调用时机与参数含义而非简单跳过。3. 车次查询接口的参数构造与余票字段解析逻辑3.1 leftTicket/queryZ接口的必填参数与动态生成规则12306车次查询核心接口为GET https://kyfw.12306.cn/otn/leftTicket/queryZ?leftTicketDTO.train_date2024-06-15leftTicketDTO.from_stationBJPleftTicketDTO.to_stationSHHpurpose_codesADULT。其中train_date必须为YYYY-MM-DD格式且不能早于当前日期不能晚于60天后12306限制from_station/to_station非城市名而是车站电报码如北京北VAP上海虹桥AOH需查https://kyfw.12306.cn/otn/resources/js/framework/station_name.js映射表purpose_codes用途代码ADULT表示成人票0X表示学生票需提前核验资质。课程源码中station_code.py模块提供电报码转换# station_code.py STATION_MAP { 北京: BJP, 北京西: BXP, 北京南: VAP, 上海: SHH, 上海虹桥: AOH, 广州: GZQ, # ... 实际代码中包含全部800车站 } def get_station_code(station_name): 根据中文站名返回电报码支持模糊匹配 for key, code in STATION_MAP.items(): if station_name in key or key in station_name: return code raise ValueError(f未找到车站 {station_name} 的电报码)注意STATION_MAP必须定期更新。12306官网JS文件路径固定但内容每月可能调整。课程作业中建议将station_name.js下载后解析为Python字典而非实时抓取——避免因网络波动导致查询失败。3.2 JSON响应中余票字段的嵌套结构与业务含义接口返回JSON中data.result为字符串数组每项以|分隔20个字段。课程源码parser.py模块定义了解析规则def parse_ticket_row(row): fields row.split(|) return { train_no: fields[2], # 车次编号G101 start_time: fields[8], # 出发时间08:00 end_time: fields[9], # 到达时间12:30 duration: fields[10], # 历时04:30 from_station: fields[6], # 出发站编码BJP to_station: fields[7], # 到达站编码SHH swz_num: fields[32] or --, # 商务座余票字段32 tz_num: fields[31] or --, # 特等座余票字段31 zy_num: fields[30] or --, # 一等座余票字段30 ze_num: fields[29] or --, # 二等座余票字段29 gr_num: fields[25] or --, # 高级软卧余票字段25 rw_num: fields[23] or --, # 软卧余票字段23 yw_num: fields[28] or --, # 硬卧余票字段28 rz_num: fields[24] or --, # 软座余票字段24 yz_num: fields[22] or --, # 硬座余票字段22 wz_num: fields[21] or --, # 无座余票字段21 can_go: fields[13] Y # 是否可预订Y/N } # 示例调用 response session.get(url, paramsparams, timeout15) data response.json() for row in data[data][result]: ticket parse_ticket_row(row) if ticket[can_go] and ticket[ze_num] ! 无: print(f{ticket[train_no]} {ticket[start_time]}→{ticket[end_time]} 二等座余票: {ticket[ze_num]})关键点在于字段索引是硬编码的但12306从未公开文档说明其含义。课程作业中需引导学生通过多次抓包对比不同车次的响应确认各索引对应的实际业务字段。例如fields[29]在2023年为二等座2024年某次更新后变为fields[28]此时必须同步修改parse_ticket_row函数——这正是理解API稳定性与版本演进的教学价值所在。3.3 多线程查询的并发控制与防封策略为提升查询效率课程源码采用concurrent.futures.ThreadPoolExecutor并发请求但严格限制最大线程数设为3max_workers3避免高频请求触发IP限频每次请求间加入random.uniform(1.2, 2.5)秒随机延迟对同一车次的多次查询启用functools.lru_cache(maxsize128)缓存结果减少重复请求。from concurrent.futures import ThreadPoolExecutor, as_completed import random import time lru_cache(maxsize128) def query_single_date(train_date, from_code, to_code): url https://kyfw.12306.cn/otn/leftTicket/queryZ params { leftTicketDTO.train_date: train_date, leftTicketDTO.from_station: from_code, leftTicketDTO.to_station: to_code, purpose_codes: ADULT } time.sleep(random.uniform(1.2, 2.5)) # 必须否则易被403 try: resp session.get(url, paramsparams, timeout15) return resp.json() except Exception as e: return {status: False, error: str(e)} def batch_query_dates(dates, from_code, to_code): with ThreadPoolExecutor(max_workers3) as executor: future_to_date { executor.submit(query_single_date, d, from_code, to_code): d for d in dates } results {} for future in as_completed(future_to_date): date future_to_date[future] try: results[date] future.result() except Exception as e: results[date] {status: False, error: str(e)} return results提示lru_cache装饰器必须作用于纯函数无side effect。若query_single_date内部修改全局Session则缓存结果会污染后续请求。课程作业中应强调“缓存粒度”概念——按日期起止站组合缓存而非整个Session对象。4. 本地缓存设计与JSON Schema验证机制4.1 SQLite本地缓存解决重复查询与离线分析需求课程作业常忽略数据持久化导致每次运行都重新请求既低效又易被限流。本源码采用SQLite作为轻量级本地缓存表结构设计兼顾查询效率与扩展性CREATE TABLE IF NOT EXISTS ticket_cache ( id INTEGER PRIMARY KEY AUTOINCREMENT, train_date TEXT NOT NULL, -- 2024-06-15 from_station TEXT NOT NULL, -- BJP to_station TEXT NOT NULL, -- SHH train_no TEXT NOT NULL, -- G101 start_time TEXT, -- 08:00 end_time TEXT, -- 12:30 ze_num TEXT DEFAULT --, -- 二等座余票 swz_num TEXT DEFAULT --, -- 商务座余票 created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, UNIQUE(train_date, from_station, to_station, train_no) );缓存写入逻辑在cache.py中实现import sqlite3 from datetime import datetime def save_to_cache(ticket_data): conn sqlite3.connect(./ticket.db) cursor conn.cursor() for t in ticket_data: cursor.execute( INSERT OR REPLACE INTO ticket_cache (train_date, from_station, to_station, train_no, start_time, end_time, ze_num, swz_num) VALUES (?, ?, ?, ?, ?, ?, ?, ?) , ( t[train_date], t[from_station], t[to_station], t[train_no], t[start_time], t[end_time], t[ze_num], t[swz_num] )) conn.commit() conn.close() def query_from_cache(train_date, from_code, to_code, min_ze1): conn sqlite3.connect(./ticket.db) cursor conn.cursor() cursor.execute( SELECT * FROM ticket_cache WHERE train_date ? AND from_station ? AND to_station ? AND ze_num ! 无 AND ze_num ! -- AND CAST(ze_num AS INTEGER) ? ORDER BY ze_num DESC , (train_date, from_code, to_code, min_ze)) rows cursor.fetchall() conn.close() return rows此设计支持去重写入INSERT OR REPLACE避免重复数据条件筛选CAST(ze_num AS INTEGER) ?支持数值型余票过滤离线分析即使网络中断仍可查询历史缓存数据。4.2 JSON Schema验证确保接口变更时快速定位字段异常12306接口无正式OpenAPI规范字段增减频繁。课程源码引入jsonschema库在解析前校验响应结构from jsonschema import validate, ValidationError import json TICKET_SCHEMA { type: object, properties: { status: {type: boolean}, httpstatus: {type: integer}, data: { type: object, properties: { result: {type: array, items: {type: string}}, map: {type: object} }, required: [result] } }, required: [status, httpstatus, data] } def validate_response(resp_json): try: validate(instanceresp_json, schemaTICKET_SCHEMA) return True except ValidationError as e: print(fJSON Schema验证失败: {e.message} at {e.json_path}) return False # 使用示例 response session.get(url, paramsparams) if response.status_code 200: data response.json() if not validate_response(data): # 触发告警或降级处理 print(检测到12306接口结构变更请检查字段索引) return []当12306某次更新导致data.result变为data.list时该验证会立即抛出ValidationError提示具体路径$.data.result缺失而非让程序静默崩溃或解析出错数据——这是工程化思维在课程作业中的关键落地。5. 三类典型故障的定位方法与修复指令5.1 HTTP 403 Forbidden检查Referer、Origin与Cookie时效性当请求返回{httpstatus:403,status:false,messages:[非法请求]}时90%源于请求头或Cookie异常。按顺序执行以下诊断验证Referer与Origin是否匹配curl -I -H Referer: https://kyfw.12306.cn/otn/leftTicket/init \ -H Origin: https://kyfw.12306.cn \ https://kyfw.12306.cn/otn/leftTicket/queryZ?leftTicketDTO.train_date2024-06-15leftTicketDTO.from_stationBJPleftTicketDTO.to_stationSHHpurpose_codesADULT若返回200 OK说明问题在Python代码的headers设置若仍403检查Cookie是否过期。检查RAIL_EXPIRATION是否过期在Python中打印当前Cookieprint(RAIL_EXPIRATION:, session.cookies.get(RAIL_EXPIRATION)) print(当前时间戳:, int(time.time() * 1000))若前者小于后者需重新调用init_session()。确认JSESSIONID是否有效访问https://kyfw.12306.cn/otn/login/conf若返回{status:true,data:N}说明会话有效若返回{status:false,messages:[用户未登录]}则需重新登录。5.2 解析字段为空定位JSON结构变更与索引偏移当fields[29]始终为None时执行以下步骤抓取原始响应并保存with open(debug_response.json, w, encodingutf-8) as f: f.write(response.text)人工检查字段分隔符打开debug_response.json搜索result:[复制首条row字符串用在线工具如regex101.com测试split(|)结果确认字段数量是否仍为33。比对历史响应若旧响应中fields[29]为12新响应中同位置为有则说明12306将数字余票改为汉字需修改解析逻辑# 原逻辑 ze_num: fields[29] or --, # 新逻辑 ze_num: 0 if fields[29] 无 else fields[29] if fields[29].isdigit() else 15.3 多线程报错ConnectionResetError与Timeout的协同处理当ThreadPoolExecutor中出现ConnectionResetError: [WinError 10054]表明服务端主动断连。此时不应简单重试而应降低并发度将max_workers从5改为2延长超时session.get(..., timeout(10, 20))连接10秒读取20秒添加指数退避重试import time from functools import wraps def retry_on_failure(max_retries3, backoff_factor1.5): def decorator(func): wraps(func) def wrapper(*args, **kwargs): for attempt in range(max_retries): try: return func(*args, **kwargs) except (requests.exceptions.ConnectionError, requests.exceptions.Timeout) as e: if attempt max_retries - 1: raise e sleep_time backoff_factor ** attempt time.sleep(sleep_time) return wrapper return decorator retry_on_failure() def query_single_date(...): # 原查询逻辑此装饰器确保单次失败后等待1.5秒、2.25秒、3.375秒再重试避免雪崩式重连请求。本文还有配套的精品资源点击获取
返回列表