""" VBA代码导入工具 使用 .env 配置直接导入 VBA 代码,无需元数据文件 """ import os import sys import shutil import tempfile from pathlib import Path from typing import Dict, List, Tuple # 加载 .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 文件读取配置 # 目标文件路径(支持 .xlsm / .accdb / .mdb) TARGET_FILE = os.getenv("TARGET_FILE", "").strip() or None # VBA代码输出目录(如果未设置,则使用源文件同目录下的VBA文件夹) VBA_OUTPUT_DIR = os.getenv("VBA_OUTPUT_DIR", "").strip() or None # ================================================= # 常量定义 STANDARD_MODULE_DIR = "Modules" CLASS_MODULE_DIR = "ClassModules" DOCUMENT_MODULE_DIR = "DocumentModules" FORMS_DIR = "Forms" # 支持的文件类型 ACCESS_EXTENSIONS = {'.accdb', '.mdb'} EXCEL_EXTENSIONS = {'.xlsm', '.xls', '.xlsb'} def get_file_type(file_path: Path) -> str: """ 根据文件扩展名判断文件类型 Returns: 'access' 或 'excel' """ ext = file_path.suffix.lower() if ext in ACCESS_EXTENSIONS: return 'access' return 'excel' class VBAImporter: """VBA代码导入器""" def __init__(self, vba_dir: str, target_file: str): """ 初始化VBA导入器 Args: vba_dir: VBA代码目录(包含 Modules, ClassModules 等子目录) target_file: 目标 Excel 文件路径 """ self.vba_dir = Path(vba_dir).resolve() self.target_file = Path(target_file).resolve() def _scan_modules(self) -> List[Dict]: """ 扫描 VBA 目录,收集所有模块信息 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: """去除模块名称中的扩展名""" 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, 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 == CLASS_MODULE_DIR: content_lines.append("VERSION 1.0 CLASS") content_lines.append("BEGIN") content_lines.append(" MultiUse = -1 'True") content_lines.append("END") # 2. 重建 Attribute VB_Name content_lines.append(f'Attribute VB_Name = "{module_name}"') # 注意:由于我们移除了元数据,不再有其他属性信息 # 如果需要其他属性,需要从源文件中解析或在代码中显式声明 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 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: print("错误: 无法访问 VBA 项目。请确保信任对 VBA 工程对象模型的访问。") return False print("开始导入模块...\n") # 扫描所有模块 modules = self._scan_modules() temp_files_created = [] 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: component = vb_project.VBComponents(module_name) except: component = None # 标准模块和类模块支持删除重建 is_reloadable = module_type_dir in [STANDARD_MODULE_DIR, CLASS_MODULE_DIR] # --------------------------------------------------------- # 策略 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: # 生成临时导入文件 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_DIR: print(f" [警告] 无法恢复 UserForm '{module_name}',跳过。") continue elif module_type_dir == DOCUMENT_MODULE_DIR: 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 import_vba_access(self): """使用Access COM导入VBA代码""" if not self.target_file.exists(): print(f"错误: 找不到目标 Access 文件: {self.target_file}") return False if not self.vba_dir.exists(): print(f"错误: 找不到 VBA 代码目录: {self.vba_dir}") return False print(f"正在打开 Access 数据库: {self.target_file.name} ...") access = None try: access = win32.Dispatch("Access.Application") access.Visible = False access.OpenCurrentDatabase(str(self.target_file)) try: vb_project = access.VBE.VBProjects(1) except Exception: print("错误: 无法访问 VBA 项目。请确保信任对 VBA 工程对象模型的访问。") return False print("开始导入模块...\n") # 扫描所有模块(Access 只导入标准模块和类模块) scan_dirs = [ (STANDARD_MODULE_DIR, ".bas"), (CLASS_MODULE_DIR, ".cls"), ] modules = [] 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 }) if not modules: print("警告: 未找到任何 VBA 模块文件") return False temp_files_created = [] 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: component = vb_project.VBComponents(module_name) except: component = None # 移除已存在的组件 if component: try: vb_project.VBComponents.Remove(component) except Exception as e: print(f" [警告] 无法移除 {module_name}: {e},将尝试更新代码") # 回退到字符串注入 try: code_module = component.CodeModule num_lines = code_module.CountOfLines if num_lines > 0: code_module.DeleteLines(1, num_lines) 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 e2: print(f" [错误] 更新代码 {module_name} 失败: {e2}") continue # 生成临时导入文件并导入 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}") # 清理临时文件 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正在保存...") try: import time time.sleep(1) # 等待 VBE 完成 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 access: try: access.CloseCurrentDatabase() except: pass try: access.Quit() except: pass def main(): print("=" * 60) print("VBA代码导入工具 (V5.0 - 支持Excel和Access)") print("=" * 60) print() script_dir = Path(__file__).parent # 检查配置 if not TARGET_FILE: print("错误: 未配置 TARGET_FILE") print("请在 .env 文件中设置目标文件路径(支持.xlsm和.accdb)") return # 确定目标文件路径 target_path = Path(TARGET_FILE) if not target_path.is_absolute(): target_path = script_dir / target_path if not target_path.exists(): print(f"错误: 配置的文件不存在: {target_path}") return file_type = get_file_type(target_path) # 确定 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_path = target_path.parent / "VBA" 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 type_label = "Access" if file_type == "access" else "Excel" print(f"目标文件: {target_path.name} ({type_label})") print(f"VBA 代码目录: {vba_path}") print() # 确认操作 print("=" * 60) print(f"警告: 此操作将覆盖目标{type_label}文件中的 VBA 代码。") choice = input("\n确认继续? (y/n): ").lower().strip() if choice != 'y': print("操作已取消") return importer = VBAImporter(str(vba_path), str(target_path)) # 显示模块数量 modules = importer._scan_modules() print(f"找到 {len(modules)} 个模块文件") print() print("=" * 60) print() # 根据文件类型选择导入方法 if file_type == 'access': success = importer.import_vba_access() else: success = importer.import_vba() print() print("=" * 60) if success: print("导入成功完成!") else: print("导入失败") print("=" * 60) if __name__ == "__main__": main()