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 为行尾校验值,不参与语义映射但需在解析时保留。
第三步:用最小解析器验证映射
映射表不能只停留在纸面,要用一段可运行的解析代码验证字段数、类型转换与取值域。以下 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 | 深度 | m | 0–300 | float | 正向下 |
| hdg_deg | 航向 | ° | 0–359 回绕 | float | 正北为 0 |
| spd_kn | 对地速度 | kn | 0–5 | float | — |
| thr_pct | 推进器推力 | % | 0–100 | int | — |
| rpm | 推进器转速 | rpm | 0–1500 | int | 与 thr_pct 正相关 |
| volt_v | 电池电压 | V | 20–28 | float | 随任务缓慢下降 |
| mode | 任务模式 | — | DIVE/HOLD/ASC/SURF | enum | 离散枚举 |
| chk | 行校验值 | — | 2 位十六进制 | hex | 保留不丢弃 |
完成以上三步后,格式类型与字段语义均已确定,后续解析、时间对齐与阶段划分都可以直接基于这张映射表展开,而不必反复回看原始日志。