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