当前位置: 首页 > news >正文

Python制作12306查票工具:从零构建铁路购票信息查询系统

Python制作12306查票工具:从零构建铁路购票信息查询系统

目录

  1. 引言:为什么需要开发12306查票工具?
  2. 课程目标与结构概述
  3. 网络爬虫基础理论详解
    • 3.1 什么是网络爬虫?
    • 3.2 爬虫的工作原理与核心流程
    • 3.3 静态网页 vs 动态网页的识别方法
    • 3.4 HTTP协议基础与请求响应机制
  4. 12306网站反爬机制分析
    • 4.1 登录验证与验证码破解难点
    • 4.2 接口加密参数逆向工程
    • 4.3 Cookie会话管理与Token刷新
    • 4.4 请求频率限制与IP封禁策略
  5. 项目实战:构建12306余票查询系统
    • 5.1 需求分析与功能设计
    • 5.2 技术选型与环境准备
    • 5.3 车次数据接口定位与抓包分析
    • 5.4 请求头伪造与浏览器指纹模拟
    • 5.5 数据解析与JSON提取
    • 5.6 命令行交互界面设计
    • 5.7 多线程并发查询优化
  6. 完整代码实现与逐行解析
  7. 进阶技巧与性能优化建议
  8. Python学习路径规划
  9. Python职业发展方向与就业指导
  10. 法律合规性与道德边界探讨
  11. 总结与后续学习建议

1. 引言:为什么需要开发12306查票工具?

每年春运期间,数以亿计的中国人踏上归途。据中国国家铁路集团统计,2024年春运期间全国铁路发送旅客总量达 48.9亿人次,日均发送约1.2亿人。

在如此庞大的出行需求下,12306官网和App成为绝大多数人购票的首选渠道。然而,许多用户都经历过以下痛点:

  • 页面加载缓慢,经常卡顿或崩溃;
  • 验证码复杂难辨,多次尝试仍失败;
  • 刷票过程繁琐,需手动刷新、选择座位、提交订单;
  • 抢票成功率低,尤其热门线路一票难求;
  • 第三方平台收取高额“加速包”费用(实则并无明显效果);

这些问题催生了大量的第三方抢票软件,其中不少正是基于 Python 爬虫技术 构建的自动化工具。

💡 本课程目的:不是教你如何“黄牛式抢票”,而是通过一个真实项目的开发,让你掌握:

  • 如何分析现代Web应用的数据接口
  • 如何绕过常见的反爬机制
  • 如何编写稳定高效的爬虫程序
  • 理解背后的技术原理,从而做出更理性的选择

2. 课程目标与结构概述

本课程旨在通过从零开始构建一个 12306余票查询工具 的全过程,帮助你系统掌握 Python 网络爬虫的核心技能,并理解其背后的工程逻辑与实际应用场景。

2.1 课程设计初衷

很多初学者在学习爬虫时常常面临两个问题:

  1. 学了一堆语法,但不知道能用来做什么;
  2. 看到别人写的“抢票神器”觉得很神秘,不知其所以然。

因此,本节课将带你亲手实现一个实用的小工具——无需登录即可快速查询某日某条线路的列车余票信息。虽然不包含自动下单功能(涉及安全与合规),但它足以揭示大多数商业抢票软件的核心工作原理。

此外,我们还将深入探讨 Python 的职业发展路径,帮助你在学习过程中明确方向。

2.2 本次课程的主要内容

我们将围绕以下五大模块展开讲解:

  1. 网络爬虫基础理论

    • 理解HTTP协议、URL、请求/响应模型
    • 区分静态与动态网页
    • 掌握浏览器开发者工具使用技巧
  2. 12306反爬机制深度剖析

    • 分析其前端架构与接口调用方式
    • 识别关键加密参数生成逻辑
    • 模拟合法用户行为规避检测
  3. 查票工具实战开发

    • 定位车次查询接口
    • 构造合法请求头
    • 解析返回的JSON数据
    • 实现命令行交互式查询
  4. 性能优化与健壮性提升

    • 使用多线程提高查询效率
    • 添加异常处理与重试机制
    • 日志记录与调试支持
  5. 职业发展建议

    • 如何系统学习Python爬虫?
    • 在求职面试中如何展示项目经验?
    • 爬虫工程师的职业晋升路径

2.3 技术要求与前置知识

为了顺利跟上本课程,你需要具备以下基础知识:

  • 基本的计算机操作能力(安装软件、创建文件夹等)
  • 对 Python 有初步了解(会写简单的 print()if 条件判断、for 循环)
  • 了解 HTML 和 CSS 的基本结构(非必须,但有助于理解网页抓取)

如果你尚未接触过这些内容,也不必担心——我们会从最基础的部分讲起,并在过程中逐步补充必要的知识点。


3. 网络爬虫基础理论详解

3.1 什么是网络爬虫?

网络爬虫(Web Crawler / Spider) 是一种按照一定规则,自动地从互联网上抓取信息的程序或脚本。

它的核心任务是:

  1. 向目标网站发送 HTTP 请求
  2. 获取返回的 HTML 或 JSON 数据
  3. 解析并提取所需内容
  4. 将结果保存到数据库或本地文件
🧩 类比理解:爬虫就像“数字快递员”

想象你要从图书馆里找一本特定的书:

  • 你走进图书馆(发起请求)
  • 查阅目录卡或电子检索系统(解析HTML)
  • 找到书架位置并取出书籍(提取数据)
  • 把书带回办公室阅读或归档(存储数据)

