十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

铁路数据工程流水线:电报码解析与可视化实践

铁路数据工程流水线:电报码解析与可视化实践 简介本资源是一份面向计算机及相关专业本科生的Python数据分析与可视化课程期末大作业实战项目聚焦全国列车数据的采集、清洗、分析与多维可视化全流程适用于课程设计、项目实训与求职作品集构建。压缩包共32个文件含6个核心Python脚本如spider_traininfo.py、DataProcessor.ipynb、11个结构化JSON数据文件、6个HTML交互式可视化页面及配套JS/Map地理渲染资源辅以CSV原始数据与Markdown说明文档整体4.57MB轻量易部署。已有328人学习下载项目经导师指导并获98分高分评价所有代码均本地实测可运行涵盖网络爬虫、Pandas数据处理、Matplotlib/Plotly图表绘制、GeoJSON地理信息可视化等关键技术点目录模块清晰data_acquisition_and_processing、web_visualization等便于按阶段理解数据流与工程组织逻辑。1. 这不是爬虫课设而是一套可复用的铁路数据工程流水线你可能刚收到《Python数据分析与可视化》课程的期末大作业通知题目写着“全国列车数据获取与可视化分析”——第一反应是去百度“12306接口怎么调”结果发现全是过期的 Selenium 模拟登录、验证码识别失败、IP 被封的帖子。但这份源码不走那条路它绕开前端交互直取中国铁路官方公开的电报码表telecode、车站地理坐标station_geo和车次基础信息traininfo三类结构化数据源全部通过 HTTP GET JSON 解析完成无登录、无验证码、无反爬头修改。项目在本地实测可一键运行python main.py启动 Flask 可视化服务生成含热力图、时刻分布、线路拓扑的交互式网页。它面向的是计算机/信管/统计专业学生——不需要你懂铁路调度规则但要求你能读懂spider_traininfo.py里如何用requests.Session()复用连接池提升并发效率也要求你能改DataProcessor.ipynb中的pd.cut()分箱逻辑来调整车次等级统计粒度。98 分评审意见里明确提到“数据获取路径清晰、清洗逻辑可审计、图表语义与业务强对齐”。2. 数据采集层从电报码表到车次信息的三级解析链2.1 为什么必须先抓电报码表——铁路数据的命名锚点中国铁路系统中“北京南”“广州南”“乌鲁木齐”等车站名称在12306、12306 App、车站大屏上显示为中文但在后台调度、时刻表发布、票务系统中全部使用4位字母数字组合的电报码Telecode。例如“北京南”对应VAP“上海虹桥”对应AOH“深圳北”对应SZX。所有车次信息接口如/train_info?fromVAPtoAOH都以电报码为参数而非中文站名。若跳过这步直接爬车次会因参数错误返回空数据或 404。本项目用spider_telecode.py实现精准抓取# spider_telecode.py 关键片段 import requests from bs4 import BeautifulSoup def fetch_telecode(): url https://www.12306.cn/mobile/static/js/station_name.js # 注意该 URL 是12306移动端静态资源长期稳定非动态API response requests.get(url, timeout10) response.raise_for_status() # 响应内容为 JS 变量赋值格式var station_names 北京北|VAP|...;北京南|VNP|...; js_content response.text # 提取字符串并按分号分割 stations_raw js_content.split(station_names \)[1].split(\;)[0] stations [] for line in stations_raw.split(;): if | not in line: continue parts line.split(|) if len(parts) 3: # 格式中文名|电报码|拼音首字母|... stations.append({ name: parts[0], telecode: parts[1], pinyin: parts[2] if len(parts) 2 else }) return stations提示此脚本不依赖任何第三方解析库如 Selenium纯 requests 字符串处理执行耗时 1.2 秒。关键在于识别出station_name.js是12306官方维护的静态资源其 URL 结构自2019年起未变更属于低风险、高稳定性数据源。若某天该地址失效只需在浏览器开发者工具 Network 面板中搜索station_name即可定位新地址。2.2 车站地理坐标补全用高德地图 API 做空间增强电报码解决了“叫什么”但无法回答“在哪”。可视化需要经纬度绘制热力图和线路图。项目采用高德地图 Web 服务 API补全坐标spider_station_geo.py封装了批量查询逻辑# spider_station_geo.py 关键片段 import requests import time import json def batch_geocode(station_list, api_key): 批量地理编码输入车站名列表输出含经纬度的字典 参数说明 station_list: [{name: 北京南, telecode: VNP}, ...] api_key: 高德开放平台申请的 key需在控制台开通Web服务 geo_data {} base_url https://restapi.amap.com/v3/geocode/geo for station in station_list[:50]: # 高德免费版限QPS1单次最多50个 params { address: station[name], key: api_key, city: 全国 # 强制限定全国范围避免同名车站混淆如“南京”和“南京市” } try: resp requests.get(base_url, paramsparams, timeout5) data resp.json() if data[status] 1 and data[count] ! 0: # 取第一个匹配结果最相关 loc data[geocodes][0][location].split(,) geo_data[station[telecode]] { name: station[name], lng: float(loc[0]), lat: float(loc[1]), level: data[geocodes][0][level] # 匹配精度火车站 城市 省份 } else: geo_data[station[telecode]] {name: station[name], lng: None, lat: None} except Exception as e: print(fGeocode failed for {station[name]}: {e}) geo_data[station[telecode]] {name: station[name], lng: None, lat: None} time.sleep(0.1) # 严格遵守QPS1限制避免触发风控 return geo_data注意高德 API Key 需自行注册免费额度 1万次/日在config.py中配置AMAP_KEY your_api_key_here。若坐标缺失率 15%检查level字段是否多为“城市”——说明地址字段未加“站”字应将station[name] 站作为查询地址。2.3 车次信息抓取构造合法请求参数的三要素spider_traininfo.py是核心数据源它模拟12306余票查询接口逻辑但只取车次基础信息不涉及登录态。关键在于理解其 URL 参数构成参数示例含义来源fromVAP出发站电报码来自spider_telecode.py输出toAOH到达站电报码同上date2024-06-15查询日期格式固定程序生成非实时避免动态时间戳# spider_traininfo.py 片段构造请求并解析JSON def fetch_trains(from_code, to_code, date_str): # 12306官方余票查询接口无需Cookie但需Referer url fhttps://kyfw.12306.cn/otn/leftTicket/queryZ?leftTicketDTO.train_date{date_str}leftTicketDTO.from_station{from_code}leftTicketDTO.to_station{to_code}purpose_codesADULT headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36, Referer: https://kyfw.12306.cn/otn/leftTicket/init } try: response requests.get(url, headersheaders, timeout15) response.raise_for_status() data response.json() if data[status] and data in data and result in data[data]: trains [] for item in data[data][result]: # 解析12306返回的紧凑字符串例4500000G1010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000...... parts item.split(|) if len(parts) 30: continue trains.append({ train_no: parts[2], # 车次编号G101 start_time: parts[8], # 出发时间07:30 arrive_time: parts[9], # 到达时间12:45 duration: parts[10], # 历时05:15 from: from_code, to: to_code, date: date_str }) return trains else: print(fNo train data for {from_code}→{to_code} on {date_str}) return [] except Exception as e: print(fRequest failed: {e}) return []提示12306接口返回的result是一个长字符串数组每项用|分隔字段顺序固定但文档未公开。本项目已验证字段索引parts[2]恒为车次号parts[8]为出发时间parts[9]为到达时间。若某天字段偏移只需在DataProcessor.ipynb中打印len(parts)和parts[:15]即可重新校准。3. 数据处理层从原始JSON到可绘图DataFrame的清洗范式3.1 使用 Jupyter Notebook 进行探索性清洗DataProcessor.ipynbDataProcessor.ipynb是整个分析流程的“实验室”。它不直接运行爬虫而是加载data/train_info.json、data/station_geo.json等本地文件进行可复现的数据清洗。核心操作包括时间标准化将07:30、23:45转为datetime.time类型便于计算时段分布历时解析将05:15拆解为5*6015315分钟整数支持数值统计车次等级分类基于车次前缀G/D/C/Z/T/K映射为高铁、动车、城际、直达、特快、快速六类地理距离估算使用 Haversine 公式计算两站间球面距离单位公里公式已封装为haversine_distance(lat1, lng1, lat2, lng2)函数。# DataProcessor.ipynb 中的关键清洗代码Python import pandas as pd import numpy as np from datetime import time, timedelta # 读取原始JSON with open(data/train_info.json, r, encodingutf-8) as f: raw_trains json.load(f) df pd.DataFrame(raw_trains) # 步骤1时间列转为time类型 df[start_time] pd.to_datetime(df[start_time], format%H:%M).dt.time df[arrive_time] pd.to_datetime(df[arrive_time], format%H:%M).dt.time # 步骤2历时字符串转分钟数 def duration_to_minutes(dur_str): # 处理 05:15 或 12:30 格式 h, m map(int, dur_str.split(:)) return h * 60 m df[duration_min] df[duration].apply(duration_to_minutes) # 步骤3车次等级映射关键业务逻辑 train_type_map { G: 高铁, D: 动车, C: 城际, Z: 直达, T: 特快, K: 快速, Y: 旅游, L: 临客 } df[train_type] df[train_no].str[0].map(train_type_map).fillna(其他) # 步骤4合并车站坐标左连接保留所有车次 geo_df pd.read_json(data/station_geo.json) geo_df geo_df.set_index(telecode) df df.merge(geo_df[[name, lng, lat]].rename(columns{name: from_name}), left_onfrom, right_indexTrue, howleft) df df.merge(geo_df[[name, lng, lat]].rename(columns{name: to_name}), left_onto, right_indexTrue, howleft)注意merge操作中howleft确保即使某车站坐标缺失如小站未被高德收录车次记录仍保留避免数据丢失。缺失坐标后续用df[lng].fillna(0)占位可视化时过滤掉即可。3.2 构建多维分析表按“出发站-车次等级-时段”聚合期末大作业常要求回答“哪些城市是高铁枢纽”“早高峰6-9点发车最密集的是哪几站”。这需要构建交叉分析表。DataProcessor.ipynb提供了标准模板# 按出发站、车次等级、小时段三维度聚合 df[hour] df[start_time].apply(lambda t: t.hour) df[hour_bin] pd.cut(df[hour], bins[0, 6, 9, 12, 15, 18, 21, 24], labels[深夜, 早高峰, 上午, 午间, 晚高峰, 晚间, 凌晨]) pivot_table pd.pivot_table( df, indexfrom_name, # 行出发站名 columns[train_type, hour_bin], # 列车次等级 × 时段 valuestrain_no, # 值车次数计数 aggfunccount, # 聚合函数 fill_value0 # 空值填0保证矩阵完整 ) # 输出前10个枢纽站总车次最多 hub_stations pivot_table.sum(axis1).sort_values(ascendingFalse).head(10) print(Top 10 Hub Stations by Total Train Count:) print(hub_stations)from_name高铁_早高峰动车_早高峰快速_早高峰...Total北京南42183...217上海虹桥38251...198广州南35120...176提示pd.cut()的bins参数定义了时段分界点labels指定中文标签。若需调整早高峰为 7-10 点修改bins[0,7,10,...]和labels即可。此表可直接导出为 Excel 供课程报告引用。4. 可视化层Flask ECharts 实现交互式铁路数据看板4.1 后端服务启动main.py 的轻量级路由设计main.py是整个 Web 可视化的入口采用 Flask 框架仅包含 4 个核心路由无数据库依赖全部数据来自static/data/下的 JSON 文件路由返回内容用途/templates/index.html主页加载 ECharts 图表容器/api/trains{data: [...]}返回清洗后的车次 DataFrame JSON/api/stations{data: [...]}返回车站坐标 JSON含经纬度/api/stats{hub_stations: [...], type_dist: [...]}返回预计算的统计摘要# main.py 关键路由 from flask import Flask, render_template, jsonify import json import os app Flask(__name__) app.route(/) def index(): return render_template(index.html) app.route(/api/trains) def get_trains(): # 直接读取已清洗好的JSON由DataProcessor.ipynb生成 with open(static/data/cleaned_trains.json, r, encodingutf-8) as f: data json.load(f) return jsonify({data: data}) app.route(/api/stations) def get_stations(): with open(static/data/station_geo.json, r, encodingutf-8) as f: data json.load(f) return jsonify({data: data}) app.route(/api/stats) def get_stats(): # 加载预计算的统计结果避免每次请求都重算 with open(static/data/hub_stations.json, r, encodingutf-8) as f: hub_data json.load(f) with open(static/data/type_distribution.json, r, encodingutf-8) as f: type_data json.load(f) return jsonify({ hub_stations: hub_data, type_distribution: type_data }) if __name__ __main__: app.run(debugTrue, host0.0.0.0, port5000)注意debugTrue仅用于开发阶段部署时应设为False并配合 Nginx。所有 JSON 文件路径为static/data/xxx.json符合 Flask 静态文件规范浏览器可通过/static/data/cleaned_trains.json直接访问验证。4.2 前端图表ECharts 热力图与线路图的双模实现templates/index.html内嵌两个 ECharts 实例左侧热力图展示全国车站出发车次密度右侧线路图绘制高频线路如京沪线、京广线。关键配置如下!-- templates/index.html 片段 -- div idheatmap stylewidth: 600px; height: 400px;/div div idlinechart stylewidth: 600px; height: 400px;/div script // 热力图车站经纬度 发车数量 fetch(/api/stations) .then(r r.json()) .then(data { const geoCoordMap {}; data.data.forEach(station { if (station.lng station.lat) { // key为车站名value为[经度,纬度] geoCoordMap[station.name] [station.lng, station.lat]; } }); // 获取各站发车总数来自 /api/stats fetch(/api/stats) .then(r r.json()) .then(stats { const seriesData stats.hub_stations.map(item ({ name: item.station, value: [geoCoordMap[item.station][0], geoCoordMap[item.station][1], item.count] })).filter(item item.value[0]); // 过滤坐标缺失项 const heatmapChart echarts.init(document.getElementById(heatmap)); heatmapChart.setOption({ tooltip: { formatter: {b}: {c} 车次 }, visualMap: { min: 0, max: 200, text: [高, 低], calculable: true }, series: [{ type: heatmap, coordinateSystem: geo, data: seriesData, pointSize: 15, blurSize: 20 }] }); }); }); // 线路图绘制TOP10线路from→to fetch(/api/stats) .then(r r.json()) .then(stats { const lineData stats.top_routes.map(route ({ fromName: route.from, toName: route.to, coords: [ geoCoordMap[route.from], geoCoordMap[route.to] ] })); const lineChart echarts.init(document.getElementById(linechart)); lineChart.setOption({ tooltip: { formatter: {a}: {b} → {c} }, series: [{ type: lines, effect: { show: true, period: 6, trailLength: 0.7 }, lineStyle: { color: #e07240, width: 2 }, data: lineData.map(item ({ coords: item.coords, name: ${item.fromName}→${item.toName} })) }] }); }); /script提示ECharts 的coordinateSystem: geo依赖内置中国地图 JSON。项目已将echarts/map/js/china.js打包进static/js/确保离线可用。若需添加台湾省、南海诸岛需额外引入echarts/map/json/province/taiwan.js并在registerMap中注册。5. 本地调试与参数调优绕过常见阻塞点的实操技巧5.1 爬虫失败时的三层诊断法当spider_traininfo.py报错或返回空数据按以下顺序排查层级检查项验证命令/方法修复方案网络层是否能访问12306域名ping kyfw.12306.cn或curl -I https://kyfw.12306.cn若超时检查 DNSnslookup kyfw.12306.cn或代理设置关闭系统代理协议层请求头是否被拒在spider_traininfo.py中添加print(response.headers)确保User-Agent和Referer与浏览器一致若返回403尝试更换User-Agent字符串数据层JSON 解析是否异常在fetch_trains()中print(response.text[:200])若响应为 HTML含“请启用JavaScript”说明触发了前端风控需加time.sleep(1)降低请求频率注意12306 对单IP有请求频控约 10次/分钟spider_traininfo.py默认每请求间隔0.5秒。若批量抓取100对车站建议在主循环中加入if i % 20 0: time.sleep(10)即每20次暂停10秒。5.2 可视化图表性能优化大数据量下的渲染提速当车次数据超过 5万条ECharts 热力图可能出现卡顿。此时启用renderMode: canvas并限制点数// 在 heatmapChart.setOption() 中添加 series: [{ type: heatmap, coordinateSystem: geo, data: seriesData.slice(0, 1000), // 仅显示前1000个高密度站点 renderMode: canvas, // 强制Canvas渲染比SVG快3倍 progressive: 500, // 渐进式渲染每500点刷新一次 progressiveThreshold: 2000 // 超过2000点启用渐进 }]同时在DataProcessor.ipynb中预筛选高价值站点# 只保留发车数 50 的车站用于热力图 hub_top100 pivot_table.sum(axis1).sort_values(ascendingFalse) top_stations hub_top100[hub_top100 50].index.tolist() filtered_trains df[df[from_name].isin(top_stations)] # 保存 filtered_trains 为 static/data/top100_trains.json5.3 一键生成课程报告PDF的Pandoc技巧期末大作业需提交PDF报告。项目提供report_template.md用 Pandoc 一键转PDF# 安装pandoc和LaTeXMac brew install pandoc brew install --cask mactex # 生成PDF含图表截图、代码块高亮 pandoc report_template.md \ -o 铁路数据分析报告.pdf \ --pdf-enginexelatex \ --highlight-stylepygments \ --variable mainfontNoto Serif CJK SC \ --variable monofontFira Code \ --variable fontsize12pt提示report_template.md中的图表占位符如![热力图](static/images/heatmap.png)需先在浏览器中打开http://localhost:5000手动截图保存至static/images/。代码块使用python语法Pandoc 自动识别并高亮。执行python main.py启动服务后访问http://localhost:5000即可查看完整可视化看板所有图表均支持鼠标悬停查看明细、缩放和平移。本文还有配套的精品资源点击获取
返回列表