refactor: remove metadata dependency in import_vba.py for simplified VBA import workflow

- Remove vba_metadata.json dependency
- Add direct VBA directory scanning with _scan_modules() method
- Read target file and output directory from .env configuration
- Simplify import process: specify file and directory instead of metadata file
- Update version to 4.0 (configuration-based approach)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Misaka_Company
2026-02-03 12:56:34 +08:00
parent 59f0ef2125
commit 7b27e50573

View File

@@ -1,17 +1,14 @@
""" """
VBA代码导入工具 (最终修正版) VBA代码导入工具
1. 修复路径识别问题 使用 .env 配置直接导入 VBA 代码,无需元数据文件
2. 修复中文乱码问题 (GB18030)
3. 修复类模块(Class)被错误导入为标准模块的问题 (补全 VERSION 1.0 CLASS 头)
""" """
import os import os
import sys import sys
import json
import shutil import shutil
import tempfile import tempfile
from pathlib import Path from pathlib import Path
from typing import Dict, Any from typing import Dict, List, Tuple
# 加载 .env 配置文件 # 加载 .env 配置文件
try: try:
@@ -29,44 +26,68 @@ except ImportError:
sys.exit(1) sys.exit(1)
# ==================== 配置区域 ==================== # ==================== 配置区域 ====================
# 从 .env 文件读取配置,如果未设置则使用 None默认模式 # 从 .env 文件读取配置
TARGET_METADATA_FILE = os.getenv("TARGET_METADATA_FILE", "").strip() or None # 目标 xlsm 文件路径用于导入VBA代码
TARGET_XLSM_FILE = os.getenv("TARGET_XLSM_FILE", "").strip() or None
# VBA代码输出目录如果未设置则使用源文件同目录下的VBA文件夹
VBA_OUTPUT_DIR = os.getenv("VBA_OUTPUT_DIR", "").strip() or None
# ================================================= # =================================================
# 常量定义 # 常量定义
METADATA_FILE = "vba_metadata.json" STANDARD_MODULE_DIR = "Modules"
VBA_DIR_NAME = "VBA" CLASS_MODULE_DIR = "ClassModules"
DOCUMENT_MODULE_DIR = "DocumentModules"
FORMS_DIR = "Forms"
class VBAImporter: class VBAImporter:
"""VBA代码导入器""" """VBA代码导入器"""
def __init__(self, metadata_path: str): def __init__(self, vba_dir: str, target_file: str):
self.metadata_path = Path(metadata_path).resolve() """
self.vba_dir = self.metadata_path.parent 初始化VBA导入器
self.project_root = self.vba_dir.parent
with open(self.metadata_path, 'r', encoding='utf-8') as f:
self.metadata = json.load(f)
# 处理 Excel 文件路径
raw_path = self.metadata.get("source_file", "")
candidate_path = Path(raw_path)
if not candidate_path.is_absolute(): Args:
candidate_path = self.project_root / candidate_path vba_dir: VBA代码目录包含 Modules, ClassModules 等子目录)
target_file: 目标 Excel 文件路径
"""
self.vba_dir = Path(vba_dir).resolve()
self.target_file = Path(target_file).resolve()
self.target_file = candidate_path def _scan_modules(self) -> List[Dict]:
"""
扫描 VBA 目录,收集所有模块信息
if not self.target_file.exists(): Returns:
print(f"提示: 在路径 {self.target_file} 未找到文件,尝试搜索...") 模块信息列表,每个元素包含:
excel_dir = self.project_root / "Excel" - name: 模块名称
if excel_dir.exists(): - type: 模块类型目录
files = list(excel_dir.glob("*.xlsm")) - file_path: 源文件完整路径
if files: - ext: 文件扩展名
print(f" -> 找到替代文件: {files[0].name}") """
self.target_file = files[0] modules = []
self.target_file = self.target_file.resolve() # 定义扫描目录和对应的扩展名
scan_dirs = [
(STANDARD_MODULE_DIR, ".bas"),
(CLASS_MODULE_DIR, ".cls"),
(DOCUMENT_MODULE_DIR, ".cls"),
(FORMS_DIR, ".frm"),
]
for dir_name, ext in scan_dirs:
dir_path = self.vba_dir / dir_name
if not dir_path.exists():
continue
for file_path in dir_path.glob(f"*{ext}"):
modules.append({
"name": file_path.stem, # 文件名不含扩展名
"type": dir_name,
"file_path": file_path,
"ext": ext
})
return modules
def _clean_component_name(self, name: str) -> str: def _clean_component_name(self, name: str) -> str:
"""去除模块名称中的扩展名""" """去除模块名称中的扩展名"""
@@ -76,7 +97,7 @@ class VBAImporter:
return name[:-len(ext)] return name[:-len(ext)]
return name return name
def _reconstruct_file_content(self, code_path: Path, attributes: Dict[str, str], module_name: str, module_type: str) -> Path: def _reconstruct_file_content(self, code_path: Path, module_name: str, module_type: str) -> Path:
""" """
读取纯代码文件,重建完整的导入文件 读取纯代码文件,重建完整的导入文件
关键逻辑: 关键逻辑:
@@ -90,39 +111,31 @@ class VBAImporter:
# 创建临时文件 # 创建临时文件
temp_dir = Path(tempfile.gettempdir()) / "vba_import_temp" temp_dir = Path(tempfile.gettempdir()) / "vba_import_temp"
temp_dir.mkdir(exist_ok=True) temp_dir.mkdir(exist_ok=True)
# 确定扩展名 # 确定扩展名
orig_ext = code_path.suffix orig_ext = code_path.suffix
temp_file_path = temp_dir / f"{module_name}{orig_ext}" temp_file_path = temp_dir / f"{module_name}{orig_ext}"
content_lines = [] content_lines = []
# ----------------------------------------------------------- # -----------------------------------------------------------
# 【关键修复】如果是类模块,必须添加 VERSION 头部块 # 【关键修复】如果是类模块,必须添加 VERSION 头部块
# ----------------------------------------------------------- # -----------------------------------------------------------
if module_type == "ClassModules": if module_type == CLASS_MODULE_DIR:
content_lines.append("VERSION 1.0 CLASS") content_lines.append("VERSION 1.0 CLASS")
content_lines.append("BEGIN") content_lines.append("BEGIN")
content_lines.append(" MultiUse = -1 'True") content_lines.append(" MultiUse = -1 'True")
content_lines.append("END") content_lines.append("END")
# 注意Attribute VB_Name 必须紧跟在 END 之后
# 2. 重建 Attribute VB_Name # 2. 重建 Attribute VB_Name
content_lines.append(f'Attribute VB_Name = "{module_name}"') content_lines.append(f'Attribute VB_Name = "{module_name}"')
# 3. 重建其他 Attribute # 注意:由于我们移除了元数据,不再有其他属性信息
for key, value in attributes.items(): # 如果需要其他属性,需要从源文件中解析或在代码中显式声明
if key == "VB_Name":
continue content_lines.append("")
if value.lower() in ['true', 'false']:
line = f'Attribute {key} = {value}'
else:
line = f'Attribute {key} = "{value}"'
content_lines.append(line)
content_lines.append("")
content_lines.append(code_body) content_lines.append(code_body)
# 4. 写入临时文件 (GB18030 防止乱码) # 4. 写入临时文件 (GB18030 防止乱码)
try: try:
with open(temp_file_path, 'w', encoding='gb18030', errors='replace') as f: with open(temp_file_path, 'w', encoding='gb18030', errors='replace') as f:
@@ -131,7 +144,7 @@ class VBAImporter:
print(f" [警告] 编码转换失败,尝试回退到 utf-8: {e}") print(f" [警告] 编码转换失败,尝试回退到 utf-8: {e}")
with open(temp_file_path, 'w', encoding='utf-8') as f: with open(temp_file_path, 'w', encoding='utf-8') as f:
f.write('\n'.join(content_lines)) f.write('\n'.join(content_lines))
return temp_file_path return temp_file_path
def import_vba(self): def import_vba(self):
@@ -140,17 +153,21 @@ class VBAImporter:
print(f"错误: 找不到目标 Excel 文件: {self.target_file}") print(f"错误: 找不到目标 Excel 文件: {self.target_file}")
return False return False
if not self.vba_dir.exists():
print(f"错误: 找不到 VBA 代码目录: {self.vba_dir}")
return False
print(f"正在打开 Excel 文件: {self.target_file.name} ...") print(f"正在打开 Excel 文件: {self.target_file.name} ...")
excel = None excel = None
workbook = None workbook = None
try: try:
excel = win32.Dispatch("Excel.Application") excel = win32.Dispatch("Excel.Application")
excel.Visible = False excel.Visible = False
excel.DisplayAlerts = False excel.DisplayAlerts = False
workbook = excel.Workbooks.Open(str(self.target_file)) workbook = excel.Workbooks.Open(str(self.target_file))
try: try:
vb_project = workbook.VBProject vb_project = workbook.VBProject
except Exception: except Exception:
@@ -158,22 +175,19 @@ class VBAImporter:
return False return False
print("开始导入模块...\n") print("开始导入模块...\n")
modules = self.metadata.get("modules", {}) # 扫描所有模块
modules = self._scan_modules()
temp_files_created = [] temp_files_created = []
for file_key, info in modules.items(): if not modules:
raw_module_name = info["name"] print("警告: 未找到任何 VBA 模块文件")
module_name = self._clean_component_name(raw_module_name) return False
module_type_dir = info["type"] # e.g., "Modules", "ClassModules"
rel_path = info["file"] for module_info in modules:
attributes = info.get("attributes", {}) module_name = module_info["name"]
module_type_dir = module_info["type"]
source_code_path = self.vba_dir / rel_path source_code_path = module_info["file_path"]
if not source_code_path.exists():
print(f" [跳过] 找不到源文件: {rel_path}")
continue
component = None component = None
try: try:
@@ -182,8 +196,8 @@ class VBAImporter:
component = None component = None
# 标准模块和类模块支持删除重建 # 标准模块和类模块支持删除重建
is_reloadable = module_type_dir in ["Modules", "ClassModules"] is_reloadable = module_type_dir in [STANDARD_MODULE_DIR, CLASS_MODULE_DIR]
# --------------------------------------------------------- # ---------------------------------------------------------
# 策略 A: 导入文件模式 (Modules, ClassModules) # 策略 A: 导入文件模式 (Modules, ClassModules)
# --------------------------------------------------------- # ---------------------------------------------------------
@@ -193,33 +207,32 @@ class VBAImporter:
vb_project.VBComponents.Remove(component) vb_project.VBComponents.Remove(component)
except Exception as e: except Exception as e:
print(f" [警告] 无法移除 {module_name}: {e},将尝试仅更新代码") print(f" [警告] 无法移除 {module_name}: {e},将尝试仅更新代码")
is_reloadable = False is_reloadable = False
if is_reloadable: if is_reloadable:
# 生成临时导入文件 # 生成临时导入文件
# 【修改】传入 module_type_dir 以判断是否需要加 Class 头 temp_file = self._reconstruct_file_content(source_code_path, module_name, module_type_dir)
temp_file = self._reconstruct_file_content(source_code_path, attributes, module_name, module_type_dir)
temp_files_created.append(temp_file) temp_files_created.append(temp_file)
try: try:
vb_project.VBComponents.Import(str(temp_file)) vb_project.VBComponents.Import(str(temp_file))
print(f" [导入] {module_name} ({module_type_dir})") print(f" [导入] {module_name} ({module_type_dir})")
except Exception as e: except Exception as e:
print(f" [错误] 导入 {module_name} 失败: {e}") print(f" [错误] 导入 {module_name} 失败: {e}")
# --------------------------------------------------------- # ---------------------------------------------------------
# 策略 B: 字符串注入模式 (Sheet, Workbook, Forms) # 策略 B: 字符串注入模式 (Sheet, Workbook, Forms)
# --------------------------------------------------------- # ---------------------------------------------------------
if not is_reloadable: if not is_reloadable:
if not component: if not component:
if module_type_dir == "Forms": if module_type_dir == FORMS_DIR:
print(f" [警告] 无法恢复 UserForm '{module_name}',跳过。") print(f" [警告] 无法恢复 UserForm '{module_name}',跳过。")
continue continue
elif module_type_dir == "DocumentModules": elif module_type_dir == DOCUMENT_MODULE_DIR:
print(f" [警告] 找不到文档对象 '{module_name}',跳过。") print(f" [警告] 找不到文档对象 '{module_name}',跳过。")
continue continue
try: try:
component = vb_project.VBComponents.Add(1) component = vb_project.VBComponents.Add(1)
component.Name = module_name component.Name = module_name
except: except:
print(f" [错误] 无法创建组件 {module_name}") print(f" [错误] 无法创建组件 {module_name}")
@@ -230,14 +243,14 @@ class VBAImporter:
num_lines = code_module.CountOfLines num_lines = code_module.CountOfLines
if num_lines > 0: if num_lines > 0:
code_module.DeleteLines(1, num_lines) code_module.DeleteLines(1, num_lines)
# 直接读取 UTF-8 字符串到内存 # 直接读取 UTF-8 字符串到内存
with open(source_code_path, 'r', encoding='utf-8') as f: with open(source_code_path, 'r', encoding='utf-8') as f:
new_code = f.read() new_code = f.read()
if new_code.strip(): if new_code.strip():
code_module.AddFromString(new_code) code_module.AddFromString(new_code)
print(f" [更新] {module_name} ({module_type_dir}) - 代码已更新") print(f" [更新] {module_name} ({module_type_dir}) - 代码已更新")
except Exception as e: except Exception as e:
print(f" [错误] 更新代码 {module_name} 失败: {e}") print(f" [错误] 更新代码 {module_name} 失败: {e}")
@@ -267,7 +280,7 @@ class VBAImporter:
import traceback import traceback
traceback.print_exc() traceback.print_exc()
return False return False
finally: finally:
if workbook: if workbook:
try: workbook.Close(SaveChanges=False) try: workbook.Close(SaveChanges=False)
@@ -278,47 +291,52 @@ class VBAImporter:
def main(): def main():
print("=" * 60) print("=" * 60)
print("VBA代码导入工具 (V3.0 最终版)") print("VBA代码导入工具 (V4.0 - 基于配置)")
print("=" * 60) print("=" * 60)
print() print()
script_dir = Path(__file__).parent script_dir = Path(__file__).parent
# 检查是否配置了目标元数据文件 # 检查配置
if TARGET_METADATA_FILE and TARGET_METADATA_FILE.strip(): if not TARGET_XLSM_FILE:
# 使用配置的元数据文件路径 print("错误: 未配置 TARGET_XLSM_FILE")
target_path = Path(TARGET_METADATA_FILE) print("请在 .env 文件中设置目标 Excel 文件路径")
return
# 如果是相对路径,则相对于脚本所在目录 # 确定目标文件路径
if not target_path.is_absolute(): target_path = Path(TARGET_XLSM_FILE)
target_path = script_dir / target_path if not target_path.is_absolute():
target_path = script_dir / target_path
if not target_path.exists(): if not target_path.exists():
print(f"错误: 配置的元数据文件不存在: {target_path}") print(f"错误: 配置的文件不存在: {target_path}")
return return
if not target_path.name == METADATA_FILE: if not target_path.suffix.lower() == '.xlsm':
print(f"警告: 文件名不是 {METADATA_FILE}: {target_path.name}") print(f"警告: 文件扩展名不是 .xlsm: {target_path.name}")
metadata_path = target_path # 确定 VBA 代码目录
print(f"使用配置的元数据文件: {metadata_path.name}") if VBA_OUTPUT_DIR:
print() vba_path = Path(VBA_OUTPUT_DIR)
if not vba_path.is_absolute():
vba_path = script_dir / vba_path
else: else:
# 默认模式:查找脚本所在目录下的 VBA 文件夹 # 使用目标文件同目录下的 VBA 文件夹
vba_dir = script_dir / VBA_DIR_NAME vba_path = target_path.parent / "VBA"
metadata_path = vba_dir / METADATA_FILE
if not metadata_path.exists(): if not vba_path.exists():
print(f"错误: 找不到元数据文件: {metadata_path}") print(f"错误: VBA 代码目录不存在: {vba_path}")
print() print()
print("提示:") print("提示:")
print(" 1. 确保已运行 extract_vba.py 提取 VBA 代码") print(" 1. 确保已运行 extract_vba.py 提取 VBA 代码")
print(" 2. 或在脚本顶部配置 TARGET_METADATA_FILE 指定元数据文件路径") print(" 2. 或在 .env 文件中设置 VBA_OUTPUT_DIR 指定代码目录")
return return
print(f"读取元数据: {metadata_path}") print(f"目标 Excel 文件: {target_path.name}")
print(f"VBA 代码目录: {vba_path}")
print()
# 显示目标文件信息 # 确认操作
print("=" * 60) print("=" * 60)
print("警告: 此操作将覆盖目标 Excel 文件中的 VBA 代码。") print("警告: 此操作将覆盖目标 Excel 文件中的 VBA 代码。")
choice = input("\n确认继续? (y/n): ").lower().strip() choice = input("\n确认继续? (y/n): ").lower().strip()
@@ -327,13 +345,11 @@ def main():
print("操作已取消") print("操作已取消")
return return
importer = VBAImporter(str(metadata_path)) importer = VBAImporter(str(vba_path), str(target_path))
# 显示导入目标信息 # 显示模块数量
print() modules = importer._scan_modules()
print(f"目标 Excel 文件: {importer.target_file.name}") print(f"找到 {len(modules)} 个模块文件")
print(f"VBA 代码目录: {importer.vba_dir}")
print(f"模块数量: {len(importer.metadata.get('modules', {}))}")
print() print()
print("=" * 60) print("=" * 60)
print() print()
@@ -349,4 +365,4 @@ def main():
print("=" * 60) print("=" * 60)
if __name__ == "__main__": if __name__ == "__main__":
main() main()