1. 轨道几何不平顺数据文件结构解析与字段映射

原始文件长什么样:先看清“行结构”再谈字段

轨道几何检测原始数据通常以纯文本逐行记录的形式落盘,一行对应一个采样点。不同检测系统(综合检测车、轨道检查仪、便携式添乘仪)导出的文件在分隔符、表头、单位上差异很大,但核心信息是一致的:里程 + 各项几何参数。解析的第一步不是写映射代码,而是确认三件事:

  • 分隔符:逗号、制表符、空格对齐或定宽列;
  • 是否有表头:有表头可直接按名取列,无表头必须按列序约定;
  • 单位与量纲:轨距常为 mm,水平/高低/轨向常为 mm,里程常为 m 或 km,部分系统用 0.1 mm 或 0.01 mm 计数。

下面是一份典型导出片段(示例数据,制表符分隔,含表头,里程单位 m,几何量单位 mm):

MILEAGE	GAUGE	LEVEL	ALIGN_L	ALIGN_R	PROFILE_L	PROFILE_R
K120+000.0	1435.2	1.8	-0.6	0.4	2.1	-1.3
K120+000.5	1435.5	2.1	-0.5	0.5	2.4	-1.1
K120+001.0	1436.1	2.6	-0.3	0.7	2.9	-0.8
K120+001.5	1434.8	1.5	-0.9	0.2	1.7	-1.6

注意两个“坑”:里程写成 K120+000.0 这种公里标+米的复合格式,不能直接当浮点数用;轨向、高低按左右股分列存储,而标准模型通常要求“单值 + 股别”或“左右分别建模”。字段映射的本质,就是把这些异构的存储形态统一到一套标准数据模型上。

字段映射关系:从原始列到标准模型

本例采用的标准数据模型字段为:mileage_m(里程,m,浮点)、gauge_mm(轨距,mm)、level_mm(水平,mm,带符号)、alignment_mm(轨向,mm,带符号)、profile_mm(高低,mm,带符号)、side(股别:L/R)。映射规则如下:

原始列标准字段转换规则单位处理
MILEAGEmileage_m解析 K{km}+{m} → km×1000 + mm
GAUGEgauge_mm直接取值mm
LEVELlevel_mm保留符号mm
ALIGN_L / ALIGN_Ralignment_mm + side拆为两行,side=L/Rmm
PROFILE_L / PROFILE_Rprofile_mm + side拆为两行,side=L/Rmm
原始检测文件(制表符分隔) 一行 = 一个采样点 MILEAGE K120+000.0 GAUGE 1435.2 LEVEL 1.8 ALIGN_L -0.6 ALIGN_R 0.4 PROFILE_L 2.1 PROFILE_R -1.3 里程为复合格式 · 左右股分列 单位需确认(mm / 0.1mm) 转换规则 里程解析 K{km}+{m} → km*1000+m 单位归一 ×系数 → 统一为 mm 左右股拆分 L/R 列 → 两行 + side 符号保留 水平/轨向/高低带正负 标准数据模型 结构化表(每行含股别) mileage_m (float, m) gauge_mm (float, mm) level_mm (float, mm) alignment_mm (float, mm) profile_mm (float, mm) side (L / R) 一行 = 一个采样点 × 一个股别 可直接进入超限判别

用 Python 完成解析与映射

下面代码自包含、可直接运行:内置示例数据写入临时文件,再按“读行 → 解析里程 → 单位归一 → 左右股拆分 → 输出结构化表”的顺序完成映射。里程解析用正则提取公里与米,单位系数集中在一个字典里,便于不同检测系统复用。

import re
import csv
import io

RAW = """MILEAGE\tGAUGE\tLEVEL\tALIGN_L\tALIGN_R\tPROFILE_L\tPROFILE_R
K120+000.0\t1435.2\t1.8\t-0.6\t0.4\t2.1\t-1.3
K120+000.5\t1435.5\t2.1\t-0.5\t0.5\t2.4\t-1.1
K120+001.0\t1436.1\t2.6\t-0.3\t0.7\t2.9\t-0.8
K120+001.5\t1434.8\t1.5\t-0.9\t0.2\t1.7\t-1.6"""

