From 8642d40032a7eb59baeee45422f7ae5f04e472fe Mon Sep 17 00:00:00 2001 From: Misaka_Company Date: Mon, 11 May 2026 09:57:26 +0800 Subject: [PATCH] docs: add Access support design and implementation plan Co-Authored-By: Claude Opus 4.6 --- .../plans/2026-05-11-access-support-design.md | 36 + docs/plans/2026-05-11-access-support-plan.md | 629 ++++++++++++++++++ 2 files changed, 665 insertions(+) create mode 100644 docs/plans/2026-05-11-access-support-design.md create mode 100644 docs/plans/2026-05-11-access-support-plan.md diff --git a/docs/plans/2026-05-11-access-support-design.md b/docs/plans/2026-05-11-access-support-design.md new file mode 100644 index 0000000..7d29160 --- /dev/null +++ b/docs/plans/2026-05-11-access-support-design.md @@ -0,0 +1,36 @@ +# Access (.accdb) VBA Code Export/Import Support Design + +## Summary + +Extend the existing `extract_vba.py` and `import_vba.py` to support Access `.accdb` files alongside Excel `.xlsm` files. The file type is auto-detected by extension via a unified `TARGET_FILE` env variable. + +## Key COM Differences + +| Dimension | Excel (.xlsm) | Access (.accdb) | +|-----------|---------------|-----------------| +| ProgID | `Excel.Application` | `Access.Application` | +| Open | `Workbooks.Open(path)` | `OpenCurrentDatabase(path)` | +| VBA Project | `workbook.VBProject` | `CurrentDb().VBE.VBProjects(1)` | +| Close | `workbook.Close()` + `Quit()` | `CloseCurrentDatabase()` + `Quit()` | +| Module types | Standard/Class/Document/Forms | Standard/Class only | + +## Changes + +### .env +- Replace `TARGET_XLSM_FILE` with `TARGET_FILE` (supports `.xlsm` and `.accdb`) + +### extract_vba.py +- Replace `TARGET_XLSM_FILE` with `TARGET_FILE` +- Add `extract_vba_modules_access_com()` method to `VBAExtractor` +- Update `main()` to detect file type by extension and auto-select COM method +- Skip olevba option for `.accdb` files (unsupported) + +### import_vba.py +- Replace `TARGET_XLSM_FILE` with `TARGET_FILE` +- Add `import_vba_access()` method to `VBAImporter` +- Update `main()` to detect file type by extension and route accordingly + +## Out of Scope +- Access Forms/Reports VBA code (user doesn't need it) +- olevba for Access files (unsupported) +- Directory structure changes (Modules/ClassModules sufficient) diff --git a/docs/plans/2026-05-11-access-support-plan.md b/docs/plans/2026-05-11-access-support-plan.md new file mode 100644 index 0000000..5926db8 --- /dev/null +++ b/docs/plans/2026-05-11-access-support-plan.md @@ -0,0 +1,629 @@ +# Access (.accdb) VBA Support Implementation Plan + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** Add Access `.accdb` VBA extraction and import support alongside existing Excel `.xlsm` support, using a unified `TARGET_FILE` config variable. + +**Architecture:** Extend the existing `VBAExtractor` and `VBAImporter` classes with Access COM methods. File type auto-detected by extension (`.xlsm` → Excel, `.accdb` → Access). Reuse existing module processing pipeline (`parse_attributes`, `_process_module`, `_scan_modules`, `_reconstruct_file_content`). + +**Tech Stack:** pywin32 (COM automation), Access.Application ProgID + +--- + +### Task 1: Update `.env` config variable + +**Files:** +- Modify: `.env:8` +- Modify: `.env:5-8` (comments) + +**Step 1: Rename variable and update comments** + +Change `.env` from: +``` +TARGET_XLSM_FILE=C:\Users\Administrator\Desktop\生产周期核对\常规产品生产周期.xlsm +``` +To: +``` +TARGET_FILE=C:\Users\Administrator\Desktop\生产周期核对\常规产品生产周期.xlsm +``` + +Update the comment block (lines 5-7) to reflect the new unified variable that supports both `.xlsm` and `.accdb`. + +**Step 2: Commit** + +```bash +git add .env +git commit -m "refactor: rename TARGET_XLSM_FILE to TARGET_FILE for unified file type support" +``` + +--- + +### Task 2: Update `extract_vba.py` — config, constructor, and file type helper + +**Files:** +- Modify: `extract_vba.py:23` (config var) +- Modify: `extract_vba.py:38-69` (constructor — rename `self.xlsm_path` to `self.source_path`) +- Modify: `extract_vba.py:129,171` (references to `self.xlsm_path`) +- Add: file type helper function after constants block + +**Step 1: Replace `TARGET_XLSM_FILE` with `TARGET_FILE`** + +Line 23: `TARGET_XLSM_FILE` → `TARGET_FILE` +Lines 21-26 — update comment block accordingly. + +**Step 2: Add file type helper function** + +Add after the constants block (after line 32), before the class: + +```python +# 支持的文件类型 +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' +``` + +**Step 3: Rename `self.xlsm_path` to `self.source_path` throughout the class** + +In `__init__`: `self.xlsm_path` → `self.source_path` (line 46, 58) +In `extract_vba_modules_olevba`: `self.xlsm_path.name` → `self.source_path.name` and `str(self.xlsm_path)` → `str(self.source_path)` (lines 129, 132) +In `extract_vba_modules_com`: `self.xlsm_path.name` → `self.source_path.name` and `str(self.xlsm_path.absolute())` → `str(self.source_path.absolute())` (lines 171, 178) +Also update the docstring in `__init__` (line 43-44): `xlsm_path: xlsm文件路径` → `source_path: 源文件路径(支持.xlsm和.accdb)` +Rename the parameter from `xlsm_path` to `source_path`. + +**Step 4: Commit** + +```bash +git add extract_vba.py +git commit -m "refactor: rename xlsm_path to source_path and add file type detection helper" +``` + +--- + +### Task 3: Add `extract_vba_modules_access_com()` to `VBAExtractor` + +**Files:** +- Modify: `extract_vba.py` — add method after `extract_vba_modules_com()` (after line 245) + +**Step 1: Add the Access COM extraction method** + +Insert after line 245 (end of `extract_vba_modules_com`): + +```python + 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),按类型分类 + # 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 +``` + +**Step 2: Commit** + +```bash +git add extract_vba.py +git commit -m "feat: add Access COM extraction method to VBAExtractor" +``` + +--- + +### Task 4: Update `extract_vba.py` `main()` for unified file dispatch + +**Files:** +- Modify: `extract_vba.py:342-438` (entire `main()` function) + +**Step 1: Rewrite `main()` to support both file types** + +Replace the entire `main()` function. Key changes: +- `TARGET_XLSM_FILE` → `TARGET_FILE` +- File validation accepts both `.xlsm` and `.accdb` +- Interactive mode scans for both extensions +- Auto-selects COM method for `.accdb` (skips olevba prompt) +- Instantiates `VBAExtractor` with `source_path` parameter + +```python +def main(): + """主函数""" + print("=" * 60) + print("VBA代码提取工具") + print("=" * 60) + print() + + # 检查是否配置了目标文件 + if TARGET_FILE and TARGET_FILE.strip(): + # 使用配置的文件路径 + script_dir = Path(__file__).parent + 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) + source_file = target_path + print(f"使用配置文件: {source_file.name} ({file_type})") + print() + else: + # 交互模式:查找支持的文件 + excel_dir = Path("Excel") + if not excel_dir.exists(): + print("错误: 未找到Excel文件夹") + return + + # 同时扫描 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(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: + source_file = all_files[0] + else: + try: + idx = int(choice) - 1 + source_file = all_files[idx] + except: + print("无效选择,使用第一个文件") + source_file = all_files[0] + else: + source_file = all_files[0] + + file_type = get_file_type(source_file) + print() + print(f"选择文件: {source_file.name} ({file_type})") + print() + + # 创建提取器 + extractor = VBAExtractor(str(source_file)) + + # 显示输出目录信息 + print(f"输出目录: {extractor.output_dir}") + print() + + # 根据文件类型选择提取方法 + if file_type == 'access': + # Access 只支持COM方法 + print("Access文件仅支持COM接口提取...") + success = extractor.extract_vba_modules_access_com() + else: + # 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) + print("提取成功完成!") + print("=" * 60) + else: + print("\n" + "=" * 60) + print("提取失败") + print("=" * 60) +``` + +**Step 2: Commit** + +```bash +git add extract_vba.py +git commit -m "feat: update extract_vba.py main() to support both Excel and Access files" +``` + +--- + +### Task 5: Update `import_vba.py` — config and add Access import method + +**Files:** +- Modify: `import_vba.py:31` (config var) +- Modify: `import_vba.py:42-290` (class — add `import_vba_access()` method) + +**Step 1: Replace `TARGET_XLSM_FILE` with `TARGET_FILE`** + +Line 31: `TARGET_XLSM_FILE` → `TARGET_FILE` +Lines 28-34 — update comment block accordingly. + +**Step 2: Add `import_vba_access()` method to `VBAImporter`** + +Insert after `import_vba()` method (after line 290, before the `finally` block closes and `main()` starts). Actually, insert as a new method on the class after `import_vba()`: + +```python + 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: + # Access 使用 RunCommand 保存 + 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 +``` + +**Step 3: Commit** + +```bash +git add import_vba.py +git commit -m "feat: add Access COM import method to VBAImporter" +``` + +--- + +### Task 6: Update `import_vba.py` `main()` for unified file dispatch + +**Files:** +- Modify: `import_vba.py:292-368` (entire `main()` function) + +**Step 1: Rewrite `main()` to support both file types** + +Replace the entire `main()` function. Key changes: +- `TARGET_XLSM_FILE` → `TARGET_FILE` +- File validation accepts both `.xlsm` and `.accdb` +- Auto-selects import method based on file type +- Display appropriate warning message + +```python +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) +``` + +Note: `main()` in `import_vba.py` also needs the `get_file_type` helper. Add the same helper function and constants to `import_vba.py` (or extract to a shared module — but per the design, we keep it simple with duplication since the helper is tiny). + +**Step 2: Add the file type helper to `import_vba.py`** + +Add after the constants block (after line 40), same as in `extract_vba.py`: + +```python +# 支持的文件类型 +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' +``` + +**Step 3: Commit** + +```bash +git add import_vba.py +git commit -m "feat: update import_vba.py main() to support both Excel and Access files" +``` + +--- + +### Task 7: Update `.gitignore` and `CLAUDE.md` docs + +**Files:** +- Modify: `.gitignore:17` (add `Access/` directory) +- Modify: `CLAUDE.md` (update docs to reflect Access support) + +**Step 1: Add Access directory to `.gitignore`** + +After line 17 (`Excel/`), add: +``` +Access/ +``` + +**Step 2: Update `CLAUDE.md`** + +Update the project overview to mention Access support: +- Project description: mention `.accdb` alongside `.xlsm` +- Directory structure: add `Access/` source directory +- Common commands: update descriptions to mention Access files +- Platform requirements: add "Microsoft Access" as optional + +**Step 3: Commit** + +```bash +git add .gitignore CLAUDE.md +git commit -m "docs: update documentation for Access file support" +```