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

Python爬虫实战:抓取12306火车站三字码数据

1. 项目概述与核心价值

最近在做一个跟火车票数据相关的自动化工具,发现很多第三方接口或者本地数据处理,都需要用到火车站的标准三字码。比如,你想查“北京”到“上海”的余票,直接传中文站名过去,很多系统是不认的,必须用“BJP”和“SHH”这样的代码。12306作为官方数据源,它的站名-代码映射表无疑是最权威、最及时的。手动去官网查?站台上千个,显然不现实。所以,写个Python爬虫来自动化抓取这份映射关系,就成了一个非常实际且高频的需求。这个项目,就是带你一步步搞定这个任务,从分析网页结构到稳定获取数据,最后整理成一份干净可用的字典或CSV文件。无论你是想学习基础的网页抓取,还是需要这份数据来做数据分析、抢票脚本或者旅行应用,这篇实操指南都能给你一个清晰的路径。

2. 目标网站分析与技术选型

2.1 12306火车站信息页面探秘

我们的目标是12306官网上的车站代码查询页面。直接访问https://www.12306.cn/index/script/core/common/station_name_v10026.js这个链接(注意版本号v10026可能会变),你会发现它返回了一个JavaScript文件。打开一看,数据以一种非常“紧凑”的格式存储着:

@bjb|北京北|VAP|beijingbei|bjb|0@bjd|北京东|BOP|beijingdong|bjd|1@bji|北京|BJP|beijing|bj|2@bjn|北京南|VNP|beijingnan|bjn|3@bjx|北京西|BXP|beijingxi|bjx|4@bjy|北京朝阳|BJP|chaoyang|bjy|5...

这种格式对于网络传输非常高效,每个车站信息用@符号分隔,内部再用|管道符分割不同字段。我们需要从中解析出中文站名和对应的三字码。选择直接抓取这个JS文件,而不是渲染后的HTML页面,是因为它数据纯净,没有反爬干扰(如复杂的JavaScript渲染或图片验证),是典型的“数据接口”型页面,非常适合爬虫初学者入门,也适合需要稳定数据源的开发者。

2.2 工具链选择与理由

对于这个任务,我们的工具链力求简洁高效:

  • 请求库:requests。这是Python HTTP客户端库的事实标准,简单易用,功能强大。相比于urllib,它的API更加人性化。在这个项目中,我们只需要发起一个简单的GET请求,requests绰绰有余。
  • 解析与处理:纯Python字符串方法。由于数据格式是规则且简单的分隔文本(@|),使用Python内置的str.split()方法进行分割,比引入BeautifulSouplxml等HTML解析库更轻量、更快速。正则表达式(re)在这里也可以,但杀鸡焉用牛刀,split()的代码可读性更高。
  • 数据存储:csv模块或直接构造字典。最终我们需要的是“站名-三字码”的映射关系。根据后续使用场景,可以将其存储为Python字典在内存中使用,也可以用csv模块写入stations.csv文件,方便用Excel打开或被其他程序读取。json格式也是一个好选择,尤其适合Web应用。

注意:虽然这个JS文件目前看起来没有反爬措施,但我们在编写爬虫时仍需遵守基本的网络礼仪。务必设置合理的请求头(User-Agent),并考虑添加短暂的延时(例如time.sleep(1)),即使单次请求,养成好习惯也对未来爬取其他网站有益。

3. 爬虫核心实现步骤拆解

3.1 环境准备与依赖安装

首先确保你的Python环境已经就绪(建议使用Python 3.6以上版本)。这个项目依赖极少,主要就是requests库。打开你的终端或命令行,使用pip安装:

pip install requests

如果安装速度慢,可以考虑使用国内的镜像源,例如清华源:

pip install requests -i https://pypi.tuna.tsinghua.edu.cn/simple

安装完成后,创建一个新的Python脚本文件,比如fetch_12306_stations.py,就可以开始编码了。

3.2 发起请求与获取原始数据

第一步是获取那个包含所有车站信息的JS文件内容。我们需要模拟浏览器发起一个GET请求。

import requests def fetch_station_data(): # 目标URL,注意版本号可能变化,如果失效需要手动更新 url = ‘https://www.12306.cn/index/script/core/common/station_name_v10026.js‘ # 设置请求头,模拟浏览器访问,这是一个好习惯 headers = { ‘User-Agent‘: ‘Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36‘ } try: response = requests.get(url, headers=headers) # 检查请求是否成功(状态码200) response.raise_for_status() # 假设响应内容编码是UTF-8,如果不是可能需要调整 response.encoding = ‘utf-8‘ return response.text except requests.exceptions.RequestException as e: print(f“请求数据失败: {e}“) return None if __name__ == ‘__main__‘: raw_data = fetch_station_data() if raw_data: print(“成功获取原始数据,前500个字符为:“) print(raw_data[:500])

