Excel测试用例文档文档转换亿图xmind工具

被转换文件和代码需在同一级目录下

需要修改的内容:

python 复制代码
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
将Excel测试用例文件转换为XMind脑图文件(.xmind)
亿图脑图(MindMaster)可以打开.xmind文件
只读取第一个sheet,支持控制台交互选择导出的列
节点结构: 主题 → 功能模块 → 子模块 → 测试点 → 操作步骤 → 预期结果
"""

import openpyxl
import os
import zipfile
import time
from collections import OrderedDict
from xml.sax.saxutils import escape

# ============================================================
# 配置
# ============================================================
EXCEL_FILE = "研发资源监控分析看板_测试用例.xlsx"
OUTPUT_FILE = "研发资源监控分析看板_测试用例.xmind"


def escape_xml(text):
    """转义XML特殊字符"""
    if text is None:
        return ''
    return escape(str(text))


def get_timestamp():
    """获取当前时间戳(毫秒)"""
    return str(int(time.time() * 1000))


class XMindBuilder:
    """构建XMind (.xmind) 格式的脑图文件"""

    def __init__(self):
        self.topic_counter = 0

    def _new_id(self):
        self.topic_counter += 1
        return f"ID_{self.topic_counter:08d}"

    def _build_topic_xml(self, title, children_xml='', structure_class=None):
        """构建单个topic的XML"""
        topic_id = self._new_id()
        timestamp = get_timestamp()

        attrs = f'id="{topic_id}" timestamp="{timestamp}"'
        if structure_class:
            attrs += f' structureClass="{structure_class}"'

        xml = f'    <topic {attrs}>\n'
        xml += f'      <title>{escape_xml(title)}</title>\n'
        if children_xml:
            xml += f'      <children>\n'
            xml += f'        <topics type="attached">\n'
            xml += children_xml
            xml += f'        </topics>\n'
            xml += f'      </children>\n'
        xml += f'    </topic>\n'
        return xml

    def _build_leaf_topic_xml(self, title):
        """构建叶子topic(无子节点)"""
        topic_id = self._new_id()
        timestamp = get_timestamp()
        return f'    <topic id="{topic_id}" timestamp="{timestamp}">\n      <title>{escape_xml(title)}</title>\n    </topic>\n'

    def build_content_xml(self, data):
        """构建content.xml内容"""
        timestamp = get_timestamp()
        sheet_id = self._new_id()
        root_topic_id = self._new_id()

        # 构建所有子主题的XML
        children_xml = ''

        # ===== 测试用例总表(第一个sheet)=====
        children_xml += self._build_hierarchy_section(data.get('test_cases', []))

        # 构建完整的content.xml
        content = '<?xml version="1.0" encoding="UTF-8" standalone="no"?>\n'
        content += '<xmap-content xmlns="urn:xmind:xmap:xmlns:content:2.0" '
        content += 'xmlns:fo="http://www.w3.org/1999/XSL/Format" '
        content += 'xmlns:svg="http://www.w3.org/2000/svg" '
        content += 'xmlns:xhtml="http://www.w3.org/1999/xhtml" '
        content += 'xmlns:xlink="http://www.w3.org/1999/xlink" '
        content += 'version="2.0">\n'
        content += f'  <sheet id="{sheet_id}" timestamp="{timestamp}" version="2.0">\n'
        content += f'    <topic id="{root_topic_id}" timestamp="{timestamp}" structureClass="org.xmind.ui.map.unbalanced">\n'
        content += '      <title>测试用例</title>\n'
        content += '      <children>\n'
        content += '        <topics type="attached">\n'
        content += children_xml
        content += '        </topics>\n'
        content += '      </children>\n'
        content += '    </topic>\n'
        content += '  </sheet>\n'
        content += '</xmap-content>\n'

        return content

    def _build_hierarchy_section(self, test_cases):
        """
        构建层级结构的XML
        节点结构: 功能模块 → 子模块 → 测试点 → 操作步骤 → 预期结果
        不使用任何特殊图标
        """
        xml = ''

        # 第一层:功能模块
        modules = OrderedDict()
        for tc in test_cases:
            module = tc.get('module', '未分类')
            if module not in modules:
                modules[module] = []
            modules[module].append(tc)

        for module_name, module_cases in modules.items():
            # 第二层:子模块
            sub_modules = OrderedDict()
            for tc in module_cases:
                sub_module = tc.get('sub_module', '')
                if not sub_module:
                    sub_module = '默认子模块'
                if sub_module not in sub_modules:
                    sub_modules[sub_module] = []
                sub_modules[sub_module].append(tc)

            module_children = ''
            for sub_module_name, sub_cases in sub_modules.items():
                # 第三层:测试点
                test_points = OrderedDict()
                for tc in sub_cases:
                    test_point = tc.get('test_point', '')
                    if not test_point:
                        test_point = '默认测试点'
                    if test_point not in test_points:
                        test_points[test_point] = []
                    test_points[test_point].append(tc)

                sub_module_children = ''
                for test_point_name, tp_cases in test_points.items():
                    # 第四层:操作步骤(测试点的子节点)
                    steps_children = ''
                    for tc in tp_cases:
                        steps = tc.get('steps', '')
                        expected = tc.get('expected', '')

                        # 第五层:预期结果(操作步骤的子节点)
                        expected_children = ''
                        if expected:
                            expected_children += self._build_leaf_topic_xml(f"预期结果: {expected}")

                        # 操作步骤节点,预期结果作为其子节点
                        if steps:
                            steps_children += self._build_topic_xml(f"操作步骤: {steps}", expected_children)
                        elif expected:
                            # 如果没有操作步骤但有预期结果,直接挂在测试点下
                            steps_children += self._build_leaf_topic_xml(f"预期结果: {expected}")

                    sub_module_children += self._build_topic_xml(test_point_name, steps_children)

                module_children += self._build_topic_xml(sub_module_name, sub_module_children)

            xml += self._build_topic_xml(module_name, module_children)

        return xml

    def build_manifest_xml(self):
        """构建META-INF/manifest.xml"""
        return '''<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<manifest xmlns="urn:xmind:xmap:xmlns:manifest:1.0">
  <file-entry full-path="content.xml" media-type="text/xml"/>
  <file-entry full-path="META-INF/" media-type=""/>
  <file-entry full-path="META-INF/manifest.xml" media-type="text/xml"/>
  <file-entry full-path="meta.xml" media-type="text/xml"/>
</manifest>
'''

    def build_meta_xml(self):
        """构建meta.xml"""
        timestamp = get_timestamp()
        return f'''<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<meta xmlns="urn:xmind:xmap:xmlns:meta:2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" version="2.0">
  <author>
    <name>Excel2XMind</name>
  </author>
  <created>{timestamp}</created>
</meta>
'''

    def save(self, filepath, data):
        """保存为.xmind文件"""
        content_str = self.build_content_xml(data)
        manifest_str = self.build_manifest_xml()
        meta_str = self.build_meta_xml()

        with zipfile.ZipFile(filepath, 'w', zipfile.ZIP_DEFLATED) as zf:
            zf.writestr('content.xml', content_str.encode('utf-8'))
            zf.writestr('META-INF/manifest.xml', manifest_str.encode('utf-8'))
            zf.writestr('meta.xml', meta_str.encode('utf-8'))

        print(f"✅ XMind文件已生成: {filepath}")
        print(f"   文件大小: {os.path.getsize(filepath)} 字节")


# ============================================================
# 交互式列选择
# ============================================================

def select_columns_interactive(headers):
    """
    交互式选择要导出的列
    返回选中的列索引列表(按用户选择的顺序)
    """
    print("\n" + "=" * 60)
    print("📊 Excel表头信息(第一个sheet)")
    print("=" * 60)

    # 显示所有列
    for i, header in enumerate(headers, 1):
        print(f"  [{i}] {header}")

    print("-" * 60)
    print("请按层级顺序输入列编号(用逗号分隔)")
    print("层级顺序: 功能模块 → 子模块 → 测试点 → 操作步骤 → 预期结果")
    print("例如: 2,3,4,6,7 表示第2列是功能模块,第3列是子模块...")
    print("输入 'q' 退出")
    print("-" * 60)

    while True:
        user_input = input("请选择: ").strip()

        if user_input.lower() == 'q':
            print("❌ 用户取消操作")
            return None

        # 解析用户输入(支持逗号、空格、中文逗号分隔)
        import re
        cleaned = re.sub(r'[,\s]+', ',', user_input)
        parts = cleaned.split(',')

        selected = []
        valid = True
        for part in parts:
            part = part.strip()
            if not part:
                continue
            try:
                num = int(part)
                if 1 <= num <= len(headers):
                    selected.append(num)
                else:
                    print(f"⚠️ 编号 {num} 超出范围(1-{len(headers)}),请重新输入")
                    valid = False
                    break
            except ValueError:
                print(f"⚠️ 无效输入: '{part}',请输入数字")
                valid = False
                break

        if valid and selected:
            return selected

        if not valid:
            continue

        print("⚠️ 未选择任何列,请重新输入")


# ============================================================
# Excel 数据解析(只读取第一个sheet)
# ============================================================

def read_excel_data(filepath, selected_col_indices):
    """
    读取Excel文件的第一个sheet数据
    selected_col_indices: 用户选择的列编号列表(从1开始)
    按层级结构解析: 功能模块 → 子模块 → 测试点 → 操作步骤 → 预期结果
    """
    wb = openpyxl.load_workbook(filepath, data_only=True)

    # 获取第一个sheet
    first_sheet_name = wb.sheetnames[0]
    ws = wb[first_sheet_name]
    print(f"📋 读取第一个sheet: {first_sheet_name}")

    # 读取表头(第一行)
    headers = []
    for cell in ws[1]:
        if cell.value is not None:
            headers.append(str(cell.value).strip())
        else:
            headers.append(f"列{cell.column}")

    # 将用户选择的列编号(1-based)转换为0-based索引
    selected_indices_0based = [i - 1 for i in selected_col_indices]

    # 获取选中的表头名称
    selected_headers = [headers[i] for i in selected_indices_0based]
    print(f"✅ 选中的列: {selected_headers}")
    print(f"   列编号: {selected_col_indices}")

    # 确定各层级对应的列
    num_cols = len(selected_indices_0based)

    # 读取数据行
    test_cases = []

    for row in ws.iter_rows(min_row=2, values_only=False):
        values = [cell.value for cell in row]

        # 跳过全空行
        if all(v is None or str(v).strip() == '' for v in values):
            continue

        # 提取选中列的数据
        case_data = {}
        for idx, col_idx in enumerate(selected_indices_0based):
            if col_idx < len(values) and values[col_idx] is not None:
                value = str(values[col_idx]).strip()
            else:
                value = ''
            case_data[selected_headers[idx]] = value

        # 根据层级结构映射字段
        # 第一列 → 功能模块
        # 第二列 → 子模块
        # 第三列 → 测试点
        # 第四列 → 操作步骤
        # 第五列 → 预期结果

        if num_cols >= 1:
            case_data['module'] = case_data.get(selected_headers[0], '') or '未分类'
        if num_cols >= 2:
            case_data['sub_module'] = case_data.get(selected_headers[1], '') or '默认子模块'
        if num_cols >= 3:
            case_data['test_point'] = case_data.get(selected_headers[2], '') or '默认测试点'
        if num_cols >= 4:
            case_data['steps'] = case_data.get(selected_headers[3], '')
        if num_cols >= 5:
            case_data['expected'] = case_data.get(selected_headers[4], '')

        test_cases.append(case_data)

    wb.close()
    return {'test_cases': test_cases}


# ============================================================
# 主函数
# ============================================================

def main():
    excel_file = EXCEL_FILE

    if not os.path.exists(excel_file):
        print(f"❌ 错误: 找不到Excel文件: {excel_file}")
        return

    print(f"📖 正在读取Excel文件: {excel_file}")

    # 先读取表头信息
    wb = openpyxl.load_workbook(excel_file, data_only=True)
    first_sheet_name = wb.sheetnames[0]
    ws = wb[first_sheet_name]

    # 读取表头
    headers = []
    for cell in ws[1]:
        if cell.value is not None:
            headers.append(str(cell.value).strip())
        else:
            headers.append(f"列{cell.column}")
    wb.close()

    if not headers:
        print("❌ 错误: 第一个sheet没有表头")
        return

    # 交互式选择列
    selected_cols = select_columns_interactive(headers)
    if selected_cols is None:
        return

    # 读取数据
    data = read_excel_data(excel_file, selected_cols)

    print(f"  ✅ 测试用例: {len(data.get('test_cases', []))}条")

    print("🔄 正在构建XMind脑图...")
    builder = XMindBuilder()
    builder.save(OUTPUT_FILE, data)

    print(f"🎉 转换完成!输出文件: {OUTPUT_FILE}")
    print(f"💡 请使用亿图脑图(MindMaster)打开 {OUTPUT_FILE}")

if __name__ == '__main__':
    main()
相关推荐
程序员杰哥6 小时前
依赖于第三方接口时,如何进行测试?
自动化测试·软件测试·python·测试工具·职场和发展·测试用例·接口测试
测试19982 天前
2026全新软件测试面试八股文(含答案+文档)
自动化测试·软件测试·python·测试工具·面试·职场和发展·测试用例
姚青&4 天前
测试流程管理
测试用例
天才测试猿4 天前
软件测试知识总结(基础篇)
自动化测试·软件测试·python·功能测试·测试工具·职场和发展·测试用例
测试老哥4 天前
软件测试:单元测试详解
自动化测试·软件测试·python·测试工具·职场和发展·单元测试·测试用例
姚青&5 天前
测试用例基础概念
测试用例