Python制作12306查票工具:从零构建铁路购票信息查询系统
Python制作12306查票工具:从零构建铁路购票信息查询系统
目录
- 引言:为什么需要开发12306查票工具?
- 课程目标与结构概述
- 网络爬虫基础理论详解
- 3.1 什么是网络爬虫?
- 3.2 爬虫的工作原理与核心流程
- 3.3 静态网页 vs 动态网页的识别方法
- 3.4 HTTP协议基础与请求响应机制
- 12306网站反爬机制分析
- 4.1 登录验证与验证码破解难点
- 4.2 接口加密参数逆向工程
- 4.3 Cookie会话管理与Token刷新
- 4.4 请求频率限制与IP封禁策略
- 项目实战:构建12306余票查询系统
- 5.1 需求分析与功能设计
- 5.2 技术选型与环境准备
- 5.3 车次数据接口定位与抓包分析
- 5.4 请求头伪造与浏览器指纹模拟
- 5.5 数据解析与JSON提取
- 5.6 命令行交互界面设计
- 5.7 多线程并发查询优化
- 完整代码实现与逐行解析
- 进阶技巧与性能优化建议
- Python学习路径规划
- Python职业发展方向与就业指导
- 法律合规性与道德边界探讨
- 总结与后续学习建议
1. 引言:为什么需要开发12306查票工具?
每年春运期间,数以亿计的中国人踏上归途。据中国国家铁路集团统计,2024年春运期间全国铁路发送旅客总量达 48.9亿人次,日均发送约1.2亿人。
在如此庞大的出行需求下,12306官网和App成为绝大多数人购票的首选渠道。然而,许多用户都经历过以下痛点:
- 页面加载缓慢,经常卡顿或崩溃;
- 验证码复杂难辨,多次尝试仍失败;
- 刷票过程繁琐,需手动刷新、选择座位、提交订单;
- 抢票成功率低,尤其热门线路一票难求;
- 第三方平台收取高额“加速包”费用(实则并无明显效果);
这些问题催生了大量的第三方抢票软件,其中不少正是基于 Python 爬虫技术 构建的自动化工具。
💡 本课程目的:不是教你如何“黄牛式抢票”,而是通过一个真实项目的开发,让你掌握:
- 如何分析现代Web应用的数据接口
- 如何绕过常见的反爬机制
- 如何编写稳定高效的爬虫程序
- 理解背后的技术原理,从而做出更理性的选择
2. 课程目标与结构概述
本课程旨在通过从零开始构建一个 12306余票查询工具 的全过程,帮助你系统掌握 Python 网络爬虫的核心技能,并理解其背后的工程逻辑与实际应用场景。
2.1 课程设计初衷
很多初学者在学习爬虫时常常面临两个问题:
- 学了一堆语法,但不知道能用来做什么;
- 看到别人写的“抢票神器”觉得很神秘,不知其所以然。
因此,本节课将带你亲手实现一个实用的小工具——无需登录即可快速查询某日某条线路的列车余票信息。虽然不包含自动下单功能(涉及安全与合规),但它足以揭示大多数商业抢票软件的核心工作原理。
此外,我们还将深入探讨 Python 的职业发展路径,帮助你在学习过程中明确方向。
2.2 本次课程的主要内容
我们将围绕以下五大模块展开讲解:
-
网络爬虫基础理论
- 理解HTTP协议、URL、请求/响应模型
- 区分静态与动态网页
- 掌握浏览器开发者工具使用技巧
-
12306反爬机制深度剖析
- 分析其前端架构与接口调用方式
- 识别关键加密参数生成逻辑
- 模拟合法用户行为规避检测
-
查票工具实战开发
- 定位车次查询接口
- 构造合法请求头
- 解析返回的JSON数据
- 实现命令行交互式查询
-
性能优化与健壮性提升
- 使用多线程提高查询效率
- 添加异常处理与重试机制
- 日志记录与调试支持
-
职业发展建议
- 如何系统学习Python爬虫?
- 在求职面试中如何展示项目经验?
- 爬虫工程师的职业晋升路径
2.3 技术要求与前置知识
为了顺利跟上本课程,你需要具备以下基础知识:
- 基本的计算机操作能力(安装软件、创建文件夹等)
- 对 Python 有初步了解(会写简单的
print()
、if
条件判断、for
循环) - 了解 HTML 和 CSS 的基本结构(非必须,但有助于理解网页抓取)
如果你尚未接触过这些内容,也不必担心——我们会从最基础的部分讲起,并在过程中逐步补充必要的知识点。
3. 网络爬虫基础理论详解
3.1 什么是网络爬虫?
网络爬虫(Web Crawler / Spider) 是一种按照一定规则,自动地从互联网上抓取信息的程序或脚本。
它的核心任务是:
- 向目标网站发送 HTTP 请求
- 获取返回的 HTML 或 JSON 数据
- 解析并提取所需内容
- 将结果保存到数据库或本地文件
🧩 类比理解:爬虫就像“数字快递员”
想象你要从图书馆里找一本特定的书:
- 你走进图书馆(发起请求)
- 查阅目录卡或电子检索系统(解析HTML)
- 找到书架位置并取出书籍(提取数据)
- 把书带回办公室阅读或归档(存储数据)
爬虫就是这样一个自动化的“图书管理员”,只不过它服务的是计算机而非人类。
3.2 爬虫的工作原理与核心流程
一个典型的爬虫程序包含以下几个步骤:
示例:抓取豆瓣电影 Top 250
import requests
from bs4 import BeautifulSoupurl = "https://movie.douban.com/top250"
headers = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
}response = requests.get(url, headers=headers)
soup = BeautifulSoup(response.text, 'html.parser')movies = soup.find_all('div', class_='item')
for movie in movies:title = movie.find('span', class_='title').textrating = movie.find('span', class_='rating_num').textprint(f"电影: {title}, 评分: {rating}")
3.3 静态网页 vs 动态网页的识别方法
这是爬虫开发中最关键的概念之一。
特征 | 静态网页 | 动态网页 |
---|---|---|
内容来源 | 直接嵌入HTML中 | 由JavaScript异步加载 |
查看源码能否看到数据 | ✅ 能 | ❌ 不能 |
加载方式 | 一次性全部返回 | 分阶段逐步渲染 |
技术栈 | HTML + CSS | HTML + JS + Ajax/Fetch |
抓取难度 | 简单(BeautifulSoup即可) | 复杂(需分析接口或Selenium) |
🔍 实战演示:如何判断页面类型
以百度首页为例:
- 打开浏览器,访问
https://www.baidu.com
- 右键点击页面 → “查看网页源代码”
- 搜索关键词如“抗击肺炎新闻”
- 如果能在源码中找到该文本 → 是静态内容
- 如果找不到 → 是动态加载
⚠️ 注意:现代网站大多是“伪静态”——初始HTML中有部分数据,其余通过JS补充。我们需要结合“Network”面板进一步分析。
3.4 HTTP协议基础与请求响应机制
(1)HTTP请求组成
一个完整的HTTP请求包括以下部分:
GET /search?q=python HTTP/1.1
Host: www.example.com
User-Agent: Mozilla/5.0 ...
Accept: text/html,application/xhtml+xml
Accept-Language: zh-CN,zh;q=0.9
Cookie: sessionid=abc123; token=xyz789
Connection: keep-alive
组件 | 说明 |
---|---|
请求方法 | GET(获取)、POST(提交)、PUT、DELETE等 |
URL路径 | /search?q=python |
协议版本 | HTTP/1.1 或 HTTP/2 |
Headers | 元信息,用于描述客户端、接受格式、认证等 |
Body | 仅POST/PUT请求有,携带提交数据 |
(2)HTTP响应结构
HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
Content-Length: 1234
Set-Cookie: session=def456; Path=/
Date: Mon, 05 Apr 2025 10:00:00 GMT<!DOCTYPE html>
<html>...</html>
状态码 | 含义 |
---|---|
200 | 成功 |
301/302 | 重定向 |
403 | 禁止访问 |
404 | 页面不存在 |
500 | 服务器内部错误 |
(3)Python中发送HTTP请求
import requests# GET请求
response = requests.get(url="https://httpbin.org/get",params={"key": "value"},headers={"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)"},timeout=10
)print(response.status_code) # 200
print(response.json()) # 返回JSON数据
4. 12306网站反爬机制分析
12306是中国最复杂的购票系统之一,也是反爬技术的“教科书级案例”。它采用了多层次防护体系。
4.1 登录验证与验证码破解难点
(1)滑块验证码(Slider CAPTCHA)
特点:
- 用户需拖动滑块完成拼图匹配
- 后端通过轨迹分析判断是否为机器人
- 每次请求生成唯一token
应对策略:
- 使用OpenCV图像识别定位缺口
- 模拟人类鼠标移动轨迹(加速度、抖动)
- 接入打码平台(如超级鹰、云打码)
⚠️ 本项目暂不实现登录功能,仅查询公开余票信息。
4.2 接口加密参数逆向工程
打开12306官网,搜索“北京 → 上海”的车次:
- 打开开发者工具(F12)→ Network → XHR
- 输入出发地、目的地、日期后点击“查询”
- 观察出现的新请求
发现关键接口为:
GET https://kyfw.12306.cn/otn/leftTicket/queryA?leftTicketDTO.train_date=2025-04-06&leftTicketDTO.from_station=BJP&leftTicketDTO.to_station=SHH&purpose_codes=ADULT
参数解析:
参数 | 示例值 | 说明 |
---|---|---|
train_date | 2025-04-06 | 出发日期(YYYY-MM-DD) |
from_station | BJP | 北京站编码(三字母缩写) |
to_station | SHH | 上海虹桥站编码 |
purpose_codes | ADULT | 成人票 |
✅ 好消息:此接口无需登录,且参数明文传递,适合入门学习!
4.3 Cookie会话管理与Token刷新
尽管该接口无需登录,但仍需携带有效的 Cookie 和 User-Agent,否则会被拦截。
常见必要Header:
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","Referer": "https://www.12306.cn/","Host": "kyfw.12306.cn","Accept": "application/json, text/javascript, */*; q=0.01"
}
💡 技巧:使用
requests.Session()
自动管理 Cookie 生命周期。
4.4 请求频率限制与IP封禁策略
12306会对高频请求进行限流:
行为 | 可能后果 |
---|---|
每秒超过3次查询 | 返回403 Forbidden |
连续查询同一车次 | 触发风控 |
缺少Referer或UA | 直接拒绝 |
应对方案:
- 控制请求间隔 ≥ 1秒
- 随机化请求顺序
- 使用代理池轮换IP(高级)
- 添加指数退避重试机制
5. 项目实战:构建12306余票查询系统
5.1 需求分析与功能设计
我们要开发的是一款命令行版12306余票查询器,具备以下功能:
✅ 核心功能
- 支持按日期、出发地、目的地查询车次
- 显示车次号、出发时间、到达时间、历时、各席别余票
- 支持模糊匹配车站名称(如“北京”匹配“北京西”、“北京南”)
- 查询结果按出发时间排序
🔧 扩展功能(后续可添加)
- 支持邮件/SMS通知有票
- 图形化界面(Tkinter/PyQt)
- 自动刷新监控指定车次
- 导出Excel报表
5.2 技术选型与环境准备
技术栈 | 用途 |
---|---|
Python 3.8+ | 主语言 |
requests | 发送 HTTP 请求 |
json | 解析 JSON 响应 |
prettytable | 格式化输出表格 |
argparse | 命令行参数解析 |
colorama (可选) | 彩色终端输出 |
安装依赖
pip install requests prettytable colorama
5.3 车次数据接口定位与抓包分析
经过前面的分析,我们已知核心接口为:
GET https://kyfw.12306.cn/otn/leftTicket/queryA
请求参数示例:
?leftTicketDTO.train_date=2025-04-06
&leftTicketDTO.from_station=BJP
&leftTicketDTO.to_station=SHH
&purpose_codes=ADULT
返回数据结构(简化):
{"data": {"result": ["列次|站名|站名|日期|G1|北京南|上海虹桥|08:00|12:00|4小时|商务座|一等座|二等座|...,","..."],"map": {"BJP": "北京","SHH": "上海虹桥"}}
}
⚠️ 注意:返回的是字符串数组,每条记录以竖线
|
分隔,需手动解析。
5.4 请求头伪造与浏览器指纹模拟
import requestsclass TrainTicketQuery: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 (KHTML, like Gecko) ""Chrome/120.0.0.0 Safari/537.36","Referer": "https://www.12306.cn/","Host": "kyfw.12306.cn"})self.station_codes = self.load_station_codes()def load_station_codes(self):"""加载车站编码映射表"""# 实际项目中应从官方资源获取或缓存return {"北京": "BJP", "北京西": "BXP", "北京南": "VNP","上海": "SHH", "上海虹桥": "AOH", "上海南": "SNH"}
5.5 数据解析与JSON提取
def parse_train_data(raw_result):trains = []for item in raw_result:fields = item.split('|')train_info = {"车次": fields[3],"出发站": fields[6],"到达站": fields[7],"出发时间": fields[8],"到达时间": fields[9],"历时": fields[10],"商务座": fields[32] or "--","一等座": fields[31] or "--","二等座": fields[30] or "--","高级软卧": fields[21] or "--","软卧": fields[23] or "--","硬卧": fields[28] or "--","硬座": fields[29] or "--","无座": fields[26] or "--"}trains.append(train_info)return trains
5.6 命令行交互界面设计
import argparsedef parse_args():parser = argparse.ArgumentParser(description="12306余票查询工具")parser.add_argument("from_city", help="出发城市,如:北京")parser.add_argument("to_city", help="目的城市,如:上海")parser.add_argument("date", help="乘车日期,格式:YYYY-MM-DD")parser.add_argument("--show-empty", action="store_true", help="显示无票车次")return parser.parse_args()# 使用示例
args = parse_args()
print(f"查询 {args.date} {args.from_city} → {args.to_city}")
运行方式:
python query.py 北京 上海 2025-04-06
5.7 多线程并发查询优化
当需要查询多个日期或路线时,可使用多线程提升效率:
from concurrent.futures import ThreadPoolExecutor
import timedef batch_query(queries, max_workers=3):results = {}with ThreadPoolExecutor(max_workers=max_workers) as executor:future_to_query = {executor.submit(query_tickets, q['from'], q['to'], q['date']): q for q in queries}for future in future_to_query:query = future_to_query[future]try:data = future.result()results[f"{query['date']}_{query['from']}_{query['to']}"] = dataexcept Exception as e:print(f"查询失败 {query}: {e}")return results
6. 完整代码实现与逐行解析
import requests
import json
import argparse
from datetime import datetime
from prettytable import PrettyTableclass TrainTicketQuery: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 (KHTML, like Gecko) ""Chrome/120.0.0.0 Safari/537.36","Referer": "https://www.12306.cn/","Host": "kyfw.12306.cn"})self.base_url = "https://kyfw.12306.cn/otn/leftTicket/queryA"self.station_map = self.load_station_codes()def load_station_codes(self):"""模拟车站编码映射(实际应从文件或API加载)"""return {"北京": "BJP", "北京西": "BXP", "北京南": "VNP", "北京北": "VNP","上海": "SHH", "上海虹桥": "AOH", "上海南": "SNH","广州": "GZQ", "广州南": "IZQ", "深圳": "SZQ", "杭州": "HZH"}def get_station_code(self, city_name):"""根据城市名查找车站编码"""for name, code in self.station_map.items():if city_name in name:return coderaise ValueError(f"未找到城市 {city_name} 的车站编码")def query_tickets(self, from_city, to_city, date):"""查询指定路线余票"""try:from_code = self.get_station_code(from_city)to_code = self.get_station_code(to_city)params = {"leftTicketDTO.train_date": date,"leftTicketDTO.from_station": from_code,"leftTicketDTO.to_station": to_code,"purpose_codes": "ADULT"}response = self.session.get(self.base_url, params=params, timeout=10)response.raise_for_status()data = response.json()if not data.get("data", {}).get("result"):print("⚠️ 未查询到相关车次,请检查输入信息。")return []return self.parse_results(data["data"]["result"])except Exception as e:print(f"❌ 查询失败: {e}")return []def parse_results(self, result_list):"""解析返回的车次数据"""trains = []for item in result_list:parts = item.split('|')if len(parts) < 33:continuetrain = {"车次": parts[3],"出发站": parts[6],"到达站": parts[7],"出发时间": parts[8],"到达时间": parts[9],"历时": parts[10],"商务座": parts[32] or "--","一等座": parts[31] or "--","二等座": parts[30] or "--","高级软卧": parts[21] or "--","软卧": parts[23] or "--","硬卧": parts[28] or "--","硬座": parts[29] or "--","无座": parts[26] or "--"}trains.append(train)return sorted(trains, key=lambda x: x["出发时间"])def display_results(self, trains):"""格式化输出结果"""if not trains:print("📭 没有可用车次。")returntable = PrettyTable()table.field_names = list(trains[0].keys())for train in trains:row = [train[key] for key in table.field_names]table.add_row(row)print(table)def main():parser = argparse.ArgumentParser(description="🚄 12306余票查询工具")parser.add_argument("from_city", help="出发城市,如:北京")parser.add_argument("to_city", help="目的城市,如:上海")parser.add_argument("date", help="乘车日期,格式:YYYY-MM-DD")parser.add_argument("--show-empty", action="store_true", help="显示无票车次")args = parser.parse_args()# 验证日期格式try:datetime.strptime(args.date, "%Y-%m-%d")except ValueError:print("❌ 日期格式错误,请使用 YYYY-MM-DD 格式。")returnprint(f"🔍 正在查询 {args.date} {args.from_city} → {args.to_city} 的余票...")query = TrainTicketQuery()trains = query.query_tickets(args.from_city, args.to_city, args.date)if not args.show_empty:trains = [t for t in trains if any(seat != "--" and seat != "无" for seat in [t["二等座"], t["一等座"], t["商务座"], t["软卧"], t["硬卧"]])]query.display_results(trains)if __name__ == "__main__":main()
7. 进阶技巧与性能优化建议
7.1 使用代理池防封 IP
proxies_list = ["http://1.1.1.1:8080","http://2.2.2.2:8080",
]import random
proxy = random.choice(proxies_list)
response = requests.get(url, proxies={"http": proxy}, timeout=10)
7.2 自动识别加密参数(逆向工程)
某些接口参数可能是动态生成的。可通过以下方式破解:
- 使用 Selenium 模拟浏览器执行 JS
- 逆向分析前端 JS 代码逻辑
- 使用 PyExecJS 执行 JavaScript
7.3 缓存机制减少重复请求
import pickle
from datetime import timedeltadef cache_result(key, data, expire_hours=1):cache_file = f"cache/{key}.pkl"with open(cache_file, 'wb') as f:pickle.dump({'data': data, 'timestamp': datetime.now()}, f)def load_cached_result(key, expire_hours=1):cache_file = f"cache/{key}.pkl"if not os.path.exists(cache_file):return Nonewith open(cache_file, 'rb') as f:cached = pickle.load(f)if datetime.now() - cached['timestamp'] < timedelta(hours=expire_hours):return cached['data']return None
8. Python学习路径规划
阶段 | 学习内容 | 推荐资源 |
---|---|---|
入门 | 基础语法、流程控制、函数 | 《Python编程:从入门到实践》 |
进阶 | 文件操作、异常处理、模块 | Real Python 教程 |
OOP | 类、继承、封装、多态 | 廖雪峰 Python 教程 |
爬虫 | requests, BeautifulSoup, Scrapy | 《Python3网络爬虫开发实战》 |
数据 | Pandas, NumPy, Matplotlib | Kaggle Learn |
自动化 | Selenium, Playwright | 官方文档 |
9. Python职业发展方向与就业指导
方向 | 核心技能 | 平均薪资(一线) |
---|---|---|
Web 开发 | Django, Flask, REST API | 15K–25K |
数据分析 | Pandas, SQL, BI 工具 | 14K–22K |
人工智能 | TensorFlow, PyTorch | 20K–40K |
自动化测试 | Selenium, Unittest | 12K–18K |
爬虫工程师 | Scrapy, 反爬破解 | 15K–28K |
💼 建议:打造 GitHub 作品集(如爬虫项目、数据分析报告)是求职加分项。
10. 法律合规性与道德边界探讨
尽管技术本身无罪,但在使用爬虫时必须遵守法律法规:
-
✅ 允许的行为:
- 抓取公开数据用于个人学习
- 遵守 robots.txt 规则
- 控制请求频率避免影响服务器
-
❌ 禁止的行为:
- 爬取用户隐私信息
- 绕过登录验证获取会员内容
- 大规模抓取造成服务器瘫痪
⚖️ 根据《网络安全法》与《民法典》,非法获取他人数据可能承担民事甚至刑事责任。
11. 总结与后续学习建议
通过本课程,你不仅学会了如何用 Python 抓取12306数据,更重要的是掌握了:
- 如何分析网页结构
- 如何定位真实接口
- 如何处理动态加载内容
- 如何编写健壮的下载程序
✅ 行动建议
- 动手修改代码:尝试增加余票提醒、图形界面等功能
- 挑战其他平台:试试12306余票监控、航班查询等
- 学习 Scrapy 框架:构建更专业的爬虫系统
- 参与开源项目:在 GitHub 上贡献代码
- 准备面试题:整理常见爬虫问题与答案
🌟 记住:每一个伟大的程序员,都是从“第一个爬虫”开始的。坚持下去,你也能创造出改变世界的作品!
视频学习来源:https://www.bilibili.com/video/BV13jhvz7ERx?s&spm_id_from=333.788.videopod.episodes&p=12