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

需要修改的内容:
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()