侧边栏壁纸
博主头像
いちごみるく

行动起来,活在当下

  • 累计撰写 70 篇文章
  • 累计创建 48 个标签
  • 累计收到 55 条评论

目 录CONTENT

文章目录

基于python实现将excel表格转换为xmind文件

示例面向第三方 XMind==1.2.0 生成的传统 XML 格式。脚本读取 Excel 层级列,检查空值与输出文件结构;ZIP/XML 检查不能代替各版本 XMind 客户端的兼容性验证。

用 Python 将 Excel 层级表转换为 XMind

Excel 每一行表示一条从根向叶子的路径,每一列表示一个层级。相同父节点下的同名子节点只生成一次;不同父节点下的同名节点仍然分别保留。适合把业务指标、分类目录等表格转成可编辑的思维导图。

L1 L2 驱动杠杆 业务域 管理重点 分析主题 指标标准名称
Level1_A Level2_A1 Lever_A1 Business_A1 Management_A1 Analysis_A1 Standard_A1
Level1_A Level2_A1 Lever_A1 Business_A1 Management_A1 Analysis_A1 Standard_A2
Level1_B Level2_B1 Lever_B1 Business_B1 Management_B1 Analysis_B1 Standard_B1

原始层级转换效果

一、安装和输入约定

python -m pip install pandas openpyxl XMind==1.2.0

这里的 PyPI 包是第三方项目 zhuifengshen/xmind,并非当前 XMind 桌面软件的官方 Python API。1.2.0 发布于 2019 年;固定该版本是为了对应本文使用的接口,不能由此推断它支持所有新版格式。PyPI 项目页面

把 Excel 保存为 input.xlsx,首行为列名。脚本显式指定从左到右的层级列,不会把额外的备注、编号列自动变成主题。末尾空层级表示路径结束;中间空层级后还有内容时拒绝转换,因为静默跳过会改变层级关系。全空行忽略。合并单元格需要先在 Excel 中填充为完整路径,本文不会猜测空白是继承上一行还是结束。

read_excel(..., keep_default_na=False) 避免把字符串 NA、NULL 当作默认缺失值;转换函数也处理真正的 None 和 NaN。有前导零的编码应在 Excel 中存成文本,数字单元格的显示格式不会自动变成字符串值。pandas Excel 读取说明

二、完整脚本

保存为 excel_to_xmind.py。默认拒绝覆盖已有文件,输出失败会抛出异常;不要把失败提示捕获后仍然显示“完成”。

from pathlib import Path
import shutil
import tempfile
import xml.etree.ElementTree as ET
import zipfile

import pandas as pd
import xmind

LEVELS = ['L1', 'L2', '驱动杠杆', '业务域', '管理重点', '分析主题', '指标标准名称']
MANIFEST = 'META-INF/manifest.xml'
NS = 'urn:xmind:xmap:xmlns:manifest:1.0'


def normalize(value):
    if value is None or pd.isna(value):
        return ''
    return str(value).strip()


def rebuild_archive(source, target):
    """重建 ZIP,移除旧 manifest,生成一个且仅一个清单条目。"""
    ET.register_namespace('', NS)
    manifest = ET.Element(f'{{{NS}}}manifest')
    with zipfile.ZipFile(source, 'r') as src:
        names = src.namelist()
        if len(names) != len(set(names)):
            raise ValueError('输入 XMind 压缩包包含重复条目')
        if 'content.xml' not in names or src.testzip() is not None:
            raise ValueError('输入 XMind 压缩包缺少正文或校验失败')
        ET.fromstring(src.read('content.xml'))
        with zipfile.ZipFile(target, 'w', compression=zipfile.ZIP_DEFLATED) as dst:
            for info in src.infolist():
                if info.filename == MANIFEST:
                    continue
                dst.writestr(info, src.read(info.filename))
                if not info.is_dir() and not info.filename.startswith('META-INF/'):
                    ET.SubElement(manifest, f'{{{NS}}}file-entry', {'full-path': info.filename})
            dst.writestr(MANIFEST, ET.tostring(manifest, encoding='utf-8', xml_declaration=True))
    with zipfile.ZipFile(target) as check:
        if check.namelist().count(MANIFEST) != 1 or check.testzip() is not None:
            raise ValueError('输出 XMind 压缩包校验失败')
        ET.fromstring(check.read(MANIFEST))


