3. 车载日志中的关键事件字段提取与结构化

原始日志行的字段特征与提取策略

CTCS车载日志中,制动施加、模式转换、等级切换等关键事件通常以半结构化文本行出现,字段顺序和分隔符并不统一。要解决“非结构化日志难以直接查询”的问题,核心是先把每类事件行拆成固定字段集合,再统一写入结构化记录。以典型车载事件日志行为例,常见形态如下(示例数据,仅用于说明格式):

2024-05-12 08:31:07.245 [ATP] MODE_CHG from=FS to=OS speed=182.4 pos=K125+340 balise=B-0142
2024-05-12 08:31:09.870 [ATP] BRAKE_APPLY level=SB rate=0.6 speed=176.1 pos=K125+512 cause=overspeed
2024-05-12 08:31:12.003 [ATP] LEVEL_CHG from=CTCS-3 to=CTCS-2 speed=168.9 pos=K125+780 rbc=RBC-03

这三类行共享“时间戳 + 设备标签 + 事件类型 + 键值对”的骨架,差异只在事件类型和键值集合。因此提取策略应分两层:行级切分负责把时间戳、设备、事件类型、键值串分离;事件级映射负责按事件类型把键值对映射到统一字段(如 from_mode、to_mode、speed、position、cause),并补齐该事件不涉及的字段为空值。这样不同事件写入同一张宽表,后续查询才可直接按字段过滤。

原始日志行(非结构化) 08:31:07 MODE_CHG from=FS to=OS speed=182.4 pos=K125+340 08:31:09 BRAKE_APPLY level=SB cause=overspeed 行级切分 ① 正则提取时间戳 ② 提取 [设备] 标签 ③ 识别事件类型码 ④ 拆分 key=value 串 输出:事件类型 + 键值字典 事件级字段映射 MODE_CHG → from_mode/to_mode BRAKE_APPLY → brake_level LEVEL_CHG → from_level/to_level 公共字段:speed/pos/cause 缺失字段补空值 结构化 记录 统一宽表 可查询 可过滤 统一结构化字段(宽表,每行一条事件) timestamp device event_type from_mode to_mode brake_level speed position cause 08:31:07.245 ATP MODE_CHG FS OS — 182.4 K125+340 — 08:31:09.870 ATP BRAKE_APPLY — — SB 176.1 K125+512 overspeed 08:31:12.003 ATP LEVEL_CHG CTCS-3 CTCS-2 — 168.9 K125+780 — 说明:不同事件类型共用同一宽表,未涉及字段以空值占位,保证后续按 event_type / speed / position 等字段直接过滤查询。

提取实现:正则切分与事件映射

下面给出一个可直接运行的 Python 实现。它用一条正则完成行级切分,再用事件类型到字段名的映射表完成结构化,最后输出为字典列表(可直接写入 CSV 或 DataFrame)。示例日志内容内嵌在代码中,不依赖外部文件。

import re
import csv

# 示例原始日志(教学示例数据)
RAW_LINES = [
    "2024-05-12 08:31:07.245 [ATP] MODE_CHG from=FS to=OS speed=182.4 pos=K125+340 balise=B-0142",
    "2024-05-12 08:31:09.870 [ATP] BRAKE_APPLY level=SB rate=0.6 speed=176.1 pos=K125+512 cause=overspeed",
    "2024-05-12 08:31:12.003 [ATP] LEVEL_CHG from=CTCS-3 to=CTCS-2 speed=168.9 pos=K125+780 rbc=RBC-03",
]

# 行级切分:时间戳、设备、事件类型、键值串
LINE_RE = re.compile(
    r"^(?P<ts>\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3})\s+"
    r"\[(?P<dev>[^\]]+)\]\s+"
    r"(?P<evt>[A-Z_]+)\s+"
    r"(?P<kv>.*)$"
)
KV_RE = re.compile(r"(\w+)=([^\s]+)")

# 事件类型 → 统一字段映射
EVENT_MAP = {
    "MODE_CHG":    {"from": "from_mode",  "to": "to_mode"},
    "LEVEL_CHG":   {"from": "from_level", "to": "to_level"},
    "BRAKE_APPLY": {"level": "brake_level"},
}

# 统一宽表字段
FIELDS = ["timestamp", "device", "event_type",
          "from_mode", "to_mode", "from_level", "to_level",
          "brake_level", "speed", "position", "cause"]

def parse_line(line):
    m = LINE_RE.match(line.strip())
    if not m:
        return None
    rec = {f: "" for f in FIELDS}
    rec["timestamp"] = m.group("ts")
    rec["device"] = m.group("dev")
    rec["event_type"] = m.group("evt")

    kv = dict(KV_RE.findall(m.group("kv")))
    # 公共字段
    rec["speed"] = kv.get("speed", "")
    rec["position"] = kv.get("pos", "")
    rec["cause"] = kv.get("cause", "")

    # 事件专属字段映射
    for src, dst in EVENT_MAP.get(rec["event_type"], {}).items():
        rec[dst] = kv.get(src, "")
    return rec

records = [r for r in (parse_line(l) for l in RAW_LINES) if r]

with open("events_structured.csv", "w", newline="", encoding="utf-8") as f:
    writer = csv.DictWriter(f, fieldnames=FIELDS)
    writer.writeheader()
    writer.writerows(records)

for r in records:
    print(r)

运行后每条日志行被转换为一条字段齐全的记录,例如模式转换行会得到 from_mode=FS、to_mode=OS,而 brake_level 为空;制动行则相反。这样“查询所有速度低于 170 km/h 的制动施加事件”只需按 event_type == "BRAKE_APPLY" 且 speed < 170 过滤,不再需要逐行文本匹配。

结果校验与常见提取陷阱

结构化是否可靠,取决于三点校验:行覆盖率(能被正则匹配的行占比)、字段完整率(关键事件必填字段是否非空)、类型正确性(速度、位置是否可转数值或规范格式)。建议在提取后立即统计:

  • 未匹配行数:若比例偏高,说明日志存在其他分隔符或前缀,需要扩展正则而非丢弃。
  • 必填字段缺失:如 MODE_CHG 行缺少 to,应标记为异常记录而非静默补空。
  • 位置格式:K125+340 需统一为“公里标+米”结构,避免后续按位置排序时字符串比较出错。
注意:键值对中若出现空格或等号(如自由文本 cause=driver brake),简单按空格切分会截断值。此时应改用“键名=值直到下一个键名”的惰性匹配,或先按已知键名列表定位边界。
工程建议:把 EVENT_MAP 与 FIELDS 外置为配置,新增事件类型时只改配置不改解析逻辑,便于日志格式随软件版本演进时维护。