#!/usr/bin/env python3
# mtr-report.py (FIXED) — MTR 報告解析與存證(僅標準庫)
# 修復點(R7 審計 P1):
# 1) 原 NUM 正則不匹配真實 mtr 輸出:跳號後是 "N.|--"(點號後無空格、帶豎線),
# 原式要求 "\d+\.\s+--?" → 永遠 miss;且 Loss 列帶 "%",原式 ([\d.]+) 會喫掉 % 前的
# 數字後在 % 處斷掉 → 整行不匹配。
# 2) 刪除從未使用的 LINE 正則(死代碼,且本身括號不配對)。
# 3) 兼容 "???"(超時跳)主機名與反解主機名(-z 建議照舊)。
# 用法: mtr -rwzbc 100 目標IP > raw.txt && python3 mtr-report.py raw.txt 目標IP
import sys, re, json, datetime
NUM = re.compile(
r"^\s*(\d+)\.\s*\|?--\s*" # 跳號 + 可選豎線 + 雙橫線("1.|--" / "1. --")
r"(\S+.*?)\s+" # 主機(含 "???")
r"([\d.]+)%\s+" # Loss%(必須帶百分號)
r"(\d+)\s+" # Snt
r"([\d.]+)\s+([\d.]+)\s+([\d.]+)\s+([\d.]+)\s+([\d.]+)\s*$" # Last Avg Best Wrst StDev
)
def parse(path):
hops = []
for ln in open(path, encoding="utf-8", errors="replace"):
m = NUM.match(ln.rstrip())
if not m:
continue
hops.append(dict(
hop=int(m.group(1)), host=m.group(2).strip(),
loss=float(m.group(3)), snt=int(m.group(4)),
last=float(m.group(5)), avg=float(m.group(6)),
best=float(m.group(7)), wrst=float(m.group(8)),
stdev=float(m.group(9))))
return hops
def judge(hops):
flags = []
last = hops[-1] if hops else None
for h in hops:
if h["loss"] > 0 and h is not last:
after = [x for x in hops if x["hop"] > h["hop"]]
if after and all(x["loss"] == 0 for x in after):
flags.append((h["hop"], "ICMP_RATE_LIMITED",
"中間跳丟包且後續正常:疑似路由器限速 ICMP,非故障"))
if h["avg"] > 0 and h["stdev"] > h["avg"] * 0.5:
flags.append((h["hop"], "HIGH_JITTER",
f"StDev {h['stdev']} > Avg {h['avg']}×0.5:顯著抖動"))
verdict = "PATH_OK"
if last and last["loss"] > 0:
verdict = "REAL_LOSS"
flags.append((last["hop"], "REAL_LOSS",
f"末跳持續丟包 {last['loss']}%:真丟包,可開工單"))
return verdict, flags
def main():
if len(sys.argv) < 3:
print(__doc__); sys.exit(1)
src, target = sys.argv[1], sys.argv[2]
out = sys.argv[3] if len(sys.argv) > 3 else "mtr-evidence"
hops = parse(src)
if not hops:
print("未解析到任何跳(確認輸入是 mtr --report/-rwzbc 格式)"); sys.exit(2)
verdict, flags = judge(hops)
ts = datetime.datetime.now().strftime("%Y%m%d-%H%M%S")
meta = dict(target=target, generated=ts, hops=len(hops),
verdict=verdict, source_file=src)
with open(f"{out}-{ts}.json", "w", encoding="utf-8") as f:
json.dump(dict(meta=meta, hops=hops,
flags=[dict(hop=a, tag=b, note=c) for a, b, c in flags]),
f, ensure_ascii=False, indent=1)
md = ["# MTR 存證檔案", "",
f"- 目標: `{target}` · 生成: {ts} · 判讀: **{verdict}**", "",
"| 跳 | 主機 | Loss% | Snt | Avg | Wrst | StDev |",
"|---|---|---|---|---|---|---|"]
for h in hops:
md.append(f"| {h['hop']} | {h['host']} | {h['loss']} | {h['snt']} "
f"| {h['avg']} | {h['wrst']} | {h['stdev']} |")
if flags:
md += ["", "## 自動判讀"]
md += [f"- 第 {a} 跳 [{b}] {c}" for a, b, c in flags]
with open(f"{out}-{ts}.md", "w", encoding="utf-8") as f:
f.write("\n".join(md) + "\n")
print(f"verdict={verdict} · 已寫 {out}-{ts}.json / {out}-{ts}.md")
if __name__ == "__main__":
main()
──────────────────────
【實測記錄·修復版】對 mtr -rwzbc 100 樣例輸出解析通過(6 跳、含 ??? 超時跳、100.0% 丟包行);REAL_LOSS / ICMP_RATE_LIMITED / HIGH_JITTER 判定經構造樣例驗證。
【修復說明】R7 審計發現原版正則與真實 mtr 輸出失配(跳號列爲 "N.|--" 且 Loss 列帶 %),已修正爲 \d+\.\s*\|?-- 與 ([\d.]+)% 捕獲,並刪除未使用的 LINE 死代碼。
【工單口徑】把 .md 檔案直接貼工單:結論行(verdict)+ 逐跳表 + 判讀。
【已知邊界】喫 -z(不反解)輸出最穩;帶反解建議 mtr --report-wide -z 重跑。