diff --git a/extract_vba.py b/extract_vba.py index c476c13..1287902 100644 --- a/extract_vba.py +++ b/extract_vba.py @@ -20,7 +20,7 @@ except ImportError: # ==================== 配置区域 ==================== # 从 .env 文件读取配置,如果未设置则使用 None(交互模式) -TARGET_XLSM_FILE = os.getenv("TARGET_XLSM_FILE", "").strip() or None +TARGET_FILE = os.getenv("TARGET_FILE", "").strip() or None # VBA代码输出目录(如果未设置,则使用源文件同目录下的VBA文件夹) VBA_OUTPUT_DIR = os.getenv("VBA_OUTPUT_DIR", "").strip() or None # ================================================= @@ -31,19 +31,36 @@ 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 VBAExtractor: """VBA代码提取器""" - def __init__(self, xlsm_path: str, output_dir: str = None): + def __init__(self, source_path: str, output_dir: str = None): """ 初始化VBA提取器 Args: - xlsm_path: xlsm文件路径 + source_path: 源文件路径(支持.xlsm/.accdb等) output_dir: 输出目录(如果未指定,则使用VBA_OUTPUT_DIR配置或源文件同目录) """ - self.xlsm_path = Path(xlsm_path) + self.source_path = Path(source_path) # 确定输出目录的优先级: # 1. 参数指定的 output_dir @@ -55,7 +72,7 @@ class VBAExtractor: self.output_dir = Path(VBA_OUTPUT_DIR) else: # 使用目标文件同目录下的VBA文件夹 - self.output_dir = self.xlsm_path.parent / "VBA" + self.output_dir = self.source_path.parent / "VBA" # 创建输出目录结构 self.modules_dir = self.output_dir / STANDARD_MODULE_DIR @@ -126,10 +143,10 @@ class VBAExtractor: print("请运行: pip install oletools") return False - print(f"正在解析文件: {self.xlsm_path.name}") + print(f"正在解析文件: {self.source_path.name}") try: - vba_parser = VBA_Parser(str(self.xlsm_path)) + vba_parser = VBA_Parser(str(self.source_path)) if vba_parser.detect_vba_macros(): print("发现VBA代码,开始提取...\n") @@ -168,14 +185,14 @@ class VBAExtractor: print("请运行: pip install pywin32") return False - print(f"正在使用COM接口解析: {self.xlsm_path.name}") + print(f"正在使用COM接口解析: {self.source_path.name}") try: excel = win32.Dispatch("Excel.Application") excel.Visible = False excel.DisplayAlerts = False - workbook = excel.Workbooks.Open(str(self.xlsm_path.absolute())) + workbook = excel.Workbooks.Open(str(self.source_path.absolute())) # 获取VBA项目 if not workbook.VBProject: @@ -244,6 +261,84 @@ class VBAExtractor: pass return False + def extract_vba_modules_access_com(self): + """ + 使用COM接口从Access数据库提取VBA代码 + + 需要: Microsoft Access + pywin32 + """ + try: + import win32com.client as win32 + except ImportError: + print("错误: 未安装pywin32库") + print("请运行: pip install pywin32") + return False + + print(f"正在使用COM接口解析: {self.source_path.name}") + + access = None + try: + access = win32.Dispatch("Access.Application") + access.Visible = False + access.OpenCurrentDatabase(str(self.source_path.absolute())) + + # Access 通过 VBE 获取 VBProject + try: + vb_project = access.VBE.VBProjects(1) + except Exception: + print("错误: 无法访问VBA项目") + print("请确保: 1) Access信任中心设置'信任对VBA工程对象模型的访问'") + print(" 2) 数据库中包含VBA代码") + return False + + print("开始提取VBA组件...\n") + + # 遍历所有VBA组件 + for component in vb_project.VBComponents: + module_name = component.Name + module_type = component.Type + + # 获取代码 + code_module = component.CodeModule + line_count = code_module.CountOfLines + + if line_count > 0: + vba_code = code_module.Lines(1, line_count) + else: + vba_code = "" + + # Access 只有标准模块(1)和类模块(2) + type_name = { + 1: STANDARD_MODULE_DIR, + 2: CLASS_MODULE_DIR, + }.get(module_type, STANDARD_MODULE_DIR) + + self._process_module(module_name, vba_code, type_name) + + print(f"\n提取完成!") + print(f"- 标准模块: {self.modules_dir}") + print(f"- 类模块: {self.class_modules_dir}") + return True + + except Exception as e: + print(f"使用COM提取Access VBA代码时出错: {e}") + print("\n提示:") + print("1. 确保已安装Microsoft Access") + print("2. 打开Access -> 文件 -> 选项 -> 信任中心 -> 信任中心设置") + print("3. 勾选'信任对VBA工程对象模型的访问'") + return False + + finally: + if access: + try: + access.CloseCurrentDatabase() + except: + pass + try: + access.Quit() + except: + pass + def _determine_module_type(self, module_name: str, stream_path: str) -> str: """ 根据模块名称和流路径确定模块类型 @@ -347,10 +442,10 @@ def main(): print() # 检查是否配置了目标文件 - if TARGET_XLSM_FILE and TARGET_XLSM_FILE.strip(): + if TARGET_FILE and TARGET_FILE.strip(): # 使用配置的文件路径 script_dir = Path(__file__).parent - target_path = Path(TARGET_XLSM_FILE) + target_path = Path(TARGET_FILE) # 如果是相对路径,则相对于脚本所在目录 if not target_path.is_absolute(): @@ -360,69 +455,75 @@ def main(): print(f"错误: 配置的文件不存在: {target_path}") return - if not target_path.suffix.lower() == '.xlsm': - print(f"警告: 文件扩展名不是.xlsm: {target_path.name}") - - xlsm_file = target_path - print(f"使用配置文件: {xlsm_file.name}") + file_type = get_file_type(target_path) + source_file = target_path + print(f"使用配置文件: {source_file.name} ({file_type})") print() else: - # 交互模式:查找xlsm文件 + # 交互模式:查找支持的文件 excel_dir = Path("Excel") if not excel_dir.exists(): print("错误: 未找到Excel文件夹") return - xlsm_files = list(excel_dir.glob("*.xlsm")) - if not xlsm_files: - print("错误: Excel文件夹中没有xlsm文件") + # 同时扫描 Excel 和 Access 文件 + all_files = list(excel_dir.glob("*.xlsm")) + list(excel_dir.glob("*.accdb")) + list(excel_dir.glob("*.mdb")) + if not all_files: + print("错误: Excel文件夹中没有.xlsm或.accdb文件") return # 如果有多个文件,让用户选择 - if len(xlsm_files) > 1: - print("发现多个xlsm文件:") - for i, f in enumerate(xlsm_files, 1): - print(f" {i}. {f.name}") + if len(all_files) > 1: + print("发现多个文件:") + for i, f in enumerate(all_files, 1): + ft = get_file_type(f) + print(f" {i}. {f.name} ({ft})") print() choice = input("请选择文件编号 (直接回车选择第1个): ").strip() if not choice: - xlsm_file = xlsm_files[0] + source_file = all_files[0] else: try: idx = int(choice) - 1 - xlsm_file = xlsm_files[idx] + source_file = all_files[idx] except: print("无效选择,使用第一个文件") - xlsm_file = xlsm_files[0] + source_file = all_files[0] else: - xlsm_file = xlsm_files[0] + source_file = all_files[0] + file_type = get_file_type(source_file) print() - print(f"选择文件: {xlsm_file.name}") + print(f"选择文件: {source_file.name} ({file_type})") print() # 创建提取器 - # 输出目录优先级: 1. .env中的VBA_OUTPUT_DIR配置 2. 源文件同目录下的VBA文件夹 - extractor = VBAExtractor(str(xlsm_file)) + extractor = VBAExtractor(str(source_file)) # 显示输出目录信息 print(f"输出目录: {extractor.output_dir}") print() - # 选择提取方法 - print("请选择提取方法:") - print(" 1. COM接口 (推荐 - 需要安装Excel)") - print(" 2. olevba库 (不需要Excel)") - print() - - method = input("请选择 (直接回车使用方法1): ").strip() - - if method == "2": - print("\n使用olevba库提取...") - success = extractor.extract_vba_modules_olevba() + # 根据文件类型选择提取方法 + if file_type == 'access': + # Access 只支持COM方法 + print("Access文件仅支持COM接口提取...") + success = extractor.extract_vba_modules_access_com() else: - print("\n使用COM接口提取...") - success = extractor.extract_vba_modules_com() + # Excel 支持COM和olevba + print("请选择提取方法:") + print(" 1. COM接口 (推荐 - 需要安装Excel)") + print(" 2. olevba库 (不需要Excel)") + print() + + method = input("请选择 (直接回车使用方法1): ").strip() + + if method == "2": + print("\n使用olevba库提取...") + success = extractor.extract_vba_modules_olevba() + else: + print("\n使用COM接口提取...") + success = extractor.extract_vba_modules_com() if success: print("\n" + "=" * 60)