From fc33cc9ad792c0882767aca0930b52ac3ef7b28f Mon Sep 17 00:00:00 2001 From: shellway <413209390@qq.com> Date: Thu, 6 Aug 2026 14:22:58 +0800 Subject: [PATCH] add tool --- equity/generate_from_master.py | 171 +++++++++++++++++++++++++++++++++ 1 file changed, 171 insertions(+) create mode 100644 equity/generate_from_master.py diff --git a/equity/generate_from_master.py b/equity/generate_from_master.py new file mode 100644 index 0000000..465ee0e --- /dev/null +++ b/equity/generate_from_master.py @@ -0,0 +1,171 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +generate_from_master.py — 从"金地商置法人主数据信息"源表直接生成 drawio 股权架构图 +================================================================================ +用法: + python3 generate_from_master.py 源表.xlsx [输出.drawio] [--internal-only] + +读取: + 01法人公司信息 -> 公司主数据(名称/信用代码/二级组织) + 02公司股东信息 -> 股东关系(股东企业名称 → 企业名称, 股权出资比例) + +--internal-only: 只画"体系内"关系(股东也在01清单里),聚焦集团内部架构 +默认: 全量(含外部股东/自然人股东) +""" +import sys, re, math +import openpyxl + +STYLE_ROOT = "rounded=1;whiteSpace=wrap;html=1;fillColor=#1e6fd9;fontColor=#ffffff;strokeColor=#0d47a1;fontSize=13;fontStyle=1;" +STYLE_NODE = "rounded=1;whiteSpace=wrap;html=1;fillColor=#e8f0fe;fontColor=#000000;strokeColor=#5b9bd5;fontSize=11;" +STYLE_LEAF = "rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;fontColor=#000000;strokeColor=#999999;fontSize=11;" +STYLE_EXT = "rounded=1;whiteSpace=wrap;html=1;fillColor=#fff8e1;fontColor=#000000;strokeColor=#e6a700;fontSize=10;dashed=1;" +STYLE_EDGE = "edgeStyle=orthogonalEdgeStyle;rounded=1;html=1;strokeColor=#666666;strokeWidth=1.5;fontSize=10;" +COL_W, COL_H = 190, 44 +GAP_X, GAP_Y = 40, 90 + + +def parse_ratio(v): + if v is None: + return None + if isinstance(v, (int, float)): + return float(v) + s = str(v).strip() + if not s: + return None + m = re.search(r"([\d.]+)\s*%", s) + if m: + return float(m.group(1)) / 100 + m = re.search(r"([\d.]+)", s) + if m: + return float(m.group(1)) + return None + + +def load_data(xlsx_path): + wb = openpyxl.load_workbook(xlsx_path, data_only=True) + # 01 公司主数据 + ws1 = wb["01法人公司信息"] + companies = {} + for r in ws1.iter_rows(values_only=True): + if not r or not r[2]: + continue + name = str(r[2]).strip() + if not name: + continue + companies[name] = { + "code": str(r[5]).strip() if r[5] else "", + "org2": str(r[4]).strip() if r[4] else "", + } + # 02 股东关系 + ws2 = wb["02公司股东信息"] + rels = [] + for r in ws2.iter_rows(values_only=True): + if not r or not r[2] or not r[3]: + continue + company = str(r[2]).strip() + holder = str(r[3]).strip() + if not company or not holder: + continue + ratio = parse_ratio(r[6]) if len(r) > 6 else None + rels.append((holder, company, ratio)) + return companies, rels + + +def build_graph(rels, internal_only, companies): + """返回 (nodes, edges, roots)。nodes: {name: {ext, org2}}""" + node_info = {} + edges = [] + for holder, company, ratio in rels: + if internal_only and holder not in companies: + continue # 只保留体系内股东 + node_info.setdefault(holder, {"ext": holder not in companies}) + node_info.setdefault(company, {"ext": company not in companies}) + edges.append((holder, company, ratio)) + as_child = {c for _, c, _ in edges} + roots = [n for n in node_info if n not in as_child] + return node_info, edges, roots + + +def layout(node_info, edges, roots): + """BFS 分层布局, 返回 {name: (x, y)} 和层级数""" + children = {} + for h, c, _ in edges: + children.setdefault(h, []).append(c) + level = {} + def dfs(n, lv): + if n in level and level[n] <= lv: + return + level[n] = lv + for ch in children.get(n, []): + dfs(ch, lv + 1) + for r in roots: + dfs(r, 0) + for n in node_info: + level.setdefault(n, 0) + by_level = {} + for n in node_info: + by_level.setdefault(level[n], []).append(n) + max_lv = max(level.values()) + pos = {} + for lv, nodes in by_level.items(): + n = len(nodes) + x0 = (n - 1) * (COL_W + GAP_X) / 2 + for i, node in enumerate(nodes): + pos[node] = (x0 - i * (COL_W + GAP_X), lv * (COL_H + GAP_Y)) + return pos, max_lv + + +def generate_drawio(node_info, edges, roots, out_path): + pos, max_lv = layout(node_info, edges, roots) + canvas_w = max((x + COL_W for x, _ in pos.values()), default=800) + 120 + canvas_h = (max_lv + 1) * (COL_H + GAP_Y) + 80 + children = {p for p, _, _ in edges} + lines = [f''] + lines.append('') + nid = 1 + for node in pos: + nid += 1 + x, y = pos[node] + if node in roots: + style = STYLE_ROOT + elif node_info[node]["ext"]: + style = STYLE_EXT + elif node in children: + style = STYLE_NODE + else: + style = STYLE_LEAF + label = node.replace("&", "&").replace("<", "<").replace(">", ">").replace('"', """) + lines.append(f'') + pos[node] = (nid, x, y) + for h, c, ratio in edges: + nid += 1 + if h not in pos or c not in pos: + continue + hid, _, _ = pos[h] + cid, _, _ = pos[c] + label = f"{ratio*100:.0f}%" if ratio is not None else "" + lines.append(f'') + lines.append('') + with open(out_path, "w", encoding="utf-8") as f: + f.write("\n".join(lines)) + return len(pos), len(edges), max_lv + + +def main(): + if len(sys.argv) < 2: + sys.exit(__doc__) + xlsx_path = sys.argv[1] + out_path = sys.argv[2] if len(sys.argv) > 2 and not sys.argv[2].startswith("--") else xlsx_path.rsplit(".", 1)[0] + ".drawio" + internal_only = "--internal-only" in sys.argv + companies, rels = load_data(xlsx_path) + node_info, edges, roots = build_graph(rels, internal_only, companies) + n_nodes, n_edges, max_lv = generate_drawio(node_info, edges, roots, out_path) + mode = "体系内(仅我司股东)" if internal_only else "全量(含外部股东)" + print(f"✅ {out_path}") + print(f" 模式: {mode} | 节点数: {n_nodes} | 关系数: {n_edges} | 最大层级: {max_lv+1} 层") + print(f" 根节点: {len(roots)} 个") + + +if __name__ == "__main__": + main()