113 lines
4.2 KiB
Python
113 lines
4.2 KiB
Python
"""drawio_read.py — 把任意 drawio 文件规整成纯 mxGraphModel XML 打到 stdout。
|
||
|
||
为什么需要它:drawio MCP 的 new_diagram 写出来的**不是**纯 mxfile XML,而是
|
||
SVG 外壳 + `content="<mxfile>..."`,里面的 diagram 段还是 base64+raw-deflate
|
||
压缩的。人手在 drawio 桌面版里存的则常常是纯 XML。两种都要能读,否则
|
||
n18(按原理图回写 drawio)和 d21(原理图与 drawio 一致性比对)根本没法比。
|
||
|
||
用法:
|
||
python drawio_read.py <drawio 文件> # 打印 mxGraphModel XML
|
||
python drawio_read.py <drawio 文件> --summary # 只打印模块/连线清单
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
import argparse
|
||
import base64
|
||
import html
|
||
import re
|
||
import sys
|
||
import urllib.parse
|
||
import zlib
|
||
from pathlib import Path
|
||
|
||
for _s in (sys.stdout, sys.stderr):
|
||
try:
|
||
_s.reconfigure(encoding="utf-8", errors="replace")
|
||
except (AttributeError, ValueError):
|
||
pass
|
||
|
||
|
||
def _inflate(payload: str) -> str:
|
||
"""base64 + raw-deflate + urlencode 三层解开(drawio 的压缩 diagram 段)。"""
|
||
blob = zlib.decompress(base64.b64decode(payload), -15)
|
||
return urllib.parse.unquote(blob.decode("utf-8"))
|
||
|
||
|
||
def to_xml(path: Path) -> str:
|
||
"""返回纯 mxGraphModel/mxfile XML,无论源文件是哪种封装。"""
|
||
raw = path.read_text(encoding="utf-8", errors="replace")
|
||
|
||
# 形态 1:SVG 外壳,真内容在 content 属性里
|
||
if not raw.lstrip().startswith(("<mxfile", "<mxGraphModel")):
|
||
m = re.search(r'content="([^"]*)"', raw)
|
||
if not m:
|
||
raise SystemExit(f"{path} 既不是 mxfile,也找不到 SVG 的 content 属性")
|
||
raw = html.unescape(m.group(1))
|
||
|
||
# 形态 2:diagram 段被压缩
|
||
d = re.search(r"<diagram[^>]*>\s*([A-Za-z0-9+/=]+)\s*</diagram>", raw)
|
||
if d:
|
||
try:
|
||
return _inflate(d.group(1))
|
||
except (zlib.error, ValueError) as exc:
|
||
raise SystemExit(f"{path} 的 diagram 段解压失败:{exc}")
|
||
|
||
# 形态 3:已经是明文 XML
|
||
return raw
|
||
|
||
|
||
_CELL = re.compile(r'<mxCell\b[^>]*?id="([^"]*)"[^>]*?>', re.S)
|
||
_VAL = re.compile(r'value="([^"]*)"')
|
||
_SRC = re.compile(r'source="([^"]*)"')
|
||
_TGT = re.compile(r'target="([^"]*)"')
|
||
|
||
|
||
def summarize(xml: str) -> str:
|
||
"""列出模块与连线,供 d21 做原理图/框图一致性比对时逐条核。"""
|
||
names: dict[str, str] = {}
|
||
edges: list[tuple[str, str, str]] = []
|
||
for m in re.finditer(r"<mxCell\b.*?(?:/>|</mxCell>)", xml, re.S):
|
||
tag = m.group(0)
|
||
cid = re.search(r'id="([^"]*)"', tag)
|
||
if not cid:
|
||
continue
|
||
val = _VAL.search(tag)
|
||
label = html.unescape(val.group(1)) if val else ""
|
||
label = re.sub(r"<[^>]+>", " ", label) # 去掉 value 里的 HTML 标签
|
||
label = re.sub(r"\s*[\r\n]+\s*", " / ", label) # 多行 value 压成一行
|
||
label = re.sub(r"\s{2,}", " ", label).strip()
|
||
if 'vertex="1"' in tag:
|
||
if "edgeLabel" in tag: # 边上的 Yes/No 标签不是模块
|
||
continue
|
||
names[cid.group(1)] = label
|
||
elif 'edge="1"' in tag:
|
||
s, t = _SRC.search(tag), _TGT.search(tag)
|
||
edges.append((s.group(1) if s else "?", t.group(1) if t else "?", label))
|
||
|
||
def short(cid: str) -> str:
|
||
"""连线清单里只用模块名首段,否则整行被模块说明撑爆。"""
|
||
return names.get(cid, cid).split(" / ")[0]
|
||
|
||
lines = [f"模块 {len(names)} 个:"]
|
||
lines += [f" [{k}] {v}" for k, v in names.items()]
|
||
lines.append(f"连线 {len(edges)} 条:")
|
||
lines += [f" {short(s)} -> {short(t)}" + (f" ({lbl})" if lbl else "")
|
||
for s, t, lbl in edges]
|
||
return "\n".join(lines)
|
||
|
||
|
||
def main() -> int:
|
||
ap = argparse.ArgumentParser(description="把任意 drawio 文件规整成 mxGraphModel XML")
|
||
ap.add_argument("path", type=Path)
|
||
ap.add_argument("--summary", action="store_true", help="只打印模块/连线清单")
|
||
args = ap.parse_args()
|
||
if not args.path.is_file():
|
||
raise SystemExit(f"文件不存在:{args.path}")
|
||
xml = to_xml(args.path)
|
||
print(summarize(xml) if args.summary else xml)
|
||
return 0
|
||
|
||
|
||
if __name__ == "__main__":
|
||
sys.exit(main())
|