Files
VBAExtractor/import_vba.py
Misaka_Company 69b92884a1 refactor: migrate configuration to .env file for better environment management
- Replace hardcoded file paths with environment variables in extract_vba.py and import_vba.py
- Add .env file for local configuration (excluded from git via .gitignore)
- Add python-dotenv dependency for environment variable loading
- Maintain fallback to interactive mode when environment variables are not set

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-28 11:20:50 +08:00

352 lines
13 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
VBA代码导入工具 (最终修正版)
1. 修复路径识别问题
2. 修复中文乱码问题 (GB18030)
3. 修复类模块(Class)被错误导入为标准模块的问题 (补全 VERSION 1.0 CLASS 头)
"""
import os
import sys
import json
import shutil
import tempfile
from pathlib import Path
from typing import Dict, Any
# 加载 .env 配置文件
try:
from dotenv import load_dotenv
load_dotenv()
except ImportError:
print("警告: 未安装 python-dotenv 库,将使用默认配置")
print("建议运行: pip install python-dotenv")
try:
import win32com.client as win32
except ImportError:
print("错误: 未安装 pywin32 库")
print("请运行: pip install pywin32")
sys.exit(1)
# ==================== 配置区域 ====================
# 从 .env 文件读取配置,如果未设置则使用 None默认模式
TARGET_METADATA_FILE = os.getenv("TARGET_METADATA_FILE", "").strip() or None
# =================================================
# 常量定义
METADATA_FILE = "vba_metadata.json"
VBA_DIR_NAME = "VBA"
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)
if not candidate_path.is_absolute():
candidate_path = self.project_root / candidate_path
self.target_file = candidate_path
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()
def _clean_component_name(self, name: str) -> str:
"""去除模块名称中的扩展名"""
lower_name = name.lower()
for ext in ['.cls', '.bas', '.frm']:
if lower_name.endswith(ext):
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:
"""
读取纯代码文件,重建完整的导入文件
关键逻辑:
1. ClassModules 需要 'VERSION 1.0 CLASS' 头部,否则会被识别为标准模块。
2. 使用 GB18030 编码写入,防止中文乱码。
"""
# 1. 读取源代码 (UTF-8)
with open(code_path, 'r', encoding='utf-8') as f:
code_body = f.read()
# 创建临时文件
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":
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(code_body)
# 4. 写入临时文件 (GB18030 防止乱码)
try:
with open(temp_file_path, 'w', encoding='gb18030', errors='replace') as f:
f.write('\n'.join(content_lines))
except Exception as e:
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):
"""执行导入过程"""
if not self.target_file.exists():
print(f"错误: 找不到目标 Excel 文件: {self.target_file}")
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:
print("错误: 无法访问 VBA 项目。请确保信任对 VBA 工程对象模型的访问。")
return False
print("开始导入模块...\n")
modules = self.metadata.get("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
component = None
try:
component = vb_project.VBComponents(module_name)
except:
component = None
# 标准模块和类模块支持删除重建
is_reloadable = module_type_dir in ["Modules", "ClassModules"]
# ---------------------------------------------------------
# 策略 A: 导入文件模式 (Modules, ClassModules)
# ---------------------------------------------------------
if is_reloadable:
if component:
try:
vb_project.VBComponents.Remove(component)
except Exception as e:
print(f" [警告] 无法移除 {module_name}: {e},将尝试仅更新代码")
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_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":
print(f" [警告] 无法恢复 UserForm '{module_name}',跳过。")
continue
elif module_type_dir == "DocumentModules":
print(f" [警告] 找不到文档对象 '{module_name}',跳过。")
continue
try:
component = vb_project.VBComponents.Add(1)
component.Name = module_name
except:
print(f" [错误] 无法创建组件 {module_name}")
continue
try:
code_module = component.CodeModule
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}")
# 清理
for p in temp_files_created:
try:
if p.exists(): p.unlink()
except: pass
try:
temp_dir = Path(tempfile.gettempdir()) / "vba_import_temp"
if temp_dir.exists(): shutil.rmtree(temp_dir)
except: pass
print("\n正在编译 VBA 项目...")
try:
workbook.Save()
print("已保存更改。")
except Exception as e:
print(f"保存文件时出错: {e}")
print(f"\n导入完成!目标文件: {self.target_file.name}")
return True
except Exception as e:
print(f"\n发生未处理的错误: {e}")
import traceback
traceback.print_exc()
return False
finally:
if workbook:
try: workbook.Close(SaveChanges=False)
except: pass
if excel:
try: excel.Quit()
except: pass
def main():
print("=" * 60)
print("VBA代码导入工具 (V3.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_path.is_absolute():
target_path = script_dir / target_path
if not target_path.exists():
print(f"错误: 配置的元数据文件不存在: {target_path}")
return
if not target_path.name == METADATA_FILE:
print(f"警告: 文件名不是 {METADATA_FILE}: {target_path.name}")
metadata_path = target_path
print(f"使用配置的元数据文件: {metadata_path.name}")
print()
else:
# 默认模式:查找脚本所在目录下的 VBA 文件夹
vba_dir = script_dir / VBA_DIR_NAME
metadata_path = vba_dir / METADATA_FILE
if not metadata_path.exists():
print(f"错误: 找不到元数据文件: {metadata_path}")
print()
print("提示:")
print(" 1. 确保已运行 extract_vba.py 提取 VBA 代码")
print(" 2. 或在脚本顶部配置 TARGET_METADATA_FILE 指定元数据文件路径")
return
print(f"读取元数据: {metadata_path}")
# 显示目标文件信息
print("=" * 60)
print("警告: 此操作将覆盖目标 Excel 文件中的 VBA 代码。")
choice = input("\n确认继续? (y/n): ").lower().strip()
if choice != 'y':
print("操作已取消")
return
importer = VBAImporter(str(metadata_path))
# 显示导入目标信息
print()
print(f"目标 Excel 文件: {importer.target_file.name}")
print(f"VBA 代码目录: {importer.vba_dir}")
print(f"模块数量: {len(importer.metadata.get('modules', {}))}")
print()
print("=" * 60)
print()
success = importer.import_vba()
print()
print("=" * 60)
if success:
print("导入成功完成!")
else:
print("导入失败")
print("=" * 60)
if __name__ == "__main__":
main()