Compare commits

...

9 Commits

Author SHA1 Message Date
Misaka_Company
585a9ed7f3 refactor: use VBA-Excel/VBA-Access as default output dirs by file type
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-11 10:01:37 +08:00
Misaka_Company
8642d40032 docs: add Access support design and implementation plan
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-11 09:57:26 +08:00
Misaka_Company
d156fdb2d8 fix: address code review issues for Access support
- Fix import_vba_access() to actually save database via DoCmd.Save()
- Update stale module docstring in extract_vba.py
- Validate file extensions in get_file_type() instead of silently
  defaulting to excel
- Scan both Excel/ and Access/ directories in interactive mode

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-11 09:44:55 +08:00
Misaka_Company
ee1120c247 docs: update documentation for Access file support
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-11 09:41:18 +08:00
Misaka_Company
df81562dd9 feat: add Access (.accdb) extraction support to extract_vba.py
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-11 09:37:32 +08:00
Misaka_Company
788a8fd9e7 feat: add Access (.accdb) import support to import_vba.py
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-11 09:34:57 +08:00
Misaka_Company
ab01835b06 refactor: export form modules as .frm files to align with VBA conventions
Change form module export from .cls to .frm extension to match standard VBA file conventions and align with import_vba.py expectations. This eliminates ambiguity between class modules and form modules.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 15:47:57 +08:00
Misaka_Company
7b27e50573 refactor: remove metadata dependency in import_vba.py for simplified VBA import workflow
- Remove vba_metadata.json dependency
- Add direct VBA directory scanning with _scan_modules() method
- Read target file and output directory from .env configuration
- Simplify import process: specify file and directory instead of metadata file
- Update version to 4.0 (configuration-based approach)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-03 12:56:34 +08:00
Misaka_Company
59f0ef2125 refactor: remove metadata generation and add configurable output directory
- Remove vba_metadata.json generation logic to simplify extraction process
- Add VBA_OUTPUT_DIR configuration to allow custom output path
- Update output directory priority: parameter > env var > source file directory
- Clean up unused imports and remove _save_metadata method

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-03 12:47:54 +08:00
6 changed files with 1165 additions and 228 deletions

1
.gitignore vendored
View File

@@ -15,4 +15,5 @@ tmpclaude-*
*.png
data/
Excel/
Access/
VBA/

View File