def dataframe_to_xmind(df, output, levels=LEVELS, root_title='指标体系'):
    output = Path(output)
    if output.suffix.lower() != '.xmind':
        raise ValueError('输出文件必须使用 .xmind 扩展名')
    if not levels or len(levels) != len(set(levels)):
        raise ValueError('层级列必须非空且不能重复')
    if df.columns.has_duplicates:
        raise ValueError('输入表包含重复列名')
    missing = [name for name in levels if name not in df.columns]
    if missing:
        raise ValueError(f'缺少层级列:{missing}')
    if output.exists():
        raise FileExistsError(f'输出文件已存在:{output}')
    if not output.parent.is_dir():
        raise FileNotFoundError(f'输出目录不存在:{output.parent}')

    paths = []
    for row_number, values in enumerate(df.loc[:, levels].itertuples(index=False, name=None), start=2):
        titles = [normalize(value) for value in values]
        if not any(titles):
            continue
        end = next((i for i, title in enumerate(titles) if not title), len(titles))
        if any(titles[end:]):
            raise ValueError(f'Excel 第 {row_number} 行有中间缺失层级')
        paths.append(titles[:end])
    if not paths:
        raise ValueError('没有可转换的非空路径')

    # 在临时目录创建新工作簿,绝不加载目标文件后继续追加。
    with tempfile.TemporaryDirectory() as temp_dir:
        source = Path(temp_dir) / 'new.xmind'
        fixed = Path(temp_dir) / 'fixed.xmind'
        workbook = xmind.load(str(source))
        sheet = workbook.getPrimarySheet()
        sheet.setTitle(root_title)
        root = sheet.getRootTopic()
        root.setTitle(root_title)
        nodes = {(): root}
        for titles in paths:
            for depth, title in enumerate(titles, start=1):
                key = tuple(titles[:depth])
                if key not in nodes:
                    child = nodes[key[:-1]].addSubTopic()
                    child.setTitle(title)
                    nodes[key] = child
        xmind.save(workbook, str(source))
        rebuild_archive(source, fixed)
        # 独占创建处理“检查存在后,其他进程刚好创建文件”的竞争。
        with fixed.open('rb') as src, output.open('xb') as dst:
            try:
                shutil.copyfileobj(src, dst)
            except BaseException:
                dst.close()
                output.unlink(missing_ok=True)
                raise
    return output


if __name__ == '__main__':
    frame = pd.read_excel('input.xlsx', sheet_name=0, engine='openpyxl',
                          dtype=object, keep_default_na=False)
    result = dataframe_to_xmind(frame, 'output.xmind')
    print(f'已生成:{result}')

三、为什么这样构建

节点缓存的键是完整路径,例如 ('Level1_A', 'Level2_A1'),因此能合并同一路径的公共祖先,也不会把不同父节点下的同名节点混为一谈。第一次出现的顺序就是主题顺序。strip() 会合并只有首尾空格不同的标题;若空格具有业务含义,需要调整这个规则。

传统 .xmind 文件是 ZIP 容器。若在追加模式下写入 META-INF/manifest.xml,已有清单时会产生两个同名条目,不同读取器可能选择不同条目。因此脚本重建压缩包,并检查条目唯一性、XML 和 CRC。库保存器负责 content.xml 等正文文件,清单处理用于适配这个版本的库。库保存器源码、Python ZIP 文档

这不是通用“修复所有 XMind 文件”的工具。生成后还要在实际使用的 XMind 客户端打开,检查层级、中文标题和保存后重新打开的结果。没有完成这一步时,只能确认文件结构检查通过。

0

评论区