"""drawio_read.py — 把任意 drawio 文件规整成纯 mxGraphModel XML 打到 stdout。 为什么需要它:drawio MCP 的 new_diagram 写出来的**不是**纯 mxfile XML,而是 SVG 外壳 + `content="..."`,里面的 diagram 段还是 base64+raw-deflate 压缩的。人手在 drawio 桌面版里存的则常常是纯 XML。两种都要能读,否则 n18(按原理图回写 drawio)和 d21(原理图与 drawio 一致性比对)根本没法比。 用法: python drawio_read.py # 打印 mxGraphModel XML python drawio_read.py --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(("]*>\s*([A-Za-z0-9+/=]+)\s*", 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']*?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"|)", 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())