630 lines
20 KiB
Markdown
630 lines
20 KiB
Markdown
# 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"
|
||
```
|