这段代码做了几件事:定义目标URL,设置一个常见的User-Agent头让服务器认为我们是普通浏览器,然后使用requests.get()发起请求。response.raise_for_status()会在状态码不是200时抛出异常,便于我们及时发现问题。最后打印前500个字符,是为了确认我们确实拿到了预期的数据格式。

3.3 数据解析:从混乱文本到结构化字典

拿到raw_data后,你会发现它并不是一个纯粹的数组字符串,很可能被包裹在一个JavaScript变量赋值语句里,比如var station_names =‘@bjb|北京北|...‘;。我们需要先把有效的数据部分提取出来。

def parse_station_data(raw_text): “““ 解析原始JS文本,提取车站名和三字码的映射。 参数: raw_text - requests返回的原始文本 返回: 一个字典,格式为 {‘北京北‘: ‘VAP‘, ‘北京东‘: ‘BOP‘, ...} “““ # 1. 找到有效数据的起始位置。通常数据在第一个单引号对之间。 # 查找第一个‘@‘符号的位置,它标志着数据段的开始。 start_index = raw_text.find(‘@‘) if start_index == -1: print(“未在响应中找到车站数据起始标记‘@‘“) return {} # 2. 提取从‘@‘开始到行尾(或分号)之前的数据字符串。 # 简单处理:找到从start_index开始,到下一个分号‘;‘之前的内容。 end_index = raw_text.find(‘;‘, start_index) if end_index == -1: # 如果没有分号,则取到末尾 data_string = raw_text[start_index:] else: data_string = raw_text[start_index:end_index] # 3. 按‘@‘分割,得到每个车站的字符串 stations_raw_list = data_string.split(‘@‘)[1:] # 第一个元素是空字符串,跳过 station_dict = {} # 4. 遍历每个车站字符串,按‘|‘分割,提取所需字段 for station in stations_raw_list: parts = station.split(‘|‘) # 根据观察,常见格式为:电报码|站名|三字码|拼音|简拼|序号(?) # 例如:”bjb|北京北|VAP|beijingbei|bjb|0“ if len(parts) >= 3: # 确保有足够字段 chinese_name = parts[1] telegraph_code = parts[2] # 这就是我们需要的三字码 station_dict[chinese_name] = telegraph_code return station_dict

解析函数的核心逻辑是字符串分割。首先定位有效数据,然后通过split(‘@‘)将整个长字符串拆分成一个个车站单元,再对每个单元用split(‘|‘)拆出字段。这里的关键是确定字段顺序,通过观察样本数据,我们确定索引为1的是中文站名,索引为2的是三字码。将其以键值对形式存入字典。

实操心得:在写解析逻辑时,一定要先打印出几个完整的车站单元样本仔细研究。不同时期12306的JS文件字段顺序或数量可能有微调。确保你的parts索引抓取的是正确的字段。另外,增加if len(parts) >= 3:这样的判断是鲁棒性编程的好习惯,可以避免因数据格式意外变化导致程序崩溃。

3.4 数据存储与持久化

解析得到字典后,我们可以根据需求选择保存方式。

方案一:保存为CSV文件(推荐,便于查看和交换)

import csv def save_to_csv(station_dict, filename=‘12306_stations.csv‘): “““将车站字典保存为CSV文件“““ with open(filename, ‘w‘, newline=‘‘, encoding=‘utf-8-sig‘) as csvfile: # 使用utf-8-sig编码可以在Excel中正确显示中文 writer = csv.writer(csvfile) # 写入表头 writer.writerow([‘车站中文名‘, ‘三字码‘]) # 写入数据 for name, code in station_dict.items(): writer.writerow([name, code]) print(f“数据已保存至 {filename},共 {len(station_dict)} 条记录。“)

方案二:保存为JSON文件(适合Web应用或配置)

import json def save_to_json(station_dict, filename=‘12306_stations.json‘): “““将车站字典保存为JSON文件“““ with open(filename, ‘w‘, encoding=‘utf-8‘) as jsonfile: json.dump(station_dict, jsonfile, ensure_ascii=False, indent=2) # ensure_ascii=False 保证中文正常显示 # indent=2 使文件有缩进,更易读 print(f“数据已保存至 {filename}。“)

方案三:在Python中直接使用字典如果只是本次脚本运行期间使用,那么parse_station_data函数返回的字典对象已经可以直接用了,例如station_dict.get(‘北京‘)会返回‘BJP‘

3.5 完整脚本整合与执行

将上述所有函数整合,并添加主程序逻辑。

