1. AUV任务日志格式识别与字段映射

先判断“它是什么”,再决定“怎么读”

面对一份来源不明的 AUV 任务日志,第一步不是急着写解析器,而是做格式取证。取证的核心是三件事:看文件头(是否有魔数、版本号、长度字段)、看分隔与行结构(逗号、制表符、换行、定长记录还是长度前缀)、看字段命名规律(t/timestamp/ts、depth/dep、hdg/heading 等)。三者交叉验证后,才能确定是 CSV、JSON Lines 还是二进制自定义协议。

下面用一份教学示例日志 auv_mission_0713.log 演示完整流程。该文件为假设数据,用于说明方法,不代表真实设备输出。

第一步:文件头与行结构取证

用十六进制查看前 64 字节,并统计行首特征:

# 查看前 64 字节(十六进制 + ASCII)
xxd -l 64 auv_mission_0713.log

# 统计前 5 行的分隔符出现次数
head -n 5 auv_mission_0713.log | awk -F',' '{print NF}'

示例输出显示:文件以 ASCII 文本 AUVLOG,v2 开头,随后每行以逗号分隔且字段数恒为 9,行尾为 \n。这排除了二进制自定义协议(无魔数、无长度前缀、无不可打印字节),也排除了 JSON Lines(行首不是 {)。结论:带自定义文件头的 CSV 变体。

判据速记:出现大量 0x00 或不可打印字节 → 优先怀疑二进制;每行以 { 开头且以 } 结尾 → JSON Lines;字段数恒定且分隔符单一 → 分隔符文本格式。

第二步:字段命名规律与语义映射

读取表头行,得到 9 个字段名,再结合取值域推断语义。示例表头为:

t_ms,dep_m,hdg_deg,spd_kn,thr_pct,rpm,volt_v,mode,chk

映射时不能只看名字,要看取值域与物理合理性:hdg_deg 取值 0–359 且循环回绕,符合航向;dep_m 单调增大后减小,符合下潜—上浮剖面;thr_pct 在 0–100 之间,符合推进器推力百分比;mode 为离散枚举(如 DIVE/HOLD/ASC),符合任务模式。字段 chk 为行尾校验值,不参与语义映射但需在解析时保留。

原始日志(示例) auv_mission_0713.log AUVLOG,v2 t_ms,dep_m,hdg_deg,spd_kn, thr_pct,rpm,volt_v,mode,chk 0,0.0,182,0.0,0,0,25.1,DIVE,A3 500,1.2,181,1.1,35,820,24.9,DIVE,7F 1000,3.5,180,1.4,42,910,24.8,DIVE,1C 1500,5.0,179,1.4,40,905,24.8,HOLD,55 2000,5.0,178,1.3,38,880,24.7,HOLD,9E 2500,4.1,177,1.2,30,760,24.7,ASC,2B 3000,2.0,176,1.0,22,640,24.6,ASC,C4 3500,0.3,175,0.6,10,410,24.6,ASC,6D 4000,0.0,175,0.0,0,0,24.5,SURF,10 取证结论 • 文件头:AUVLOG,v2(文本) • 分隔符:逗号,字段数恒为 9 • 行尾:\n,无长度前缀 • 判定:带自定义头的 CSV • 排除:二进制 / JSON Lines 字段映射 字段 → 任务语义映射表 原始字段 语义 单位/取值域 类型 t_ms 任务时间 ms,单调递增 int64 dep_m 深度 m,0–300 float hdg_deg 航向 °,0–359 回绕 float spd_kn 对地速度 kn,0–5 float thr_pct 推进器推力 %,0–100 int rpm 推进器转速 rpm,0–1500 int volt_v 电池电压 V,22–26 float mode 任务模式 DIVE/HOLD/ASC/SURF enum chk 行校验值 2 位十六进制 hex 映射校验要点 取值域合理 + 物理趋势一致(深度先增后减、航向缓慢变化)→ 映射可信

第三步:用最小解析器验证映射

映射表不能只停留在纸面,要用一段可运行的解析代码验证字段数、类型转换与取值域。以下 Python 脚本读取示例日志,跳过文件头,输出每条记录的语义化字典,并对越界值报警:

import csv

FIELD_MAP = {
    "t_ms":    ("time_ms",   int,   (0, 10**9)),
    "dep_m":   ("depth_m",   float, (0.0, 300.0)),
    "hdg_deg": ("heading_deg", float, (0.0, 360.0)),
    "spd_kn":  ("speed_kn",  float, (0.0, 5.0)),
    "thr_pct": ("thrust_pct", int,  (0, 100)),
    "rpm":     ("rpm",       int,   (0, 1500)),
    "volt_v":  ("voltage_v", float, (20.0, 28.0)),
    "mode":    ("mode",      str,   None),
    "chk":     ("checksum",  str,   None),
}

def parse(path):
    records, warnings = [], []
    with open(path, newline="") as f:
        header = f.readline().strip()          # AUVLOG,v2
        assert header.startswith("AUVLOG"), "非预期文件头"
        reader = csv.reader(f)
        cols = next(reader)                    # 字段名行
        assert len(cols) == len(FIELD_MAP), "字段数不匹配"
        for lineno, row in enumerate(reader, start=3):
            if len(row) != len(cols):
                warnings.append(f"第{lineno}行字段数={len(row)}")
                continue
            rec = {}
            for raw, name in zip(row, cols):
                key, caster, rng = FIELD_MAP[name]
                val = caster(raw)
                if rng and not (rng[0] <= val <= rng[1]):
                    warnings.append(f"第{lineno}行 {name}={val} 越界")
                rec[key] = val
            records.append(rec)
    return records, warnings

recs, warns = parse("auv_mission_0713.log")
print(len(recs), "条记录")
print(recs[3])          # 期望 depth_m=5.0, mode='HOLD'
print(warns[:3])

预期现象(基于示例数据可直接推出):输出 9 条记录,第 4 条记录为 {'time_ms': 1500, 'depth_m': 5.0, 'heading_deg': 179.0, 'speed_kn': 1.4, 'thrust_pct': 40, 'rpm': 905, 'voltage_v': 24.8, 'mode': 'HOLD', 'checksum': '55'},且无越界告警。若出现字段数不匹配或越界告警,说明映射或格式判定有误,应回到取证步骤复核。

不要把 chk 当作可丢弃字段。后续若需校验行完整性或定位传输损坏,它是唯一依据;解析阶段应原样保留,而不是在类型转换时丢弃。

可复用的字段映射表模板

把上面的结论固化为一张可直接填写的映射表,供后续解析器共用。字段名、语义、单位、取值域、类型五列缺一不可,取值域是发现“名字对但含义错”的关键防线。

原始字段任务语义单位取值域类型备注
t_ms任务时间ms单调递增int64基准时钟待确认
dep_m深度m0–300float正向下
hdg_deg航向°0–359 回绕float正北为 0
spd_kn对地速度kn0–5float—
thr_pct推进器推力%0–100int—
rpm推进器转速rpm0–1500int与 thr_pct 正相关
volt_v电池电压V20–28float随任务缓慢下降
mode任务模式—DIVE/HOLD/ASC/SURFenum离散枚举
chk行校验值—2 位十六进制hex保留不丢弃

完成以上三步后,格式类型与字段语义均已确定,后续解析、时间对齐与阶段划分都可以直接基于这张映射表展开,而不必反复回看原始日志。