sync: WIN10-I9-64G 2026-09-19
This commit is contained in:
@@ -0,0 +1,112 @@
|
||||
"""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())
|
||||
Reference in New Issue
Block a user