import requests import csv import json import time # 此处插入上面定义的 fetch_station_data, parse_station_data, save_to_csv, save_to_json 函数 def main(): print(“开始抓取12306车站三字码...“) # 1. 获取数据 raw_text = fetch_station_data() if not raw_text: print(“获取原始数据失败,程序退出。“) return # 2. 解析数据 print(“正在解析数据...“) station_dict = parse_station_data(raw_text) if not station_dict: print(“解析数据失败,未得到有效车站信息。“) return print(f“解析成功,共获取 {len(station_dict)} 个车站信息。“) # 3. 保存数据(按需选择一种或多种) # 保存为CSV save_to_csv(station_dict) # 保存为JSON save_to_json(station_dict) # 4. 简单演示查询 print(“\n--- 演示查询 ---“) test_stations = [‘北京‘, ‘上海‘, ‘广州‘, ‘深圳‘, ‘成都‘] for station in test_stations: code = station_dict.get(station) if code: print(f“{station} -> {code}“) else: print(f“未找到车站: {station}“) print(“\n任务完成!“) if __name__ == ‘__main__‘: main() # 可选:添加一个短暂延时,以示友好 # time.sleep(1)

运行这个脚本,你应该能在同目录下得到12306_stations.csv12306_stations.json两个文件,并在控制台看到关键车站的代码输出。

4. 关键问题排查与进阶优化

4.1 常见问题与解决方案

在实际操作中,你可能会遇到以下问题:

问题现象可能原因解决方案
请求失败,返回状态码403/4041. URL中的版本号(如v10026)已过期。
2. 网站屏蔽了简单的爬虫请求。
1.手动更新URL:访问12306官网,按F12打开开发者工具,在Network(网络)选项卡中刷新页面,搜索station_name相关的JS文件,找到最新的URL替换。
2.完善请求头:除了User-Agent,可以尝试添加Referer(来源页)等头信息,模拟更真实的浏览器行为。
解析出的字典为空或数据错乱1. 数据格式发生变化,字段顺序或分隔符改变。
2. 提取有效数据字符串的逻辑有误。
1.打印并检查原始数据片段:在解析前,print(raw_text[:1000]),确认数据格式是否还是`@xxx
保存的CSV文件用Excel打开中文乱码Excel默认可能不识别UTF-8 without BOM编码。open()函数中,将编码encoding=‘utf-8‘改为encoding=‘utf-8-sig‘,这个BOM头能帮助Excel正确识别。
脚本偶尔运行成功,偶尔失败网络不稳定,或触发了网站轻微的频率限制。加入异常重试机制延时。使用requeststimeout参数,并用try-except包裹请求,失败后等待几秒重试。

4.2 代码健壮性优化建议

一个用于生产环境的爬虫,需要考虑更多的边界情况和稳定性。

1. 增加重试机制:

import time from requests.adapters import HTTPAdapter from requests.packages.urllib3.util.retry import Retry def create_session_with_retries(retries=3, backoff_factor=0.5): “““创建一个带重试机制的requests Session“““ session = requests.Session() retry_strategy = Retry( total=retries, backoff_factor=backoff_factor, # 重试等待时间:{backoff_factor} * (2^{重试次数-1})秒 status_forcelist=[500, 502, 503, 504], # 遇到这些状态码才重试 ) adapter = HTTPAdapter(max_retries=retry_strategy) session.mount(“http://“, adapter) session.mount(“https://“, adapter) return session # 在fetch_station_data函数中,使用session代替直接的requests.get session = create_session_with_retries() response = session.get(url, headers=headers, timeout=10) # 设置超时

2. 更灵活的数据提取:如果数据格式包裹方式多变,可以使用正则表达式来匹配核心数据部分,容错性更强。

import re def extract_data_string(raw_text): “““使用正则表达式提取车站数据字符串“““ # 匹配模式:以@开头,包含大量|和字符,直到分号或字符串结束 pattern = r‘(@[a-z]+\|[^;]+)‘ match = re.search(pattern, raw_text) if match: return match.group(1) else: # 备用模式:尝试匹配单引号内的所有内容 pattern2 = r“station_names\s*=\s*‘([^‘]+)‘“ match2 = re.search(pattern2, raw_text) return match2.group(1) if match2 else None

3. 数据验证与清洗:在存入字典前,可以对站名和代码进行简单的清洗和验证,比如去除首尾空白,检查三字码是否为大写字母和数字的组合等。

4.3 应用场景延伸

获取到这份站名-代码映射表后,它的用处远不止于查询:

  1. 构建本地车站数据库:作为抢票脚本、余票查询工具的后端数据支撑。
  2. 数据清洗与关联:在处理第三方火车票订单或日志数据时,用于将代码转换为可读的站名,或反之。
  3. 旅行类应用开发:开发行程规划、票价分析等应用时,这是必不可少的基础数据。
  4. 数据分析:结合车次数据,可以分析城市间的铁路连接度、热门线路等。

你可以定期运行这个爬虫脚本(例如每周一次),以更新本地数据,确保与12306官方数据同步。将脚本部署到服务器,搭配定时任务(如Linux的cron或Windows的任务计划程序),即可实现全自动化更新。

5. 法律与道德边界提醒

最后,也是最重要的一点,我们必须明确爬虫行为的边界。12306是中国铁路客户服务中心官方网站,承载着巨大的公共服务职能。

  • 遵守robots.txt:虽然技术上我们可以抓取这个JS文件,但首先应检查https://www.12306.cn/robots.txt,尊重网站所有者设置的爬虫协议。如果明确禁止抓取相关路径,则应停止。
  • 控制访问频率:本项目仅单次请求获取静态数据,对服务器造成的压力微乎其微,这是合理的。绝对禁止为了监控变化而进行高频、循环的请求,这会对服务器造成不必要的负担,可能被视为恶意攻击。
  • 数据用途:获取的数据应用于个人学习、研究或合法的项目开发。严禁用于商业倒卖、干扰12306系统正常运行(如开发恶意抢票软件对服务器造成压力)、或任何侵犯他人权益的用途。
  • 规避反爬是技术探讨,而非鼓励对抗:文中提到的完善请求头等方法,是为了让请求更“像”正常浏览器,提高成功率。这不应被理解为教唆去刻意绕过网站的安全防护措施。技术的运用必须合乎法律与道德。

写爬虫,技术是手段,责任是底线。在动手之前,多想一想“应不应该”,和“怎么实现”同样重要。这个项目为你提供了一个绝佳的实战场景,来练习HTTP请求、数据解析、文件操作和错误处理这些核心技能。希望你在获取数据的同时,也能建立起对网络数据采集的正确认知。

http://www.jsqmd.com/news/1382747/

相关文章:

  • 数字时代一人公司如何构建护城河:超越信息差与标准化竞争
  • AI制药必备公开数据集全解析:从MoleculeNet到PDBbind的实战指南
  • Java Lambda表达式与Stream API实战:从语法到性能优化的完整指南
  • SelectDB实时更新与倒排索引:物流海量数据秒级查询实战
  • ComfyUI 0.28+ 降级兼容方案:快速回退与多版本共存指南
  • AI Agent:为LLM装上手脚,突破原生大模型的五大能力边界
  • Doris数据库建表实战:从核心概念到高效表结构设计
  • 大模型学习路径:从理论到工程实践的完整指南
  • 微前端架构实战:基于micro-app的沙箱隔离与子应用集成指南
  • 9大网盘直链解析工具终极指南:免费获取真实下载地址的完整教程
  • LLM并发工具调用实战:幂等性、竞态条件与失败补偿的5大生产级陷阱
  • 为QQ机器人构建可观测链路:基于DAG的黑匣子设计与实现
  • 从“龙虾”到“悟空”:深度体验阿里AI助手如何重塑工作流与效率
  • 深入理解Makefile的include指令:模块化构建与工程实践
  • Temu和亚马逊有什么区别?核心差异深度对比
  • 2026微信语音转文字突然用不了对比评测:我的实操成本经验
  • 建站前先别急,这份网站建设准备资料清单让你少走三年弯路
  • 2026年8月东莞皮雕软包成型机/东莞印花烘干烤箱靠谱公司推荐_东莞勋聚机械科技有限公司 - 行业平台推荐
  • Dev-C++ 安装与配置全攻略:从版本选择到第一个C++程序
  • RFM客群细分AI:从数据洞察到自动化策略的工程实践
  • Claude Code高效使用:语义提问工作流解析
  • Edge浏览器收藏夹默认新标签页打开的4种解决方案与效率优化
  • 数学建模国赛A题解析:机理模型构建、数值求解与数据校验全流程指南
  • 从AI编程助手到AI工程智能体:Harness框架如何重塑软件运维
  • M2 Mac与.m2文件转PDF全攻略:原生方案、工具选择与排错技巧
  • React+Node.js构建AI聊天应用:从零实现实时对话与流式响应
  • 插件注入与移除:从原理到实战的安全扩展技术指南
  • 口碑好的值班岗亭厂:2026年严选 - 品牌推广大师
  • CAD2020系统自学指南:112课时从零到精通的实战路线图
  • SlopCodeBench:渐进披露机制下的大语言模型代码重构能力评估实战