ARTICLE DETAIL

资讯详情

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

用Python把列车进站记录变成结构化数据:EXIF、OCR与归档实践

用Python把列车进站记录变成结构化数据:EXIF、OCR与归档实践 在常州北站 2 站台记录到一列 CR400BF-A-5058 担当 G1802 次列车进站这个场景对于铁路摄影爱好者和数据控来说并不陌生。很多人看到列车进站后会随手拍下照片但过一段时间再回头整理往往只剩下一张图片车次、车型、车号、站台这些关键信息早就记不清了。与其事后凭记忆补录不如从一开始就把“进站记录”数据化。本文以这趟 G1802 次列车进常州北站 2 台的记录为例拆解一套可以复用的“列车进站记录卡”生成思路包含信息要素、查询方法、Python 自动归档与基础可视化。即使你此前没有接触过任何编程也可以照着本文把零散照片整理成结构化的 Markdown 和 CSV 数据。整套方案以 Python 为主会用到图片 EXIF 信息读取、OCR 辅助识别车号、公开车次信息查询、Markdown 文档自动生成等知识点。对于刚接触 Python 的读者这是一次很好的综合练习对于已经在做铁路拍摄记录的爱好者这套脚本也可以直接改造成个人“拍车日志”工具。需要提前说明的是文中所有列车运行数据、车型担当信息都应以铁路 12306 官方渠道和实际运行图为准技术接口仅用于学习演示查询频率务必克制更不能把脚本用于商业或非法用途。1. 背景与核心概念1.1 一条完整进站记录包含哪些信息要记录一次列车进站表面上只需要“车次 站台”就够了但真正有价值的历史记录应当包含更多字段。以“CR400BF-A-5058 担当 G1802 次列车进常州北站 2 台”这句话为例里面至少包含车型CR400BF-A这是复兴号动车组的一种型号。车号5058表示这列车底的编号。车次G1802表示当前执行的高速动车组旅客列车车次。车站常州北站。站台2 台也就是停靠在常州北站的第 2 站台。日期和时间进站发生的具体日期与时刻。方向列车是上行还是下行进站方向是北京方向还是上海方向。照片记录现场拍摄的图片文件最好保留原始文件并附上拍摄时间。这些字段组合起来才能形成一条可以检索、可以统计、可以长期归档的“进站记录”。如果只用笔记本手工记不仅容易丢失后续想做车型分布、站台使用频率、车次与车号对应关系等统计也会非常困难。1.2 CR400BF-A 是什么车CR400BF-A 是复兴号动车组大家庭中的一员。其中“CR”代表中国铁路“400”代表设计速度等级达到 400 公里/小时级别“BF”代表该型号属于“复兴号”家族中的某系列产品而“A”通常代表长编组或特定子型号。和短编组相比CR400BF-A 的车厢数量更多、运力更大车身外观上也会体现出明显的长编组特征。车身上的车号比如“5058”相当于这列车的“身份证号”通过车号可以追踪这列车底的配属和运行记录。需要提醒的是铁路车型参数、编组定员、配属信息会随着车辆运用和调度安排发生变化不同时期的公开资料可能有差异。如果你在文章或视频里需要引用这些参数建议优先查阅官方发布或铁路专业书籍不要只依赖某张网络截图。1.3 G1802 次列车与常州北站G 字头列车是高速动车组旅客列车G1802 是其中一个具体车次。车次号并不是随机的它在铁路运行图中代表一条固定的运行线但担当这个车次的车底可能每天不同可能由 CR400BF-A 执行也可能临时换成其他车型。因此在记录时写清“当日担当车号为 5058”非常必要。常州北站是京沪高铁沿线的一座车站具体站台布局、股道用途在不同时期可能微调。常见的站台编号规则中“2台”通常指车站的第 2 站台也就是列车停靠股道对应的站台。判断列车究竟停靠哪一个站台最准确的方式是看站台上的“列车信息屏”、听广播或者参考 12306 的“车站大屏”信息。不要仅凭照片中列车朝向猜测。1.4 为什么要把记录工具化手工记录最大的问题是“不可检索、不可统计、不可校验”。把记录工具化之后好处非常明显第一字段固定。每次记录都会按照同一个模板生成不会漏掉车号、站台这些关键信息。第二自动归档。手机照片的 EXIF 信息里带有拍摄时间甚至 GPS 坐标脚本可以把这些信息抽出来直接填到记录里减少手工录入。第三方便统计。记录一多自然想分析“我最常拍到哪个型号”“哪个站台出镜率最高”“哪些车次最近换了车底”。这些分析在结构化数据面前只是几条 pandas 语句的事。第四便于分享。生成 Markdown 或 CSV 后可以发布到博客、GitHub也可以导入 Excel 继续处理。2. 环境准备与版本说明本文示例基于以下环境版本需要根据你的项目实际情况调整重点演示配置思路操作系统Windows 10/11macOS 或 Linux 均可。Python 版本3.9 或更高版本建议使用 3.10。开发工具PyCharm、VS Code或者直接使用命令行编辑器。依赖库requests、pandas、Pillow、exifread、opencv-python、pytesseract、matplotlib、charset-normalizer。外部程序Tesseract OCR。该程序需要单独安装Windows 用户可以从开源镜像或官方仓库下载对应版本安装后把tesseract.exe所在目录加入系统 PATH。建议先创建 Python 虚拟环境避免依赖冲突。python -m venv train-envWindows 激活虚拟环境train-env\Scripts\activatemacOS 或 Linux 激活虚拟环境source train-env/bin/activate然后升级 pip 并安装依赖pip install --upgrade pip pip install requests pandas Pillow exifread opencv-python pytesseract matplotlib如果你的网络环境安装较慢可以使用国内镜像源例如pip install requests pandas Pillow exifread opencv-python pytesseract matplotlib -i https://pypi.tuna.tsinghua.edu.cn/simpleTesseract OCR 安装完成后可以先在命令行验证一下tesseract --version如果命令找不到说明没有配置 PATH需要在系统环境变量中添加 Tesseract 安装目录。OCR 不是必选项如果只是做基础记录可以先跳过这一部分后续再补充。3. 核心数据与查询原理3.1 车次、车型、车号从哪里获取要生成一条高可信度的进站记录信息来源不能只有一个。常用渠道有以下几类第一类是铁路官方渠道。12306 官网、12306 App、车站大屏是最权威的信息来源。通过 12306 可以查询车次时刻表、停站信息、正晚点信息车站大屏可以看到当前车次实际停靠站台。第二类是车迷常用的时刻表软件。例如“路路通”“盛名时刻表”等工具可以离线查询车次、交路、担当车型等信息。这类工具的数据来自热心网友整理和公开资料下载和更新需要注意来源可靠性数据最终仍需与官方信息核对。第三类是现场观察。车型可以通过车头外观、车厢数量、涂装标识判断车号可以通过车头或车身喷涂的白色数字识别。如果拍得够清晰后续还可以通过 OCR 自动识别。不过“某车次由某车号担当”并不是永远固定的。铁路部门会根据检修计划、客流情况、临时调度调整车底。因此做记录时建议采用“当日担当”的思路而不是把某车次与某车型长期绑定。3.2 站台与方向怎么判断列车进出站有固定的股道和方向。常州北站所在铁路线的上下行方向可以参考“列车运行方向”规则通常上行方向是开往北京方向或指定的上行方向下行方向是远离北京方向或指定的下行方向。但不同线路、不同车站可能存在例外所以不应当只凭“往北走还是往南走”来判断上下行。站台编号判断最准确的方法是听车站广播、看站台信息屏。如果拍车位置在站台末端也可以结合站台编号标志牌判断。需要注意的是高速铁路车站站台安全要求非常严格拍摄时绝对不要越过站台安全白线不要使用可能侵入线路的拍摄设备更不要进入轨道区域。3.3 OCR 识别车号的原理OCR 的全称是 Optical Character Recognition光学字符识别。车号通常由字母和数字组成印刷字体相对规范因此具备自动识别的可行性。基本思路是先把照片转换为灰度图再通过二值化增强字符与背景的对比度然后截取车号区域交给 OCR 引擎识别。由于现场光线、角度、车速、玻璃反光等因素影响OCR 的准确率很难达到 100%所以识别结果只能作为辅助最终应该由人工确认。3.4 公开车次信息查询接口的边界12306 网页版提供了一些公开查询接口可以用于查询车次余票、停站等信息。作为学习用途可以通过浏览器开发者工具查看网络请求然后模拟请求获取数据。但必须遵守以下边界控制请求频率不要高并发请求避免对官方服务造成压力。不要利用接口做抢票、刷票等违规操作。接口地址和参数可能随网页改版变化文章示例请以实际抓包结果为准。查询到的数据仅用于个人学习与记录不要用于商业或非法用途。4. 完整实战构建进站记录卡生成器下面开始编写一个完整的 Python 小工具实现“输入照片路径和部分手工信息输出 Markdown 记录卡和 CSV 数据”的功能。4.1 项目结构建议创建如下目录结构train-record/ ├── main.py ├── record.py ├── photo_utils.py ├── ocr_utils.py ├── station_query.py ├── photos/ │ └── g1802_changzhoubei.jpg └── output/ ├── records.json └── records.md其中record.py定义记录模型photo_utils.py负责读取照片 EXIF 和 GPSocr_utils.py负责车号 OCRstation_query.py负责查询车次信息main.py负责组装整个流程。4.2 定义记录数据模型为了让所有记录字段统一先用dataclass定义记录结构。这样可以避免手写字典时打错字段名也能方便后续转 JSON、CSV。# 文件路径record.py from dataclasses import dataclass, asdict from datetime import datetime from typing import Optional dataclass class TrainRecord: record_id: str date: str time: str train_no: str train_type: str train_number: str station: str platform: str direction: str photo_path: str note: str def to_dict(self): return asdict(self) classmethod def from_dict(cls, data: dict): return cls(**data)字段含义record_id记录唯一标识建议用时间戳生成例如20250601_143000_G1802。date进站日期格式YYYY-MM-DD。time进站时间格式HH:MM:SS。train_no车次例如G1802。train_type车型例如CR400BF-A。train_number车号例如5058。station车站例如常州北。platform站台编号例如2台。direction运行方向例如上行或下行。photo_path照片路径可以填相对路径或绝对路径。note备注比如“当日车底担当临时调整”。to_dict()方法方便转 JSONfrom_dict()方法方便从文件读取。4.3 读取照片 EXIF 信息与 GPS照片的 EXIF 信息里包含拍摄时间、相机型号、镜头参数部分手机照片还包含 GPS 坐标。用 Pillow 可以直接读取。# 文件路径photo_utils.py from PIL import Image from PIL.ExifTags import TAGS, GPSTAGS from datetime import datetime from typing import Optional def get_exif_data(image_path: str) - dict: 读取照片 EXIF 信息返回字典。 image Image.open(image_path) raw_exif image.getexif() if not raw_exif: return {} exif {} for tag_id, value in raw_exif.items(): tag_name TAGS.get(tag_id, tag_id) exif[tag_name] value return exif def get_gps_data(exif: dict) - Optional[dict]: 从 EXIF 中提取 GPS 信息。 gps_raw exif.get(GPSInfo) if not gps_raw: return None gps {} for tag_id, value in gps_raw.items(): tag_name GPSTAGS.get(tag_id, tag_id) gps[tag_name] value return gps def _convert_to_degrees(value): 将 EXIF 中的 GPS 坐标格式转换为十进制度数。 d, m, s value return d m / 60.0 s / 3600.0 def get_lat_lng(gps: dict) - Optional[tuple]: 返回 (纬度, 经度)如果 GPS 信息不完整则返回 None。 if not gps or GPSLatitude not in gps or GPSLongitude not in gps: return None lat _convert_to_degrees(gps[GPSLatitude]) lng _convert_to_degrees(gps[GPSLongitude]) if gps.get(GPSLatitudeRef) S: lat -lat if gps.get(GPSLongitudeRef) W: lng -lng return lat, lng def get_shoot_time(exif: dict) - Optional[str]: 从 EXIF 中提取拍摄时间并格式化为字符串。 raw_time exif.get(DateTimeOriginal) if not raw_time: return None try: dt datetime.strptime(raw_time, %Y:%m:%d %H:%M:%S) return dt.strftime(%Y-%m-%d %H:%M:%S) except ValueError: return raw_time这里需要注意的是不同手机厂商的 EXIF 字段可能略有差异部分手机照片在传输或压缩后会丢失 GPS 信息。如果get_lat_lng返回None工具不会崩溃后续可以手工补充车站和站台信息。4.4 使用 OCR 辅助识别车号OCR 部分使用pytesseract和opencv-python。由于车号一般位于车头或车身建议先人工截取车号区域保存成小图再交给脚本识别这样准确率会高很多。# 文件路径ocr_utils.py import cv2 import pytesseract def preprocess_image(image_path: str) - str: 读取图像并做灰度、二值化预处理返回临时图像路径。 image cv2.imread(image_path) if image is None: raise FileNotFoundError(f无法读取图像: {image_path}) gray cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) # 使用 Otsu 自适应阈值增强字符与背景的对比度 _, binary cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY | cv2.THRESH_OTSU) temp_path image_path _temp.png cv2.imwrite(temp_path, binary) return temp_path def recognize_car_number(image_path: str) - str: 识别车号。建议传入已经裁切好的车号区域图片。 返回识别到的字符串识别失败时返回空字符串。 temp_path preprocess_image(image_path) try: text pytesseract.image_to_string( temp_path, langeng, config--psm 7 -c tessedit_char_whitelist0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ ) return text.strip() finally: # 清理临时文件 import os if os.path.exists(temp_path): os.remove(temp_path)--psm 7表示把图片视为单行文本适合车号这类短字符串。tessedit_char_whitelist限制了只识别数字和大写字母可以减少无关字符干扰。OCR 不是万能的如果灯光太强、车号反光或者拍摄角度太斜识别结果可能为空或错误。建议在调用后打印结果由人工确认再写入记录。4.5 查询车次基础信息下面用一个示例演示如何通过 12306 网页公开接口查询车次信息。这里强调一点接口地址和参数需要以你在浏览器中抓到的实际请求为准不同版本可能不同。# 文件路径station_query.py import requests def query_train_info( train_date: str, from_station_code: str, to_station_code: str, ) - dict: 查询指定日期 from 到 to 的车次列表。 train_date: 日期格式 2025-06-01 from_station_code: 出发站电报码例如 SHH 表示上海 to_station_code: 到达站电报码 query_url https://kyfw.12306.cn/otn/leftTicket/queryG params { leftTicketDTO.train_date: train_date, leftTicketDTO.from_station: from_station_code, leftTicketDTO.to_station: to_station_code, purpose_codes: ADULT, } headers { User-Agent: ( Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0 Safari/537.36 ), Referer: https://kyfw.12306.cn/otn/leftTicket/init, } response requests.get(query_url, paramsparams, headersheaders, timeout10) response.raise_for_status() return response.json()注意车站代码通常是 3 位电报码比如常州北站的具体电报码需要查询车站字典不要凭记忆写死。查询频率一定要低如果遇到请求失败先检查参数和请求头不要反复重试。实际项目中更推荐直接在 12306 App 或网页查询车次脚本只用来做小数据量的辅助核对。4.6 生成 Markdown 记录卡有了记录模型就可以生成一份可读性很强的 Markdown 文件。下面这个函数会把单条记录渲染成一个模板。# 文件路径record.py 中追加 def to_markdown(record: TrainRecord) - str: return f## 进站记录{record.train_no} - 日期{record.date} - 时间{record.time} - 车次{record.train_no} - 车型{record.train_type} - 车号{record.train_number} - 车站{record.station} - 站台{record.platform} - 方向{record.direction} - 照片{record.photo_path} - 备注{record.note if record.note else 无} --- 如果有多条记录可以把它们拼接起来写入同一个 Markdown 文件方便在博客或 GitHub 上展示。4.7 主流程组装最后在main.py中把以上功能串起来。# 文件路径main.py import json import sys from datetime import datetime from record import TrainRecord, to_markdown from photo_utils import get_exif_data, get_gps_data, get_lat_lng, get_shoot_time from ocr_utils import recognize_car_number def build_record( train_no: str, station: str, platform: str, direction: str, photo_path: str, train_type: str , train_number: str , ): exif get_exif_data(photo_path) shoot_time get_shoot_time(exif) gps get_gps_data(exif) lat_lng get_lat_lng(gps) if gps else None if shoot_time: date_str, time_str shoot_time.split( ) else: date_str datetime.now().strftime(%Y-%m-%d) time_str datetime.now().strftime(%H:%M:%S) if not train_number: # 如果照片里截取了车号区域可以尝试 OCR 识别 train_number recognize_car_number(photo_path) record_id f{date_str.replace(-, )}_{time_str.replace(:, )}_{train_no} record TrainRecord( record_idrecord_id, datedate_str, timetime_str, train_notrain_no, train_typetrain_type, train_numbertrain_number, stationstation, platformplatform, directiondirection, photo_pathphoto_path, notefGPS: {lat_lng} if lat_lng else , ) return record def main(): record build_record( train_noG1802, station常州北, platform2台, direction上行, photo_pathphotos/g1802_changzhoubei.jpg, train_typeCR400BF-A, train_number5058, ) md to_markdown(record) print(md) with open(output/records.md, a, encodingutf-8) as f: f.write(md \n) try: with open(output/records.json, r, encodingutf-8) as f: records json.load(f) except FileNotFoundError: records [] records.append(record.to_dict()) with open(output/records.json, w, encodingutf-8) as f: json.dump(records, f, ensure_asciiFalse, indent2) if __name__ __main__: main()运行命令python main.py预期输出是一段 Markdown 文本同时会在output目录下生成records.md和records.json。这里的照片路径需要替换成你自己的实际文件。如果不想依赖 OCR可以直接在参数里传入车号脚本会跳过识别流程。5. 进阶把记录转成可视化报表记录多了之后单纯的 Markdown 已经不够直观。可以把records.json导入 pandas做一些简单的统计和可视化。5.1 读取 JSON 并生成 DataFrame# 文件路径analyze.py import json import pandas as pd import matplotlib.pyplot as plt def load_records(json_path: str) - pd.DataFrame: with open(json_path, r, encodingutf-8) as f: data json.load(f) return pd.DataFrame(data) def main(): df load_records(output/records.json) print(df.head()) # 按车型统计 type_count df[train_type].value_counts() print(type_count) # 按站台统计 platform_count df[platform].value_counts() print(platform_count) # 绘制车型分布柱状图 type_count.plot(kindbar, titleTrain Type Distribution) plt.savefig(output/train_type_bar.png) plt.close() # 导出 CSV df.to_csv(output/records.csv, indexFalse, encodingutf-8-sig) if __name__ __main__: main()保存 CSV 时使用utf-8-sig编码可以避免 Excel 打开中文乱码。matplotlib 绘图时如果出现中文字体问题需要额外设置字体Windows 下可以用plt.rcParams[font.sans-serif] [Microsoft YaHei] plt.rcParams[axes.unicode_minus] False这样进站记录就从照片变成了一份可以继续分析的数据集。6. 常见问题与排查思路在实际运行中最常见的几个问题如下问题现象常见原因解决思路OCR 识别出的车号总是带字母 O 或 I车号字体中的数字 0 和 1 与字母 O、I 相似增加白名单或人工确认对比车头实物涂装照片读取不到 GPS 信息手机相册关闭了定位权限或图片被压缩上传后 EXIF 丢失恢复原图在记录中手工填写车站和站台12306 接口请求失败车站代码错误、请求头缺失、风控拦截降低请求频率用浏览器 F12 抓包确认最新接口与参数站台记录与实际不符仅凭列车运行方向推测站台以车站信息屏、广播、12306 车站大屏为准控制台打印中文乱码Windows 默认编码不是 UTF-8在 Python 中执行sys.stdout.reconfigure(encodingutf-8)Tesseract 报错找不到引擎未安装 Tesseract 或未配置 PATH安装后重启终端或代码中指定pytesseract.pytesseract.tesseract_cmd如果 OCR 效果不理想建议不要过度调参而是先从源头解决拍摄时尽量站在光线充足但不逆光的位置使用长焦镜头稳定拍摄拍完马上查看车号区域是否清晰。车号识别这件事一张清晰的裁切图胜过十次图像预处理。7. 最佳实践与工程建议7.1 数据一定要多源核对铁路信息具有很强的时效性。某车型担当某车次、某车次停靠某站台都可能因为图定调整、临时加开、车底替换而变化。最稳妥的记录流程是现场记录 官方渠道核实 事后归档。不要因为看到一次担当就认定“永久如此”更不要在没有把握的情况下把推断写成结论。7.2 文件命名与目录规范照片和记录文件建议统一命名例如20250601_143000_G1802_CR400BF-A-5058_常州北_2台.jpg这种命名包含了时间、车次、车型、车号、车站、站台即使以后脱离数据库单独看文件名也能还原大部分信息。脚本生成的record_id也尽量采用这一逻辑保证唯一性。7.3 脚本健壮性调用外部接口时一定要加超时和异常处理。网络请求不可控日志里要能看出哪一步失败。建议把请求函数封装成带重试的版本但重试次数不要太多避免对服务造成压力。import time import requests from requests.adapters import HTTPAdapter from urllib3.util.retry import Retry def make_session_with_retry(): session requests.Session() retry Retry( total2, backoff_factor0.5, status_forcelist[500, 502, 503, 504], ) adapter HTTPAdapter(max_retriesretry) session.mount(https://, adapter) return session7.4 安全与合规拍摄火车站时必须严格遵守车站安全要求站在站台安全白线以内不要为拍摄而越过安全线。不要使用自拍杆、三脚架等物品侵入线路限界。不要进入轨道、股道、检修库等禁止区域。不要干扰车站工作人员和乘降秩序。不要拍摄执勤人员、旅客面部等与记录无关的信息避免隐私问题。如果要把照片和记录发布到网络建议对车牌、人脸等无关信息做模糊处理。本文涉及的接口查询仅用于个人学习不要在短时间内大量请求。7.5 生产环境与长期维护如果这个工具只是个人使用JSON 文件就足够了。如果以后想把记录做成一个线上相册或数据平台建议把记录存到 SQLite 或 MySQL并增加用户鉴权、操作日志、数据备份。字段变动时考虑兼容旧数据例如在 JSON 中增加schema_version字段方便后续迁移。8. 总结与下一步从一次“CR400BF-A-5058 担当 G1802 次列车进常州北站 2 台”的现场记录出发本文完成了一套完整的进站记录卡生成方案。你可以先手工记录车次、车型、车号、站台、时间再逐步用脚本读取 EXIF、OCR 识别车号、查询公开车次信息、生成 Markdown 和 CSV最后用 pandas 做基础统计与可视化。如果只想快速使用不需要把每个模块都跑通只用record.py和main.py也能满足基本归档需求。下一步可以从三个方向继续深入一是优化车号自动识别通过裁切和多帧投票提高准确率二是把车次时刻表数据接入工具自动生成“预计进站时间”提醒三是把记录结果接入地图或 GIS 系统展示列车运行轨迹。铁路记录看似只是“拍一张照片”但背后涉及图像处理、数据建模、接口请求、数据可视化等多个技术点非常适合作为 Python 综合练习。如果你对哪一部分感兴趣可以直接把代码跑起来结合自己的照片做一次完整记录。如果本文对你有帮助可以收藏备用。也欢迎在实际使用中根据自己的记录习惯调整字段和输出格式做出一套真正属于自己的“进站记录数据库”。
返回列表