From 4e7f59927458abd220a79095bb259fc448f106ba Mon Sep 17 00:00:00 2001 From: xiaowu Date: Fri, 7 Aug 2026 11:39:00 +0800 Subject: [PATCH] =?UTF-8?q?=E7=A7=BB=E9=99=A4=20equity=20=E6=96=87?= =?UTF-8?q?=E4=BB=B6=E5=A4=B9=EF=BC=88=E8=AF=AF=E6=94=BE=EF=BC=9A=E5=86=85?= =?UTF-8?q?=E5=AE=B9=E5=B1=9E=E4=BA=8E=20gemdalepi=20=E9=A1=B9=E7=9B=AE?= =?UTF-8?q?=EF=BC=8Cgemdalepi=20=E5=B7=B2=E6=9C=89=E6=9B=B4=E6=96=B0?= =?UTF-8?q?=E7=89=88=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- equity/README.md | 21 - equity/excel2drawio.py | 173 --- equity/generate_from_master.py | 171 --- equity/金地商置_体系内.drawio | 1215 ------------------ equity/金地商置_全量.drawio | 2207 -------------------------------- 5 files changed, 3787 deletions(-) delete mode 100644 equity/README.md delete mode 100644 equity/excel2drawio.py delete mode 100644 equity/generate_from_master.py delete mode 100644 equity/金地商置_体系内.drawio delete mode 100644 equity/金地商置_全量.drawio diff --git a/equity/README.md b/equity/README.md deleted file mode 100644 index 1897889..0000000 --- a/equity/README.md +++ /dev/null @@ -1,21 +0,0 @@ -# 集团股权架构可视化(金地商置境内数据) - -## 文件说明 -- `金地商置_体系内.drawio` — 仅我司体系内股东关系(578节点/634关系/7层) -- `金地商置_全量.drawio` — 含外部股东全量(1031节点/1172关系/6层) -- `excel2drawio.py` — 通用转换工具(读"母公司|子公司|比例"格式Excel) -- `generate_from_master.py` — 直接读主数据表(01法人公司信息+02公司股东信息)生成 - -## 用法 -```bash -# 通用模板方式 -python3 excel2drawio.py 表格.xlsx 输出.drawio -# 主数据表方式 -python3 generate_from_master.py 主数据.xlsx 输出.drawio [--internal-only] -``` - -## 生成记录 -- 2026-08-06: 基于《金地商置法人主数据信息-境内-2024.xlsx》生成 - - 01表: 673家(境内665+BVI 2),去重后667 - - 02表: 1171条股东关系 - - 体系内股东: 216家 diff --git a/equity/excel2drawio.py b/equity/excel2drawio.py deleted file mode 100644 index d4b48e1..0000000 --- a/equity/excel2drawio.py +++ /dev/null @@ -1,173 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -excel2drawio.py — 从 Excel 股权关系表生成 drawio 股权架构图 -============================================================= -用法: - python3 excel2drawio.py 输入.xlsx [输出.drawio] - -Excel 格式 (Sheet 名任意, 表头必须为以下列名): - 母公司名称 | 子公司名称 | 股权比例(%) | 备注 - 每行 = 一条股权关系。无母公司的公司 = 根节点(集团/母公司)。 - 例: - 母公司名称 子公司名称 股权比例(%) 备注 - 集团总公司 子公司A 100 全资 - 集团总公司 子公司B 60 控股 - 子公司A 孙公司C 51 控股 - -输出: drawio 格式 XML, 可用 drawio 桌面版/网页版打开, 或嵌入网页。 -""" -import sys, 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_EDGE = "edgeStyle=orthogonalEdgeStyle;rounded=1;html=1;strokeColor=#666666;strokeWidth=1.5;fontSize=10;" -COL_W, COL_H = 180, 44 # 节点宽高 -GAP_X, GAP_Y = 30, 80 # 横向/纵向间距 - - -def read_relations(xlsx_path): - """读取 Excel, 返回 (关系列表, 所有公司集合)""" - wb = openpyxl.load_workbook(xlsx_path, data_only=True) - ws = wb.active - rows = list(ws.iter_rows(values_only=True)) - if not rows: - sys.exit("Excel 为空") - header = [str(c).strip() if c else "" for c in rows[0]] - # 兼容列名变体:先精确匹配,再包含匹配(按 names 顺序优先) - def col(*names): - for i, h in enumerate(header): - if h in names: - return i - for n in names: - for i, h in enumerate(header): - if n in h: - return i - return None - i_parent = col("母公司", "父公司", "上级") - i_child = col("子公司", "公司", "被投") - i_ratio = col("股权比例", "比例", "持股") - i_note = col("备注") - if i_parent is None or i_child is None: - sys.exit(f"表头需要包含「母公司名称」「子公司名称」列, 实际: {header}") - relations, companies = [], set() - for r in rows[1:]: - if not r or all(v is None or str(v).strip() == "" for v in r): - continue - parent = str(r[i_parent]).strip() if i_parent is not None and r[i_parent] else "" - child = str(r[i_child]).strip() if r[i_child] else "" - if not child: - continue - ratio = r[i_ratio] if i_ratio is not None and r[i_ratio] is not None else "" - note = str(r[i_note]).strip() if i_note is not None and r[i_note] else "" - relations.append((parent, child, ratio, note)) - companies.add(child) - if parent: - companies.add(parent) - return relations, companies - - -def build_levels(relations, companies): - """计算每家公司层级 (根=0), 返回 {公司: 层级}""" - children = {} - for p, c, _, _ in relations: - children.setdefault(p, []).append(c) - # 没有作为子节点出现的公司 = 根(集团/母公司) - as_child = {c for _, c, _, _ in relations} - roots = [c for c in companies if c not in as_child] - level = {} - def dfs(node, lv): - if node in level and level[node] <= lv: - return - level[node] = lv - for ch in children.get(node, []): - dfs(ch, lv + 1) - for r in roots: - dfs(r, 0) - # 兜底: 未覆盖的孤儿节点 - for c in companies: - if c not in level: - level[c] = 0 - return level, roots - - -def generate_drawio(relations, companies, xlsx_path): - level, roots = build_levels(relations, companies) - # 按层级分组, 保持输入顺序 - order = [] - seen = set() - def add(n): - if n not in seen: - seen.add(n); order.append(n) - for p, c, _, _ in relations: - add(p); add(c) - for c in companies: - add(c) - by_level = {} - for n in order: - by_level.setdefault(level[n], []).append(n) - max_lv = max(level.values()) - # 计算每层宽度 → 画布大小 - positions = {} - for lv, nodes in by_level.items(): - n = len(nodes) - x0 = (n - 1) * (COL_W + GAP_X) / 2 - for i, node in enumerate(nodes): - positions[node] = (x0 - i * (COL_W + GAP_X), lv * (COL_H + GAP_Y)) - canvas_w = max((x + COL_W for x, _ in positions.values()), default=800) + 100 - canvas_h = (max_lv + 1) * (COL_H + GAP_Y) + 60 - - # 节点样式: 根=深蓝, 有子公司=浅蓝, 叶子=灰 - children = {p for p, _, _, _ in relations} - lines = [] - lines.append(f'') - lines.append('') - nid = 1 - for node in order: - nid += 1 - x, y = positions[node] - if node in roots: - style = STYLE_ROOT - elif node in children: - style = STYLE_NODE - else: - style = STYLE_LEAF - lines.append(f'') - positions[node] = (nid, x, y) # 记录 id - for p, c, ratio, note in relations: - nid += 1 - pid, _, _ = positions[p] if p in positions else (None, None, None) - cid, _, _ = positions[c] if c in positions else (None, None, None) - if pid is None or cid is None: - continue - label = "" - if ratio not in (None, ""): - label = f"{ratio}%" - if note: - label += f" ({note})" - lines.append(f'') - lines.append('') - return "\n".join(lines) - - -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 else xlsx_path.rsplit(".", 1)[0] + ".drawio" - relations, companies = read_relations(xlsx_path) - if not companies: - sys.exit("没有读到任何公司") - xml = generate_drawio(relations, companies, xlsx_path) - with open(out_path, "w", encoding="utf-8") as f: - f.write(xml) - print(f"✅ 生成成功: {out_path}") - print(f" 公司数: {len(companies)}, 关系数: {len(relations)}") - print(" 用 drawio 打开该文件即可查看架构图") - - -if __name__ == "__main__": - main() diff --git a/equity/generate_from_master.py b/equity/generate_from_master.py deleted file mode 100644 index 465ee0e..0000000 --- a/equity/generate_from_master.py +++ /dev/null @@ -1,171 +0,0 @@ -#!/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() diff --git a/equity/金地商置_体系内.drawio b/equity/金地商置_体系内.drawio deleted file mode 100644 index 2c13976..0000000 --- a/equity/金地商置_体系内.drawio +++ /dev/null @@ -1,1215 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/equity/金地商置_全量.drawio b/equity/金地商置_全量.drawio deleted file mode 100644 index c2e5afb..0000000 --- a/equity/金地商置_全量.drawio +++ /dev/null @@ -1,2207 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file