@@ -4,7 +4,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
## Project Overview
**Auto_BOM** is a VBA code extraction and management toolkit for Excel-based Bill of Materials (BOM) processing. The project provides Python tools to extract VBA code from `.xlsm` files, manage it externally, and import it back into Excel.
**Auto_BOM** is a VBA code extraction and management toolkit for Excel-based Bill of Materials (BOM) processing. The project provides Python tools to extract VBA code from `.xlsm` (Excel) and `.accdb` (Access) files, manage it externally, and import it back.
The VBA code implements a hierarchical BOM management system with:
- **clsBOMManager**: Main class managing BOM data structure and category relationships
@@ -16,14 +16,15 @@ The VBA code implements a hierarchical BOM management system with:
```
Auto_BOM/
├── Excel/ # Source Excel files (.xlsm) - gitignored
├── Access/ # Source Access files (.accdb) - gitignored
├── VBA/ # Extracted VBA code - gitignored
│ ├── Modules/ # Standard modules (.bas)
│ ├── ClassModules/ # Class modules (.cls)
│ ├── DocumentModules/# Sheet/workbook modules (.cls)
│ ├── Forms/ # User forms
│ └── vba_metadata.json # Module metadata for import
├── extract_vba.py # Extract VBA from Excel files
├── import_vba.py # Import VBA back to Excel files
├── extract_vba.py # Extract VBA from Excel/Access files
├── import_vba.py # Import VBA back to Excel/Access files
├── main.py # Empty placeholder
└── requirements.txt # Python dependencies
```
@@ -40,8 +41,8 @@ pip install -r requirements.txt
```
**Dependencies:**
- `pywin32>=306` - Windows COM interface for Excel automation (Windows only)
- `oletools>=0.60` - Alternative VBA extraction without Excel dependency
- `pywin32>=306` - Windows COM interface for Excel/Access automation (Windows only)
- `oletools>=0.60` - Alternative VBA extraction without Excel dependency (Excel only)
## Common Commands
@@ -51,20 +52,24 @@ pip install -r requirements.txt
python extract_vba.py
```
Two extraction methods available:
**Excel (.xlsm)** - Two extraction methods available:
1. **COM Interface** (recommended) - Requires Microsoft Excel, more reliable
2. **olevba Library** - No Excel required, uses oletools
For COM method, ensure Excel trusts VBA access:
**Access (.accdb)** - Only COM Interface supported (requires Microsoft Access)
For COM method, ensure the application trusts VBA access:
- Excel > Options > Trust Center > Trust Center Settings
- Check "Trust access to the VBA project object model"
### Import VBA Code
```bash
# Import from VBA/ directory back to Excel
python import_vba.py VBA/vba_metadata.json
# Import from VBA/ directory back to Excel or Access
python import_vba.py
```
Set `TARGET_FILE` in `.env` to the target `.xlsm` or `.accdb` file path.
## VBA Code Architecture
### BOM Data Model
@@ -98,12 +103,12 @@ The VBA system implements a hierarchical category-based material management:
- Cleans `Attribute` statements from exported code for readability
- Automatically categorizes modules by type (Standard/Class/Document/Form)
- Generates `vba_metadata.json` tracking source file, module names, types, and file mappings
- Module type detection based on naming conventions (mod_=Standard, cls=Class, sheet=Document)
- Auto-detects file type by extension (.xlsm → Excel, .accdb → Access)
### VBA Import (import_vba.py)
- Uses Windows COM to interact with Excel
- Uses Windows COM to interact with Excel or Access
- **Critical fix for ClassModules**: Reconstructs `VERSION 1.0 CLASS` header before import
- **Encoding handling**: Uses GB18030 for temp files to prevent Chinese character corruption
- Path recognition logic handles relative/absolute paths in metadata
@@ -125,15 +130,16 @@ The `.vscode/settings.json` associates `.cls` files with Visual Basic syntax hig
## Platform Requirements
- **Windows required** for import functionality (COM interface)
- **Microsoft Excel** required for COM-based extraction/import
- Cross-platform extraction possible with oletools (no Excel needed)
- **Microsoft Excel** required for Excel COM-based extraction/import
- **Microsoft Access** required for Access COM-based extraction/import
- Cross-platform extraction possible with oletools (Excel only, no Office needed)
## Git Workflow
The `.gitignore` excludes:
- Virtual environment (`.venv/`)
- Build artifacts (`build/`, `dist/`)
- Project data (`Excel/`, `VBA/`)
- Project data (`Excel/`, `Access/`, `VBA/`)
- Claude temporary files (`.claude/`, `tmpclaude-*`)
Only commit code changes, not extracted VBA or Excel files.

View File

@@ -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)

View File

