4. 关键事件提取与异常标记
从原始日志到事件时间线
本案例的输入是一份已对齐时间基准的AUV任务日志(例2、例3的产物),每行包含t(秒,统一时钟)、src(来源子系统)、msg(原始消息)。任务是从中提取五类关键事件,并按规则标记异常,最终输出按时间排序的事件时间线。
先明确事件识别规则。日志消息采用固定前缀编码,提取时用前缀匹配而非全文搜索,避免把描述性文本误判为事件:
| 事件类型 | 匹配模式 | 关键字段 | 异常判定规则 |
|---|---|---|---|
| 任务开始 | MISSION_START | 任务号 | 重复出现 >1 次 → 异常 |
| 到达目标点 | WAYPOINT_REACHED | 目标点编号、偏差 | 偏差 > 5 m → 异常 |
| 避障触发 | OBSTACLE_AVOID | 最近距离 | 最近距离 < 2 m → 异常 |
| 通信中断 | COMM_LOST / COMM_RESTORED | 中断时长 | 中断时长 > 120 s → 异常 |
| 任务结束 | MISSION_END | 结束状态 | 状态非 NORMAL → 异常 |
其中“通信中断”是成对事件:COMM_LOST 开启一个中断区间,COMM_RESTORED 关闭它,时长由两者时间差计算。若日志结束时仍有未闭合的 COMM_LOST,按“持续至日志末尾”处理并标记为异常。
提取与标记的实现
下面是一段可直接运行的 Python 实现。它读取日志行列表,输出事件字典列表,每条事件含类型、时间、来源、是否异常及异常原因。示例日志为教学构造数据,用于演示规则如何生效。
import re
# 教学示例日志(已对齐时间基准)
raw_log = [
(10, "MISSION", "MISSION_START id=M07"),
(150, "NAV", "WAYPOINT_REACHED wp=1 dev=3.2"),
(330, "SONAR", "OBSTACLE_AVOID dist=1.4"),
(460, "COMM", "COMM_LOST"),
(640, "COMM", "COMM_RESTORED"),
(980, "MISSION", "MISSION_END status=NORMAL"),
]
PATTERNS = {
"MISSION_START": re.compile(r"^MISSION_START\b"),
"WAYPOINT_REACHED": re.compile(r"^WAYPOINT_REACHED\b"),
"OBSTACLE_AVOID": re.compile(r"^OBSTACLE_AVOID\b"),
"COMM_LOST": re.compile(r"^COMM_LOST\b"),
"COMM_RESTORED": re.compile(r"^COMM_RESTORED\b"),
"MISSION_END": re.compile(r"^MISSION_END\b"),
}
def parse_kv(msg):
"""提取 key=value 字段,如 dev=3.2 -> {'dev': '3.2'}"""
return dict(re.findall(r"(\w+)=([\w.]+)", msg))
def extract_events(raw_log):
events = []
for t, src, msg in raw_log:
for etype, pat in PATTERNS.items():
if pat.match(msg):
events.append({
"t": t, "type": etype, "src": src,
"fields": parse_kv(msg),
"abnormal": False, "reason": ""
})
break
return events
def mark_abnormal(events, log_end_t):
# 1) 任务开始重复
starts = [e for e in events if e["type"] == "MISSION_START"]
if len(starts) > 1:
for e in starts[1:]:
e["abnormal"], e["reason"] = True, "MISSION_START 重复"
# 2) 目标点偏差 > 5 m
for e in events:
if e["type"] == "WAYPOINT_REACHED":
dev = float(e["fields"].get("dev", 0))
if dev > 5.0:
e["abnormal"], e["reason"] = True, f"偏差 {dev} m > 5 m"
# 3) 避障最近距离 < 2 m
for e in events:
if e["type"] == "OBSTACLE_AVOID":
d = float(e["fields"].get("dist", 99))
if d < 2.0:
e["abnormal"], e["reason"] = True, f"最近距离 {d} m < 2 m"
# 4) 通信中断区间配对与时长判定
lost = None
for e in events:
if e["type"] == "COMM_LOST":
lost = e
elif e["type"] == "COMM_RESTORED" and lost is not None:
dur = e["t"] - lost["t"]
lost["fields"]["duration"] = dur
if dur > 120:
lost["abnormal"], lost["reason"] = True, f"中断 {dur} s > 120 s"
lost = None
if lost is not None: # 未闭合
dur = log_end_t - lost["t"]
lost["fields"]["duration"] = dur
lost["abnormal"], lost["reason"] = True, f"中断未恢复,持续 {dur} s"
# 5) 任务结束状态非 NORMAL
for e in events:
if e["type"] == "MISSION_END":
st = e["fields"].get("status", "")
if st != "NORMAL":
e["abnormal"], e["reason"] = True, f"结束状态 {st}"
return events
events = extract_events(raw_log)
events = mark_abnormal(events, log_end_t=1000)
events.sort(key=lambda e: e["t"])
for e in events:
flag = "⚠ " + e["reason"] if e["abnormal"] else "OK"
print(f"t={e['t']:>4} {e['type']:<16} {flag}")
按上述示例数据运行,预期输出为:MISSION_START 与 MISSION_END 为 OK;WAYPOINT_REACHED 偏差 3.2 m 为 OK;OBSTACLE_AVOID 因 1.4 m < 2 m 标记异常;COMM_LOST 因 640−460=180 s > 120 s 标记异常;COMM_RESTORED 本身不判异常,仅作为区间闭合点。这正是 SVG 时间线上两个 ⚠ 标记的来源。
前缀匹配使用
^ 锚定行首,可避免 COMM_LOST 被 COMM_LOST_RETRY 之类的衍生消息误触发;若日志存在多种变体,应显式列出模式而非放宽为子串匹配。结果校验与常见偏差
- 事件数量核对:五类事件中,
COMM_LOST与COMM_RESTORED应成对出现;若配对数为奇数,说明存在未闭合中断,需检查日志是否被截断。 - 时间单调性:排序后相邻事件时间差不应为负;若出现负值,说明时间对齐环节仍有残留问题,应回到例2的校正结果复查。
- 阈值边界:规则使用严格不等号(>5、<2、>120),恰好等于阈值时不标记异常。若任务规范要求“达到即异常”,需改为
>=/<=,并在文档中写明。 - 缺失关键事件:若时间线中没有
MISSION_START或MISSION_END,应作为日志完整性问题单独报告,而不是静默跳过。
完成上述步骤后,得到的事件时间线即为后续阶段分析、深度剖面清洗与轨迹重构的输入基础。