# 单位系数:原始值 × 系数 = mm(示例文件已是 mm,系数为 1.0)
UNIT_SCALE = {"GAUGE": 1.0, "LEVEL": 1.0, "ALIGN": 1.0, "PROFILE": 1.0}

MILEAGE_RE = re.compile(r"^K(\d+)\+(\d+(?:\.\d+)?)$")

def parse_mileage(text):
    """K120+000.5 -> 120000.5 (m)"""
    m = MILEAGE_RE.match(text.strip())
    if not m:
        raise ValueError(f"无法解析里程: {text!r}")
    km, meters = m.group(1), m.group(2)
    return int(km) * 1000.0 + float(meters)

def parse_file(text):
    reader = csv.DictReader(io.StringIO(text), delimiter="\t")
    rows = []
    for raw in reader:
        mileage = parse_mileage(raw["MILEAGE"])
        gauge = float(raw["GAUGE"]) * UNIT_SCALE["GAUGE"]
        level = float(raw["LEVEL"]) * UNIT_SCALE["LEVEL"]
        for side in ("L", "R"):
            rows.append({
                "mileage_m": mileage,
                "gauge_mm": gauge,
                "level_mm": level,
                "alignment_mm": float(raw[f"ALIGN_{side}"]) * UNIT_SCALE["ALIGN"],
                "profile_mm": float(raw[f"PROFILE_{side}"]) * UNIT_SCALE["PROFILE"],
                "side": side,
            })
    return rows

rows = parse_file(RAW)
print(f"原始数据行数: 4, 映射后行数: {len(rows)}")
for r in rows[:4]:
    print(r)

运行后应看到:原始 4 行数据被展开为 8 行(每点左右股各一行),里程由 K120+000.0 变为 120000.0,字段名与单位全部对齐标准模型。示例输出(由给定输入直接推出):

原始数据行数: 4, 映射后行数: 8
{'mileage_m': 120000.0, 'gauge_mm': 1435.2, 'level_mm': 1.8, 'alignment_mm': -0.6, 'profile_mm': 2.1, 'side': 'L'}
{'mileage_m': 120000.0, 'gauge_mm': 1435.2, 'level_mm': 1.8, 'alignment_mm': 0.4, 'profile_mm': -1.3, 'side': 'R'}
{'mileage_m': 120000.5, 'gauge_mm': 1435.5, 'level_mm': 2.1, 'alignment_mm': -0.5, 'profile_mm': 2.4, 'side': 'L'}
{'mileage_m': 120000.5, 'gauge_mm': 1435.5, 'level_mm': 2.1, 'alignment_mm': 0.5, 'profile_mm': -1.1, 'side': 'R'}

映射正确性的三项校验

字段映射最容易出错的地方不是代码,而是“看起来对、其实错”。建议在解析后立即做三项检查:

  1. 行数守恒:映射后行数 = 原始行数 × 2(左右股拆分时),若不等说明有列缺失或表头不匹配。
  2. 里程单调递增:同一股别内 mileage_m 应严格递增,出现回退说明里程解析或排序有问题。
  3. 量纲合理性:轨距应在 1400–1470 mm 附近,水平/高低/轨向绝对值一般不超过 30 mm;若出现 14352 这类值,多半是原始单位为 0.1 mm 而系数未设。
若原始文件无表头,用 csv.reader 按列序读取,并把列序约定写成常量(如 COL_MILEAGE=0, COL_GAUGE=1 ...),避免魔法数字散落在代码里。
不要假设所有检测系统的里程都是“公里+米”格式。有的系统直接给累计米数(如 120000.5),有的给“120+000.5”。解析前先抽样 3–5 行确认格式,再决定正则或分支逻辑,否则会在整条线路数据上静默出错。