@@ -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"
```

View File

@@ -1,15 +1,14 @@
"""
VBA代码提取工具
xlsm文件中提取模块和类模块代码分类保存到VBA文件夹
自动清理Attribute信息并生成元数据JSON文件
Excel(.xlsm)或Access(.accdb)文件中提取VBA代码分类保存到VBA文件夹
自动清理Attribute信息
"""
import os
import sys
import json
import re
from pathlib import Path
from typing import Dict, List, Tuple, Optional
from typing import Tuple, Dict
# 加载 .env 配置文件
try:
@@ -21,7 +20,9 @@ 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
# =================================================
# VBA项目相关常量
@@ -29,32 +30,56 @@ STANDARD_MODULE_DIR = "Modules"
CLASS_MODULE_DIR = "ClassModules"
DOCUMENT_MODULE_DIR = "DocumentModules"
FORMS_DIR = "Forms"
METADATA_FILE = "vba_metadata.json"
# 支持的文件类型
ACCESS_EXTENSIONS = {'.accdb', '.mdb'}
EXCEL_EXTENSIONS = {'.xlsm', '.xls', '.xlsb'}
def get_file_type(file_path: Path) -> str:
"""
根据文件扩展名判断文件类型
Returns:
'access''excel'
Raises:
ValueError: 不支持的文件扩展名
"""
ext = file_path.suffix.lower()
if ext in ACCESS_EXTENSIONS:
return 'access'
if ext in EXCEL_EXTENSIONS:
return 'excel'
raise ValueError(f"不支持的文件类型: {ext}(支持: .xlsm, .xls, .xlsb, .accdb, .mdb")
class VBAExtractor:
"""VBA代码提取器"""
def __init__(self, xlsm_path: str, output_dir: str = None, use_same_dir: bool = True):
def __init__(self, source_path: str, output_dir: str = None):
"""
初始化VBA提取器
Args:
xlsm_path: xlsm文件路径
output_dir: 输出目录(当use_same_dir=False时有效
use_same_dir: 是否使用目标文件同目录下的VBA文件夹默认True
source_path: 源文件路径(支持.xlsm/.accdb等
output_dir: 输出目录(如果未指定则使用VBA_OUTPUT_DIR配置或源文件同目录
"""
self.xlsm_path = Path(xlsm_path)
self.source_path = Path(source_path)
if use_same_dir:
# 使用目标文件同目录下的VBA文件夹
self.output_dir = self.xlsm_path.parent / "VBA"
elif output_dir is None:
# 使用脚本所在目录项目根目录下的VBA文件夹
script_dir = Path(__file__).parent
self.output_dir = script_dir / "VBA"
else:
# 确定输出目录的优先级:
# 1. 参数指定的 output_dir
# 2. 环境变量配置的 VBA_OUTPUT_DIR
# 3. 默认源文件同目录下的VBA文件夹
if output_dir is not None:
self.output_dir = Path(output_dir)
elif VBA_OUTPUT_DIR is not None:
self.output_dir = Path(VBA_OUTPUT_DIR)
else:
# 根据文件类型使用不同的默认文件夹
file_type = get_file_type(self.source_path)
default_dir = "VBA-Access" if file_type == 'access' else "VBA-Excel"
self.output_dir = self.source_path.parent / default_dir
# 创建输出目录结构
self.modules_dir = self.output_dir / STANDARD_MODULE_DIR
@@ -67,12 +92,6 @@ class VBAExtractor:
self.document_modules_dir.mkdir(parents=True, exist_ok=True)
self.forms_dir.mkdir(parents=True, exist_ok=True)
# 存储模块元数据
self.metadata = {
"source_file": str(self.xlsm_path),
"modules": {}
}
def parse_attributes(self, code: str) -> Tuple[Dict[str, str], str]:
"""
解析VBA代码中的Attribute信息
@@ -131,10 +150,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")
@@ -145,14 +164,10 @@ class VBAExtractor:
vba_parser.close()
# 保存元数据文件
self._save_metadata()
print(f"\n提取完成!")
print(f"- 标准模块: {self.modules_dir}")
print(f"- 类模块: {self.class_modules_dir}")
print(f"- 文档模块: {self.document_modules_dir}")
print(f"- 元数据: {self.output_dir / METADATA_FILE}")
return True
else:
print("未在文件中发现VBA代码")
@@ -177,14 +192,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:
@@ -235,14 +250,10 @@ class VBAExtractor:
workbook.Close(False)
excel.Quit()
# 保存元数据文件
self._save_metadata()
print(f"\n提取完成!")
print(f"- 标准模块: {self.modules_dir}")
print(f"- 类模块: {self.class_modules_dir}")
print(f"- 文档模块: {self.document_modules_dir}")
print(f"- 元数据: {self.output_dir / METADATA_FILE}")
return True
except Exception as e:
@@ -257,6 +268,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:
"""
根据模块名称和流路径确定模块类型
@@ -266,7 +355,7 @@ class VBAExtractor:
stream_path: 流路径
Returns:
模块类型: Modules, ClassModules, DocumentModules
模块类型: Modules, ClassModules, DocumentModules, Forms
"""
name_lower = module_name.lower()
@@ -282,6 +371,10 @@ class VBAExtractor:
if name_lower.startswith('cls') or name_lower.startswith('class'):
return CLASS_MODULE_DIR
# 窗体模块
if name_lower.startswith('userform') or name_lower.startswith('frm_') or name_lower.startswith('frm'):
return FORMS_DIR
# 根据stream_path判断
if stream_path:
path_lower = stream_path.lower()
@@ -289,6 +382,8 @@ class VBAExtractor:
return DOCUMENT_MODULE_DIR
elif 'class' in path_lower or 'cls' in path_lower:
return CLASS_MODULE_DIR
elif 'form' in path_lower or 'userform' in path_lower:
return FORMS_DIR
# 默认为标准模块
return STANDARD_MODULE_DIR
@@ -322,7 +417,12 @@ class VBAExtractor:
}.get(module_type, self.modules_dir)
# 确定文件扩展名
ext = '.cls' if module_type in [CLASS_MODULE_DIR, DOCUMENT_MODULE_DIR, FORMS_DIR] else '.bas'
if module_type == FORMS_DIR:
ext = '.frm'
elif module_type in [CLASS_MODULE_DIR, DOCUMENT_MODULE_DIR]:
ext = '.cls'
else:
ext = '.bas'
# 清理文件名(移除已有扩展名)
clean_name = module_name.replace('/', '_').replace('\\', '_')
@@ -338,22 +438,8 @@ class VBAExtractor:
with open(file_path, 'w', encoding='utf-8') as f:
f.write(clean_code)
# 保存元数据
self.metadata["modules"][clean_name] = {
"name": module_name,
"type": module_type,
"attributes": attributes,
"file": str(file_path.relative_to(self.output_dir))
}
print(f" [OK] 已保存: {clean_name} ({module_type})")
def _save_metadata(self):
"""保存元数据到JSON文件"""
metadata_path = self.output_dir / METADATA_FILE
with open(metadata_path, 'w', encoding='utf-8') as f:
json.dump(self.metadata, f, indent=2, ensure_ascii=False)
def main():
"""主函数"""
@@ -363,10 +449,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():
@@ -376,68 +462,74 @@ 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
# 交互模式:查找支持的文件
all_files = []
for scan_dir in [Path("Excel"), Path("Access")]:
if scan_dir.exists():
for ext in ["*.xlsm", "*.accdb", "*.mdb"]:
all_files.extend(scan_dir.glob(ext))
xlsm_files = list(excel_dir.glob("*.xlsm"))
if not xlsm_files:
print("错误: Excel文件夹中没有xlsm文件")
if not all_files:
print("错误: 未找到.xlsm或.accdb文件请检查Excel/或Access/文件夹)")
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()
# 创建提取器使用默认参数输出到目标文件同目录下的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)

View File

@@ -1,17 +1,14 @@
"""
VBA代码导入工具 (最终修正版)
1. 修复路径识别问题
2. 修复中文乱码问题 (GB18030)
3. 修复类模块(Class)被错误导入为标准模块的问题 (补全 VERSION 1.0 CLASS 头)
VBA代码导入工具
使用 .env 配置直接导入 VBA 代码,无需元数据文件
"""
import os
import sys
import json
import shutil
import tempfile
from pathlib import Path
from typing import Dict, Any
from typing import Dict, List, Tuple
# 加载 .env 配置文件
try:
@@ -29,44 +26,91 @@ except ImportError:
sys.exit(1)
# ==================== 配置区域 ====================
# 从 .env 文件读取配置,如果未设置则使用 None默认模式
TARGET_METADATA_FILE = os.getenv("TARGET_METADATA_FILE", "").strip() or None
# 从 .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
# =================================================
# 常量定义
METADATA_FILE = "vba_metadata.json"
VBA_DIR_NAME = "VBA"
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'
Raises:
ValueError: 不支持的文件扩展名
"""
ext = file_path.suffix.lower()
if ext in ACCESS_EXTENSIONS:
return 'access'
if ext in EXCEL_EXTENSIONS:
return 'excel'
raise ValueError(f"不支持的文件类型: {ext}(支持: .xlsm, .xls, .xlsb, .accdb, .mdb")
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)
def __init__(self, vba_dir: str, target_file: str):
"""
初始化VBA导入器
if not candidate_path.is_absolute():
candidate_path = self.project_root / candidate_path
Args:
vba_dir: VBA代码目录包含 Modules, ClassModules 等子目录)
target_file: 目标 Excel 文件路径
"""
self.vba_dir = Path(vba_dir).resolve()
self.target_file = Path(target_file).resolve()
self.target_file = candidate_path
def _scan_modules(self) -> List[Dict]:
"""
扫描 VBA 目录,收集所有模块信息
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()
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:
"""去除模块名称中的扩展名"""
@@ -76,7 +120,7 @@ class VBAImporter:
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:
def _reconstruct_file_content(self, code_path: Path, module_name: str, module_type: str) -> Path:
"""
读取纯代码文件,重建完整的导入文件
关键逻辑:
@@ -90,39 +134,31 @@ class VBAImporter:
# 创建临时文件
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":
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")
# 注意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("")
content_lines.append(code_body)
# 4. 写入临时文件 (GB18030 防止乱码)
try:
with open(temp_file_path, 'w', encoding='gb18030', errors='replace') as f:
@@ -131,7 +167,7 @@ class VBAImporter:
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):
@@ -140,17 +176,21 @@ class VBAImporter:
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:
@@ -158,22 +198,19 @@ class VBAImporter:
return False
print("开始导入模块...\n")
modules = self.metadata.get("modules", {})
# 扫描所有模块
modules = self._scan_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
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:
@@ -182,8 +219,8 @@ class VBAImporter:
component = None
# 标准模块和类模块支持删除重建
is_reloadable = module_type_dir in ["Modules", "ClassModules"]
is_reloadable = module_type_dir in [STANDARD_MODULE_DIR, CLASS_MODULE_DIR]
# ---------------------------------------------------------
# 策略 A: 导入文件模式 (Modules, ClassModules)
# ---------------------------------------------------------
@@ -193,33 +230,32 @@ class VBAImporter:
vb_project.VBComponents.Remove(component)
except Exception as e:
print(f" [警告] 无法移除 {module_name}: {e},将尝试仅更新代码")
is_reloadable = False
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_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":
if module_type_dir == FORMS_DIR:
print(f" [警告] 无法恢复 UserForm '{module_name}',跳过。")
continue
elif module_type_dir == "DocumentModules":
elif module_type_dir == DOCUMENT_MODULE_DIR:
print(f" [警告] 找不到文档对象 '{module_name}',跳过。")
continue
try:
component = vb_project.VBComponents.Add(1)
component = vb_project.VBComponents.Add(1)
component.Name = module_name
except:
print(f" [错误] 无法创建组件 {module_name}")
@@ -230,14 +266,14 @@ class VBAImporter:
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}")
@@ -267,7 +303,7 @@ class VBAImporter:
import traceback
traceback.print_exc()
return False
finally:
if workbook:
try: workbook.Close(SaveChanges=False)
@@ -276,69 +312,206 @@ class VBAImporter:
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:
access.DoCmd.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 access:
try:
access.CloseCurrentDatabase()
except:
pass
try:
access.Quit()
except:
pass
def main():
print("=" * 60)
print("VBA代码导入工具 (V3.0 最终版)")
print("VBA代码导入工具 (V5.0 - 支持Excel和Access)")
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_FILE:
print("错误: 未配置 TARGET_FILE")
print("请在 .env 文件中设置目标文件路径(支持.xlsm和.accdb")
return
# 如果是相对路径,则相对于脚本所在目录
if not target_path.is_absolute():
target_path = script_dir / target_path
# 确定目标文件路径
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
if not target_path.exists():
print(f"错误: 配置的文件不存在: {target_path}")
return
if not target_path.name == METADATA_FILE:
print(f"警告: 文件名不是 {METADATA_FILE}: {target_path.name}")
file_type = get_file_type(target_path)
metadata_path = target_path
print(f"使用配置的元数据文件: {metadata_path.name}")
print()
# 确定 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_dir = script_dir / VBA_DIR_NAME
metadata_path = vba_dir / METADATA_FILE
# 根据文件类型使用不同的默认文件夹
default_dir = "VBA-Access" if file_type == "access" else "VBA-Excel"
vba_path = target_path.parent / default_dir
if not metadata_path.exists():
print(f"错误: 找不到元数据文件: {metadata_path}")
print()
print("提示:")
print(" 1. 确保已运行 extract_vba.py 提取 VBA 代码")
print(" 2. 或在脚本顶部配置 TARGET_METADATA_FILE 指定元数据文件路径")
return
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
print(f"读取元数据: {metadata_path}")
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("警告: 此操作将覆盖目标 Excel 文件中的 VBA 代码。")
print(f"警告: 此操作将覆盖目标{type_label}文件中的 VBA 代码。")
choice = input("\n确认继续? (y/n): ").lower().strip()
if choice != 'y':
print("操作已取消")
return
importer = VBAImporter(str(metadata_path))
importer = VBAImporter(str(vba_path), str(target_path))
# 显示导入目标信息
print()
print(f"目标 Excel 文件: {importer.target_file.name}")
print(f"VBA 代码目录: {importer.vba_dir}")
print(f"模块数量: {len(importer.metadata.get('modules', {}))}")
# 显示模块数量
modules = importer._scan_modules()
print(f"找到 {len(modules)} 个模块文件")
print()
print("=" * 60)
print()
success = importer.import_vba()
# 根据文件类型选择导入方法
if file_type == 'access':
success = importer.import_vba_access()
else:
success = importer.import_vba()
print()
print("=" * 60)
@@ -349,4 +522,4 @@ def main():
print("=" * 60)
if __name__ == "__main__":
main()
main()