爬虫就是这样一个自动化的“图书管理员”,只不过它服务的是计算机而非人类。


3.2 爬虫的工作原理与核心流程

一个典型的爬虫程序包含以下几个步骤:

确定目标网站
分析网页结构
构造HTTP请求
发送请求并获取响应
解析响应内容
提取目标数据
数据清洗与格式化
存储至文件或数据库
循环下一页或下一个关键词
示例:抓取豆瓣电影 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 + CSSHTML + JS + Ajax/Fetch
抓取难度简单(BeautifulSoup即可)复杂(需分析接口或Selenium)
🔍 实战演示:如何判断页面类型

以百度首页为例:

  1. 打开浏览器,访问 https://www.baidu.com
  2. 右键点击页面 → “查看网页源代码”
  3. 搜索关键词如“抗击肺炎新闻”
  4. 如果能在源码中找到该文本 → 是静态内容
  5. 如果找不到 → 是动态加载

⚠️ 注意:现代网站大多是“伪静态”——初始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官网,搜索“北京 → 上海”的车次:

  1. 打开开发者工具(F12)→ Network → XHR
  2. 输入出发地、目的地、日期后点击“查询”
  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
参数解析:
参数示例值说明
train_date2025-04-06出发日期(YYYY-MM-DD)
from_stationBJP北京站编码(三字母缩写)
to_stationSHH上海虹桥站编码
purpose_codesADULT成人票

好消息:此接口无需登录,且参数明文传递,适合入门学习!


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, MatplotlibKaggle Learn
自动化Selenium, Playwright官方文档

9. Python职业发展方向与就业指导

方向核心技能平均薪资(一线)
Web 开发Django, Flask, REST API15K–25K
数据分析Pandas, SQL, BI 工具14K–22K
人工智能TensorFlow, PyTorch20K–40K
自动化测试Selenium, Unittest12K–18K
爬虫工程师Scrapy, 反爬破解15K–28K

💼 建议:打造 GitHub 作品集(如爬虫项目、数据分析报告)是求职加分项。


10. 法律合规性与道德边界探讨

尽管技术本身无罪,但在使用爬虫时必须遵守法律法规:

  • ✅ 允许的行为:

    • 抓取公开数据用于个人学习
    • 遵守 robots.txt 规则
    • 控制请求频率避免影响服务器
  • ❌ 禁止的行为:

    • 爬取用户隐私信息
    • 绕过登录验证获取会员内容
    • 大规模抓取造成服务器瘫痪

⚖️ 根据《网络安全法》与《民法典》,非法获取他人数据可能承担民事甚至刑事责任。


11. 总结与后续学习建议

通过本课程,你不仅学会了如何用 Python 抓取12306数据,更重要的是掌握了:

  • 如何分析网页结构
  • 如何定位真实接口
  • 如何处理动态加载内容
  • 如何编写健壮的下载程序

✅ 行动建议

  1. 动手修改代码:尝试增加余票提醒、图形界面等功能
  2. 挑战其他平台:试试12306余票监控、航班查询等
  3. 学习 Scrapy 框架:构建更专业的爬虫系统
  4. 参与开源项目:在 GitHub 上贡献代码
  5. 准备面试题:整理常见爬虫问题与答案

🌟 记住:每一个伟大的程序员,都是从“第一个爬虫”开始的。坚持下去,你也能创造出改变世界的作品!


视频学习来源:https://www.bilibili.com/video/BV13jhvz7ERx?s&spm_id_from=333.788.videopod.episodes&p=12

http://www.dtcms.com/a/457957.html

相关文章:

  • 《道德经》第十三章
  • 东莞做网站网络公司官网建设的重要性
  • Docker 容器操作
  • 小说网站建设源码潜江网络
  • 做网页游戏网站html网页设计大赛作品
  • 日语学习-日语知识点小记-进阶-JLPT-N1阶段应用练习(8):语法 +考え方21+2022年7月N1
  • 维基框架 (Wiki Framework) v1.1.2 | 企业级微服务开发框架
  • 做的网站提示不安全个人网站的名字
  • 用wordpress建站会不会显得水平差喜迎二十大作文
  • 我已经把 Cookie 的值从 zhangfei 改成了 guanyu,为什么再次获取时还是 zhangfei?”
  • C++回调函数的设计以及调用者应注意的问题
  • 上海推广网站公司网站搭建什么意思
  • 美团-Mtgsig4.0.4逆向-Js逆向
  • 巩义推广网站哪家好制作网站设计的技术有
  • 孝感房地产网站建设建设总承包网站
  • 杭州网站建设服务公司小程序商城源代码
  • SSH运维操作:从基础概念到高级
  • WinSCP下载和安装教程(附安装包,图文并茂)
  • Linux环境基础开发工具
  • 备案期间网站wordpress个人简历主题
  • AI智能体(Agent)大模型入门【8】--关于ocr文字识别图片识别
  • 商城版网站建设网站开发的经验
  • Linux命令--minio安装
  • 长春网站推广网诚传媒互联网服务商
  • 提供网站建设的理由创建私人网站
  • 【Proteus仿真】基于AT89C51单片机的单片机双向通信
  • 温州市网站制作多少钱wordpress 数据库设计
  • 鲅鱼圈网站怎么做分公司vi设计
  • OpenTiny学习中如何快速提升项目效率?
  • 预训练与后训练 区别