Compare commits
9 Commits
3569c76e53
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
585a9ed7f3 | ||
|
|
8642d40032 | ||
|
|
d156fdb2d8 | ||
|
|
ee1120c247 | ||
|
|
df81562dd9 | ||
|
|
788a8fd9e7 | ||
|
|
ab01835b06 | ||
|
|
7b27e50573 | ||
|
|
59f0ef2125 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -15,4 +15,5 @@ tmpclaude-*
|
|||||||
*.png
|
*.png
|
||||||
data/
|
data/
|
||||||
Excel/
|
Excel/
|
||||||
|
Access/
|
||||||
VBA/
|
VBA/
|
||||||
34
CLAUDE.md
34
CLAUDE.md
@@ -4,7 +4,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
|||||||
|
|
||||||
## Project Overview
|
## 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:
|
The VBA code implements a hierarchical BOM management system with:
|
||||||
- **clsBOMManager**: Main class managing BOM data structure and category relationships
|
- **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/
|
Auto_BOM/
|
||||||
├── Excel/ # Source Excel files (.xlsm) - gitignored
|
├── Excel/ # Source Excel files (.xlsm) - gitignored
|
||||||
|
├── Access/ # Source Access files (.accdb) - gitignored
|
||||||
├── VBA/ # Extracted VBA code - gitignored
|
├── VBA/ # Extracted VBA code - gitignored
|
||||||
│ ├── Modules/ # Standard modules (.bas)
|
│ ├── Modules/ # Standard modules (.bas)
|
||||||
│ ├── ClassModules/ # Class modules (.cls)
|
│ ├── ClassModules/ # Class modules (.cls)
|
||||||
│ ├── DocumentModules/# Sheet/workbook modules (.cls)
|
│ ├── DocumentModules/# Sheet/workbook modules (.cls)
|
||||||
│ ├── Forms/ # User forms
|
│ ├── Forms/ # User forms
|
||||||
│ └── vba_metadata.json # Module metadata for import
|
│ └── vba_metadata.json # Module metadata for import
|
||||||
├── extract_vba.py # Extract VBA from Excel files
|
├── extract_vba.py # Extract VBA from Excel/Access files
|
||||||
├── import_vba.py # Import VBA back to Excel files
|
├── import_vba.py # Import VBA back to Excel/Access files
|
||||||
├── main.py # Empty placeholder
|
├── main.py # Empty placeholder
|
||||||
└── requirements.txt # Python dependencies
|
└── requirements.txt # Python dependencies
|
||||||
```
|
```
|
||||||
@@ -40,8 +41,8 @@ pip install -r requirements.txt
|
|||||||
```
|
```
|
||||||
|
|
||||||
**Dependencies:**
|
**Dependencies:**
|
||||||
- `pywin32>=306` - Windows COM interface for Excel automation (Windows only)
|
- `pywin32>=306` - Windows COM interface for Excel/Access automation (Windows only)
|
||||||
- `oletools>=0.60` - Alternative VBA extraction without Excel dependency
|
- `oletools>=0.60` - Alternative VBA extraction without Excel dependency (Excel only)
|
||||||
|
|
||||||
## Common Commands
|
## Common Commands
|
||||||
|
|
||||||
@@ -51,20 +52,24 @@ pip install -r requirements.txt
|
|||||||
python extract_vba.py
|
python extract_vba.py
|
||||||
```
|
```
|
||||||
|
|
||||||
Two extraction methods available:
|
**Excel (.xlsm)** - Two extraction methods available:
|
||||||
1. **COM Interface** (recommended) - Requires Microsoft Excel, more reliable
|
1. **COM Interface** (recommended) - Requires Microsoft Excel, more reliable
|
||||||
2. **olevba Library** - No Excel required, uses oletools
|
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
|
- Excel > Options > Trust Center > Trust Center Settings
|
||||||
- Check "Trust access to the VBA project object model"
|
- Check "Trust access to the VBA project object model"
|
||||||
|
|
||||||
### Import VBA Code
|
### Import VBA Code
|
||||||
```bash
|
```bash
|
||||||
# Import from VBA/ directory back to Excel
|
# Import from VBA/ directory back to Excel or Access
|
||||||
python import_vba.py VBA/vba_metadata.json
|
python import_vba.py
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Set `TARGET_FILE` in `.env` to the target `.xlsm` or `.accdb` file path.
|
||||||
|
|
||||||
## VBA Code Architecture
|
## VBA Code Architecture
|
||||||
|
|
||||||
### BOM Data Model
|
### 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
|
- Cleans `Attribute` statements from exported code for readability
|
||||||
- Automatically categorizes modules by type (Standard/Class/Document/Form)
|
- 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)
|
- 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)
|
### 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
|
- **Critical fix for ClassModules**: Reconstructs `VERSION 1.0 CLASS` header before import
|
||||||
- **Encoding handling**: Uses GB18030 for temp files to prevent Chinese character corruption
|
- **Encoding handling**: Uses GB18030 for temp files to prevent Chinese character corruption
|
||||||
- Path recognition logic handles relative/absolute paths in metadata
|
- 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
|
## Platform Requirements
|
||||||
|
|
||||||
- **Windows required** for import functionality (COM interface)
|
- **Windows required** for import functionality (COM interface)
|
||||||
- **Microsoft Excel** required for COM-based extraction/import
|
- **Microsoft Excel** required for Excel COM-based extraction/import
|
||||||
- Cross-platform extraction possible with oletools (no Excel needed)
|
- **Microsoft Access** required for Access COM-based extraction/import
|
||||||
|
- Cross-platform extraction possible with oletools (Excel only, no Office needed)
|
||||||
|
|
||||||
## Git Workflow
|
## Git Workflow
|
||||||
|
|
||||||
The `.gitignore` excludes:
|
The `.gitignore` excludes:
|
||||||
- Virtual environment (`.venv/`)
|
- Virtual environment (`.venv/`)
|
||||||
- Build artifacts (`build/`, `dist/`)
|
- Build artifacts (`build/`, `dist/`)
|
||||||
- Project data (`Excel/`, `VBA/`)
|
- Project data (`Excel/`, `Access/`, `VBA/`)
|
||||||
- Claude temporary files (`.claude/`, `tmpclaude-*`)
|
- Claude temporary files (`.claude/`, `tmpclaude-*`)
|
||||||
|
|
||||||
Only commit code changes, not extracted VBA or Excel files.
|
Only commit code changes, not extracted VBA or Excel files.
|
||||||
|
|||||||
36
docs/plans/2026-05-11-access-support-design.md
Normal file
36
docs/plans/2026-05-11-access-support-design.md
Normal 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)
|
||||||
629
docs/plans/2026-05-11-access-support-plan.md
Normal file
629
docs/plans/2026-05-11-access-support-plan.md
Normal 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"
|
||||||
|
```
|
||||||
276
extract_vba.py
276
extract_vba.py
@@ -1,15 +1,14 @@
|
|||||||
"""
|
"""
|
||||||
VBA代码提取工具
|
VBA代码提取工具
|
||||||
从xlsm文件中提取模块和类模块代码,分类保存到VBA文件夹
|
从Excel(.xlsm)或Access(.accdb)文件中提取VBA代码,分类保存到VBA文件夹
|
||||||
自动清理Attribute信息并生成元数据JSON文件
|
自动清理Attribute信息
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
import json
|
|
||||||
import re
|
import re
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Dict, List, Tuple, Optional
|
from typing import Tuple, Dict
|
||||||
|
|
||||||
# 加载 .env 配置文件
|
# 加载 .env 配置文件
|
||||||
try:
|
try:
|
||||||
@@ -21,7 +20,9 @@ except ImportError:
|
|||||||
|
|
||||||
# ==================== 配置区域 ====================
|
# ==================== 配置区域 ====================
|
||||||
# 从 .env 文件读取配置,如果未设置则使用 None(交互模式)
|
# 从 .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项目相关常量
|
# VBA项目相关常量
|
||||||
@@ -29,32 +30,56 @@ STANDARD_MODULE_DIR = "Modules"
|
|||||||
CLASS_MODULE_DIR = "ClassModules"
|
CLASS_MODULE_DIR = "ClassModules"
|
||||||
DOCUMENT_MODULE_DIR = "DocumentModules"
|
DOCUMENT_MODULE_DIR = "DocumentModules"
|
||||||
FORMS_DIR = "Forms"
|
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:
|
class VBAExtractor:
|
||||||
"""VBA代码提取器"""
|
"""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提取器
|
初始化VBA提取器
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
xlsm_path: xlsm文件路径
|
source_path: 源文件路径(支持.xlsm/.accdb等)
|
||||||
output_dir: 输出目录(当use_same_dir=False时有效)
|
output_dir: 输出目录(如果未指定,则使用VBA_OUTPUT_DIR配置或源文件同目录)
|
||||||
use_same_dir: 是否使用目标文件同目录下的VBA文件夹,默认True
|
|
||||||
"""
|
"""
|
||||||
self.xlsm_path = Path(xlsm_path)
|
self.source_path = Path(source_path)
|
||||||
|
|
||||||
if use_same_dir:
|
# 确定输出目录的优先级:
|
||||||
# 使用目标文件同目录下的VBA文件夹
|
# 1. 参数指定的 output_dir
|
||||||
self.output_dir = self.xlsm_path.parent / "VBA"
|
# 2. 环境变量配置的 VBA_OUTPUT_DIR
|
||||||
elif output_dir is None:
|
# 3. 默认:源文件同目录下的VBA文件夹
|
||||||
# 使用脚本所在目录(项目根目录)下的VBA文件夹
|
if output_dir is not None:
|
||||||
script_dir = Path(__file__).parent
|
|
||||||
self.output_dir = script_dir / "VBA"
|
|
||||||
else:
|
|
||||||
self.output_dir = Path(output_dir)
|
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
|
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.document_modules_dir.mkdir(parents=True, exist_ok=True)
|
||||||
self.forms_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]:
|
def parse_attributes(self, code: str) -> Tuple[Dict[str, str], str]:
|
||||||
"""
|
"""
|
||||||
解析VBA代码中的Attribute信息
|
解析VBA代码中的Attribute信息
|
||||||
@@ -131,10 +150,10 @@ class VBAExtractor:
|
|||||||
print("请运行: pip install oletools")
|
print("请运行: pip install oletools")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
print(f"正在解析文件: {self.xlsm_path.name}")
|
print(f"正在解析文件: {self.source_path.name}")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
vba_parser = VBA_Parser(str(self.xlsm_path))
|
vba_parser = VBA_Parser(str(self.source_path))
|
||||||
|
|
||||||
if vba_parser.detect_vba_macros():
|
if vba_parser.detect_vba_macros():
|
||||||
print("发现VBA代码,开始提取...\n")
|
print("发现VBA代码,开始提取...\n")
|
||||||
@@ -145,14 +164,10 @@ class VBAExtractor:
|
|||||||
|
|
||||||
vba_parser.close()
|
vba_parser.close()
|
||||||
|
|
||||||
# 保存元数据文件
|
|
||||||
self._save_metadata()
|
|
||||||
|
|
||||||
print(f"\n提取完成!")
|
print(f"\n提取完成!")
|
||||||
print(f"- 标准模块: {self.modules_dir}")
|
print(f"- 标准模块: {self.modules_dir}")
|
||||||
print(f"- 类模块: {self.class_modules_dir}")
|
print(f"- 类模块: {self.class_modules_dir}")
|
||||||
print(f"- 文档模块: {self.document_modules_dir}")
|
print(f"- 文档模块: {self.document_modules_dir}")
|
||||||
print(f"- 元数据: {self.output_dir / METADATA_FILE}")
|
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
print("未在文件中发现VBA代码")
|
print("未在文件中发现VBA代码")
|
||||||
@@ -177,14 +192,14 @@ class VBAExtractor:
|
|||||||
print("请运行: pip install pywin32")
|
print("请运行: pip install pywin32")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
print(f"正在使用COM接口解析: {self.xlsm_path.name}")
|
print(f"正在使用COM接口解析: {self.source_path.name}")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
excel = win32.Dispatch("Excel.Application")
|
excel = win32.Dispatch("Excel.Application")
|
||||||
excel.Visible = False
|
excel.Visible = False
|
||||||
excel.DisplayAlerts = False
|
excel.DisplayAlerts = False
|
||||||
|
|
||||||
workbook = excel.Workbooks.Open(str(self.xlsm_path.absolute()))
|
workbook = excel.Workbooks.Open(str(self.source_path.absolute()))
|
||||||
|
|
||||||
# 获取VBA项目
|
# 获取VBA项目
|
||||||
if not workbook.VBProject:
|
if not workbook.VBProject:
|
||||||
@@ -235,14 +250,10 @@ class VBAExtractor:
|
|||||||
workbook.Close(False)
|
workbook.Close(False)
|
||||||
excel.Quit()
|
excel.Quit()
|
||||||
|
|
||||||
# 保存元数据文件
|
|
||||||
self._save_metadata()
|
|
||||||
|
|
||||||
print(f"\n提取完成!")
|
print(f"\n提取完成!")
|
||||||
print(f"- 标准模块: {self.modules_dir}")
|
print(f"- 标准模块: {self.modules_dir}")
|
||||||
print(f"- 类模块: {self.class_modules_dir}")
|
print(f"- 类模块: {self.class_modules_dir}")
|
||||||
print(f"- 文档模块: {self.document_modules_dir}")
|
print(f"- 文档模块: {self.document_modules_dir}")
|
||||||
print(f"- 元数据: {self.output_dir / METADATA_FILE}")
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -257,6 +268,84 @@ class VBAExtractor:
|
|||||||
pass
|
pass
|
||||||
return False
|
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:
|
def _determine_module_type(self, module_name: str, stream_path: str) -> str:
|
||||||
"""
|
"""
|
||||||
根据模块名称和流路径确定模块类型
|
根据模块名称和流路径确定模块类型
|
||||||
@@ -266,7 +355,7 @@ class VBAExtractor:
|
|||||||
stream_path: 流路径
|
stream_path: 流路径
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
模块类型: Modules, ClassModules, DocumentModules
|
模块类型: Modules, ClassModules, DocumentModules, Forms
|
||||||
"""
|
"""
|
||||||
name_lower = module_name.lower()
|
name_lower = module_name.lower()
|
||||||
|
|
||||||
@@ -282,6 +371,10 @@ class VBAExtractor:
|
|||||||
if name_lower.startswith('cls') or name_lower.startswith('class'):
|
if name_lower.startswith('cls') or name_lower.startswith('class'):
|
||||||
return CLASS_MODULE_DIR
|
return CLASS_MODULE_DIR
|
||||||
|
|
||||||
|
# 窗体模块
|
||||||
|
if name_lower.startswith('userform') or name_lower.startswith('frm_') or name_lower.startswith('frm'):
|
||||||
|
return FORMS_DIR
|
||||||
|
|
||||||
# 根据stream_path判断
|
# 根据stream_path判断
|
||||||
if stream_path:
|
if stream_path:
|
||||||
path_lower = stream_path.lower()
|
path_lower = stream_path.lower()
|
||||||
@@ -289,6 +382,8 @@ class VBAExtractor:
|
|||||||
return DOCUMENT_MODULE_DIR
|
return DOCUMENT_MODULE_DIR
|
||||||
elif 'class' in path_lower or 'cls' in path_lower:
|
elif 'class' in path_lower or 'cls' in path_lower:
|
||||||
return CLASS_MODULE_DIR
|
return CLASS_MODULE_DIR
|
||||||
|
elif 'form' in path_lower or 'userform' in path_lower:
|
||||||
|
return FORMS_DIR
|
||||||
|
|
||||||
# 默认为标准模块
|
# 默认为标准模块
|
||||||
return STANDARD_MODULE_DIR
|
return STANDARD_MODULE_DIR
|
||||||
@@ -322,7 +417,12 @@ class VBAExtractor:
|
|||||||
}.get(module_type, self.modules_dir)
|
}.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('\\', '_')
|
clean_name = module_name.replace('/', '_').replace('\\', '_')
|
||||||
@@ -338,22 +438,8 @@ class VBAExtractor:
|
|||||||
with open(file_path, 'w', encoding='utf-8') as f:
|
with open(file_path, 'w', encoding='utf-8') as f:
|
||||||
f.write(clean_code)
|
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})")
|
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():
|
def main():
|
||||||
"""主函数"""
|
"""主函数"""
|
||||||
@@ -363,10 +449,10 @@ def main():
|
|||||||
print()
|
print()
|
||||||
|
|
||||||
# 检查是否配置了目标文件
|
# 检查是否配置了目标文件
|
||||||
if TARGET_XLSM_FILE and TARGET_XLSM_FILE.strip():
|
if TARGET_FILE and TARGET_FILE.strip():
|
||||||
# 使用配置的文件路径
|
# 使用配置的文件路径
|
||||||
script_dir = Path(__file__).parent
|
script_dir = Path(__file__).parent
|
||||||
target_path = Path(TARGET_XLSM_FILE)
|
target_path = Path(TARGET_FILE)
|
||||||
|
|
||||||
# 如果是相对路径,则相对于脚本所在目录
|
# 如果是相对路径,则相对于脚本所在目录
|
||||||
if not target_path.is_absolute():
|
if not target_path.is_absolute():
|
||||||
@@ -376,68 +462,74 @@ def main():
|
|||||||
print(f"错误: 配置的文件不存在: {target_path}")
|
print(f"错误: 配置的文件不存在: {target_path}")
|
||||||
return
|
return
|
||||||
|
|
||||||
if not target_path.suffix.lower() == '.xlsm':
|
file_type = get_file_type(target_path)
|
||||||
print(f"警告: 文件扩展名不是.xlsm: {target_path.name}")
|
source_file = target_path
|
||||||
|
print(f"使用配置文件: {source_file.name} ({file_type})")
|
||||||
xlsm_file = target_path
|
|
||||||
print(f"使用配置文件: {xlsm_file.name}")
|
|
||||||
print()
|
print()
|
||||||
else:
|
else:
|
||||||
# 交互模式:查找xlsm文件
|
# 交互模式:查找支持的文件
|
||||||
excel_dir = Path("Excel")
|
all_files = []
|
||||||
if not excel_dir.exists():
|
for scan_dir in [Path("Excel"), Path("Access")]:
|
||||||
print("错误: 未找到Excel文件夹")
|
if scan_dir.exists():
|
||||||
return
|
for ext in ["*.xlsm", "*.accdb", "*.mdb"]:
|
||||||
|
all_files.extend(scan_dir.glob(ext))
|
||||||
|
|
||||||
xlsm_files = list(excel_dir.glob("*.xlsm"))
|
if not all_files:
|
||||||
if not xlsm_files:
|
print("错误: 未找到.xlsm或.accdb文件(请检查Excel/或Access/文件夹)")
|
||||||
print("错误: Excel文件夹中没有xlsm文件")
|
|
||||||
return
|
return
|
||||||
|
|
||||||
# 如果有多个文件,让用户选择
|
# 如果有多个文件,让用户选择
|
||||||
if len(xlsm_files) > 1:
|
if len(all_files) > 1:
|
||||||
print("发现多个xlsm文件:")
|
print("发现多个文件:")
|
||||||
for i, f in enumerate(xlsm_files, 1):
|
for i, f in enumerate(all_files, 1):
|
||||||
print(f" {i}. {f.name}")
|
ft = get_file_type(f)
|
||||||
|
print(f" {i}. {f.name} ({ft})")
|
||||||
print()
|
print()
|
||||||
choice = input("请选择文件编号 (直接回车选择第1个): ").strip()
|
choice = input("请选择文件编号 (直接回车选择第1个): ").strip()
|
||||||
if not choice:
|
if not choice:
|
||||||
xlsm_file = xlsm_files[0]
|
source_file = all_files[0]
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
idx = int(choice) - 1
|
idx = int(choice) - 1
|
||||||
xlsm_file = xlsm_files[idx]
|
source_file = all_files[idx]
|
||||||
except:
|
except:
|
||||||
print("无效选择,使用第一个文件")
|
print("无效选择,使用第一个文件")
|
||||||
xlsm_file = xlsm_files[0]
|
source_file = all_files[0]
|
||||||
else:
|
else:
|
||||||
xlsm_file = xlsm_files[0]
|
source_file = all_files[0]
|
||||||
|
|
||||||
|
file_type = get_file_type(source_file)
|
||||||
print()
|
print()
|
||||||
print(f"选择文件: {xlsm_file.name}")
|
print(f"选择文件: {source_file.name} ({file_type})")
|
||||||
print()
|
print()
|
||||||
|
|
||||||
# 创建提取器(使用默认参数,输出到目标文件同目录下的VBA文件夹)
|
# 创建提取器
|
||||||
extractor = VBAExtractor(str(xlsm_file))
|
extractor = VBAExtractor(str(source_file))
|
||||||
|
|
||||||
# 显示输出目录信息
|
# 显示输出目录信息
|
||||||
print(f"输出目录: {extractor.output_dir}")
|
print(f"输出目录: {extractor.output_dir}")
|
||||||
print()
|
print()
|
||||||
|
|
||||||
# 选择提取方法
|
# 根据文件类型选择提取方法
|
||||||
print("请选择提取方法:")
|
if file_type == 'access':
|
||||||
print(" 1. COM接口 (推荐 - 需要安装Excel)")
|
# Access 只支持COM方法
|
||||||
print(" 2. olevba库 (不需要Excel)")
|
print("Access文件仅支持COM接口提取...")
|
||||||
print()
|
success = extractor.extract_vba_modules_access_com()
|
||||||
|
|
||||||
method = input("请选择 (直接回车使用方法1): ").strip()
|
|
||||||
|
|
||||||
if method == "2":
|
|
||||||
print("\n使用olevba库提取...")
|
|
||||||
success = extractor.extract_vba_modules_olevba()
|
|
||||||
else:
|
else:
|
||||||
print("\n使用COM接口提取...")
|
# Excel 支持COM和olevba
|
||||||
success = extractor.extract_vba_modules_com()
|
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:
|
if success:
|
||||||
print("\n" + "=" * 60)
|
print("\n" + "=" * 60)
|
||||||
|
|||||||
417
import_vba.py
417
import_vba.py
@@ -1,17 +1,14 @@
|
|||||||
"""
|
"""
|
||||||
VBA代码导入工具 (最终修正版)
|
VBA代码导入工具
|
||||||
1. 修复路径识别问题
|
使用 .env 配置直接导入 VBA 代码,无需元数据文件
|
||||||
2. 修复中文乱码问题 (GB18030)
|
|
||||||
3. 修复类模块(Class)被错误导入为标准模块的问题 (补全 VERSION 1.0 CLASS 头)
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
import json
|
|
||||||
import shutil
|
import shutil
|
||||||
import tempfile
|
import tempfile
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Dict, Any
|
from typing import Dict, List, Tuple
|
||||||
|
|
||||||
# 加载 .env 配置文件
|
# 加载 .env 配置文件
|
||||||
try:
|
try:
|
||||||
@@ -29,44 +26,91 @@ except ImportError:
|
|||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
# ==================== 配置区域 ====================
|
# ==================== 配置区域 ====================
|
||||||
# 从 .env 文件读取配置,如果未设置则使用 None(默认模式)
|
# 从 .env 文件读取配置
|
||||||
TARGET_METADATA_FILE = os.getenv("TARGET_METADATA_FILE", "").strip() or None
|
# 目标文件路径(支持 .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"
|
STANDARD_MODULE_DIR = "Modules"
|
||||||
VBA_DIR_NAME = "VBA"
|
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:
|
class VBAImporter:
|
||||||
"""VBA代码导入器"""
|
"""VBA代码导入器"""
|
||||||
|
|
||||||
def __init__(self, metadata_path: str):
|
def __init__(self, vba_dir: str, target_file: str):
|
||||||
self.metadata_path = Path(metadata_path).resolve()
|
"""
|
||||||
self.vba_dir = self.metadata_path.parent
|
初始化VBA导入器
|
||||||
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)
|
|
||||||
|
|
||||||
if not candidate_path.is_absolute():
|
Args:
|
||||||
candidate_path = self.project_root / candidate_path
|
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():
|
Returns:
|
||||||
print(f"提示: 在路径 {self.target_file} 未找到文件,尝试搜索...")
|
模块信息列表,每个元素包含:
|
||||||
excel_dir = self.project_root / "Excel"
|
- name: 模块名称
|
||||||
if excel_dir.exists():
|
- type: 模块类型目录
|
||||||
files = list(excel_dir.glob("*.xlsm"))
|
- file_path: 源文件完整路径
|
||||||
if files:
|
- ext: 文件扩展名
|
||||||
print(f" -> 找到替代文件: {files[0].name}")
|
"""
|
||||||
self.target_file = files[0]
|
modules = []
|
||||||
|
|
||||||
self.target_file = self.target_file.resolve()
|
# 定义扫描目录和对应的扩展名
|
||||||
|
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:
|
def _clean_component_name(self, name: str) -> str:
|
||||||
"""去除模块名称中的扩展名"""
|
"""去除模块名称中的扩展名"""
|
||||||
@@ -76,7 +120,7 @@ class VBAImporter:
|
|||||||
return name[:-len(ext)]
|
return name[:-len(ext)]
|
||||||
return name
|
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 = Path(tempfile.gettempdir()) / "vba_import_temp"
|
||||||
temp_dir.mkdir(exist_ok=True)
|
temp_dir.mkdir(exist_ok=True)
|
||||||
|
|
||||||
# 确定扩展名
|
# 确定扩展名
|
||||||
orig_ext = code_path.suffix
|
orig_ext = code_path.suffix
|
||||||
temp_file_path = temp_dir / f"{module_name}{orig_ext}"
|
temp_file_path = temp_dir / f"{module_name}{orig_ext}"
|
||||||
|
|
||||||
content_lines = []
|
content_lines = []
|
||||||
|
|
||||||
# -----------------------------------------------------------
|
# -----------------------------------------------------------
|
||||||
# 【关键修复】如果是类模块,必须添加 VERSION 头部块
|
# 【关键修复】如果是类模块,必须添加 VERSION 头部块
|
||||||
# -----------------------------------------------------------
|
# -----------------------------------------------------------
|
||||||
if module_type == "ClassModules":
|
if module_type == CLASS_MODULE_DIR:
|
||||||
content_lines.append("VERSION 1.0 CLASS")
|
content_lines.append("VERSION 1.0 CLASS")
|
||||||
content_lines.append("BEGIN")
|
content_lines.append("BEGIN")
|
||||||
content_lines.append(" MultiUse = -1 'True")
|
content_lines.append(" MultiUse = -1 'True")
|
||||||
content_lines.append("END")
|
content_lines.append("END")
|
||||||
# 注意:Attribute VB_Name 必须紧跟在 END 之后
|
|
||||||
|
|
||||||
# 2. 重建 Attribute VB_Name
|
# 2. 重建 Attribute VB_Name
|
||||||
content_lines.append(f'Attribute VB_Name = "{module_name}"')
|
content_lines.append(f'Attribute VB_Name = "{module_name}"')
|
||||||
|
|
||||||
# 3. 重建其他 Attribute
|
# 注意:由于我们移除了元数据,不再有其他属性信息
|
||||||
for key, value in attributes.items():
|
# 如果需要其他属性,需要从源文件中解析或在代码中显式声明
|
||||||
if key == "VB_Name":
|
|
||||||
continue
|
content_lines.append("")
|
||||||
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(code_body)
|
content_lines.append(code_body)
|
||||||
|
|
||||||
# 4. 写入临时文件 (GB18030 防止乱码)
|
# 4. 写入临时文件 (GB18030 防止乱码)
|
||||||
try:
|
try:
|
||||||
with open(temp_file_path, 'w', encoding='gb18030', errors='replace') as f:
|
with open(temp_file_path, 'w', encoding='gb18030', errors='replace') as f:
|
||||||
@@ -131,7 +167,7 @@ class VBAImporter:
|
|||||||
print(f" [警告] 编码转换失败,尝试回退到 utf-8: {e}")
|
print(f" [警告] 编码转换失败,尝试回退到 utf-8: {e}")
|
||||||
with open(temp_file_path, 'w', encoding='utf-8') as f:
|
with open(temp_file_path, 'w', encoding='utf-8') as f:
|
||||||
f.write('\n'.join(content_lines))
|
f.write('\n'.join(content_lines))
|
||||||
|
|
||||||
return temp_file_path
|
return temp_file_path
|
||||||
|
|
||||||
def import_vba(self):
|
def import_vba(self):
|
||||||
@@ -140,17 +176,21 @@ class VBAImporter:
|
|||||||
print(f"错误: 找不到目标 Excel 文件: {self.target_file}")
|
print(f"错误: 找不到目标 Excel 文件: {self.target_file}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
if not self.vba_dir.exists():
|
||||||
|
print(f"错误: 找不到 VBA 代码目录: {self.vba_dir}")
|
||||||
|
return False
|
||||||
|
|
||||||
print(f"正在打开 Excel 文件: {self.target_file.name} ...")
|
print(f"正在打开 Excel 文件: {self.target_file.name} ...")
|
||||||
|
|
||||||
excel = None
|
excel = None
|
||||||
workbook = None
|
workbook = None
|
||||||
try:
|
try:
|
||||||
excel = win32.Dispatch("Excel.Application")
|
excel = win32.Dispatch("Excel.Application")
|
||||||
excel.Visible = False
|
excel.Visible = False
|
||||||
excel.DisplayAlerts = False
|
excel.DisplayAlerts = False
|
||||||
|
|
||||||
workbook = excel.Workbooks.Open(str(self.target_file))
|
workbook = excel.Workbooks.Open(str(self.target_file))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
vb_project = workbook.VBProject
|
vb_project = workbook.VBProject
|
||||||
except Exception:
|
except Exception:
|
||||||
@@ -158,22 +198,19 @@ class VBAImporter:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
print("开始导入模块...\n")
|
print("开始导入模块...\n")
|
||||||
|
|
||||||
modules = self.metadata.get("modules", {})
|
# 扫描所有模块
|
||||||
|
modules = self._scan_modules()
|
||||||
temp_files_created = []
|
temp_files_created = []
|
||||||
|
|
||||||
for file_key, info in modules.items():
|
if not modules:
|
||||||
raw_module_name = info["name"]
|
print("警告: 未找到任何 VBA 模块文件")
|
||||||
module_name = self._clean_component_name(raw_module_name)
|
return False
|
||||||
module_type_dir = info["type"] # e.g., "Modules", "ClassModules"
|
|
||||||
rel_path = info["file"]
|
for module_info in modules:
|
||||||
attributes = info.get("attributes", {})
|
module_name = module_info["name"]
|
||||||
|
module_type_dir = module_info["type"]
|
||||||
source_code_path = self.vba_dir / rel_path
|
source_code_path = module_info["file_path"]
|
||||||
|
|
||||||
if not source_code_path.exists():
|
|
||||||
print(f" [跳过] 找不到源文件: {rel_path}")
|
|
||||||
continue
|
|
||||||
|
|
||||||
component = None
|
component = None
|
||||||
try:
|
try:
|
||||||
@@ -182,8 +219,8 @@ class VBAImporter:
|
|||||||
component = None
|
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)
|
# 策略 A: 导入文件模式 (Modules, ClassModules)
|
||||||
# ---------------------------------------------------------
|
# ---------------------------------------------------------
|
||||||
@@ -193,33 +230,32 @@ class VBAImporter:
|
|||||||
vb_project.VBComponents.Remove(component)
|
vb_project.VBComponents.Remove(component)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f" [警告] 无法移除 {module_name}: {e},将尝试仅更新代码")
|
print(f" [警告] 无法移除 {module_name}: {e},将尝试仅更新代码")
|
||||||
is_reloadable = False
|
is_reloadable = False
|
||||||
|
|
||||||
if is_reloadable:
|
if is_reloadable:
|
||||||
# 生成临时导入文件
|
# 生成临时导入文件
|
||||||
# 【修改】传入 module_type_dir 以判断是否需要加 Class 头
|
temp_file = self._reconstruct_file_content(source_code_path, module_name, module_type_dir)
|
||||||
temp_file = self._reconstruct_file_content(source_code_path, attributes, module_name, module_type_dir)
|
|
||||||
temp_files_created.append(temp_file)
|
temp_files_created.append(temp_file)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
vb_project.VBComponents.Import(str(temp_file))
|
vb_project.VBComponents.Import(str(temp_file))
|
||||||
print(f" [导入] {module_name} ({module_type_dir})")
|
print(f" [导入] {module_name} ({module_type_dir})")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f" [错误] 导入 {module_name} 失败: {e}")
|
print(f" [错误] 导入 {module_name} 失败: {e}")
|
||||||
|
|
||||||
# ---------------------------------------------------------
|
# ---------------------------------------------------------
|
||||||
# 策略 B: 字符串注入模式 (Sheet, Workbook, Forms)
|
# 策略 B: 字符串注入模式 (Sheet, Workbook, Forms)
|
||||||
# ---------------------------------------------------------
|
# ---------------------------------------------------------
|
||||||
if not is_reloadable:
|
if not is_reloadable:
|
||||||
if not component:
|
if not component:
|
||||||
if module_type_dir == "Forms":
|
if module_type_dir == FORMS_DIR:
|
||||||
print(f" [警告] 无法恢复 UserForm '{module_name}',跳过。")
|
print(f" [警告] 无法恢复 UserForm '{module_name}',跳过。")
|
||||||
continue
|
continue
|
||||||
elif module_type_dir == "DocumentModules":
|
elif module_type_dir == DOCUMENT_MODULE_DIR:
|
||||||
print(f" [警告] 找不到文档对象 '{module_name}',跳过。")
|
print(f" [警告] 找不到文档对象 '{module_name}',跳过。")
|
||||||
continue
|
continue
|
||||||
try:
|
try:
|
||||||
component = vb_project.VBComponents.Add(1)
|
component = vb_project.VBComponents.Add(1)
|
||||||
component.Name = module_name
|
component.Name = module_name
|
||||||
except:
|
except:
|
||||||
print(f" [错误] 无法创建组件 {module_name}")
|
print(f" [错误] 无法创建组件 {module_name}")
|
||||||
@@ -230,14 +266,14 @@ class VBAImporter:
|
|||||||
num_lines = code_module.CountOfLines
|
num_lines = code_module.CountOfLines
|
||||||
if num_lines > 0:
|
if num_lines > 0:
|
||||||
code_module.DeleteLines(1, num_lines)
|
code_module.DeleteLines(1, num_lines)
|
||||||
|
|
||||||
# 直接读取 UTF-8 字符串到内存
|
# 直接读取 UTF-8 字符串到内存
|
||||||
with open(source_code_path, 'r', encoding='utf-8') as f:
|
with open(source_code_path, 'r', encoding='utf-8') as f:
|
||||||
new_code = f.read()
|
new_code = f.read()
|
||||||
|
|
||||||
if new_code.strip():
|
if new_code.strip():
|
||||||
code_module.AddFromString(new_code)
|
code_module.AddFromString(new_code)
|
||||||
|
|
||||||
print(f" [更新] {module_name} ({module_type_dir}) - 代码已更新")
|
print(f" [更新] {module_name} ({module_type_dir}) - 代码已更新")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f" [错误] 更新代码 {module_name} 失败: {e}")
|
print(f" [错误] 更新代码 {module_name} 失败: {e}")
|
||||||
@@ -267,7 +303,7 @@ class VBAImporter:
|
|||||||
import traceback
|
import traceback
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
return False
|
return False
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
if workbook:
|
if workbook:
|
||||||
try: workbook.Close(SaveChanges=False)
|
try: workbook.Close(SaveChanges=False)
|
||||||
@@ -276,69 +312,206 @@ class VBAImporter:
|
|||||||
try: excel.Quit()
|
try: excel.Quit()
|
||||||
except: pass
|
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():
|
def main():
|
||||||
print("=" * 60)
|
print("=" * 60)
|
||||||
print("VBA代码导入工具 (V3.0 最终版)")
|
print("VBA代码导入工具 (V5.0 - 支持Excel和Access)")
|
||||||
print("=" * 60)
|
print("=" * 60)
|
||||||
print()
|
print()
|
||||||
|
|
||||||
script_dir = Path(__file__).parent
|
script_dir = Path(__file__).parent
|
||||||
|
|
||||||
# 检查是否配置了目标元数据文件
|
# 检查配置
|
||||||
if TARGET_METADATA_FILE and TARGET_METADATA_FILE.strip():
|
if not TARGET_FILE:
|
||||||
# 使用配置的元数据文件路径
|
print("错误: 未配置 TARGET_FILE")
|
||||||
target_path = Path(TARGET_METADATA_FILE)
|
print("请在 .env 文件中设置目标文件路径(支持.xlsm和.accdb)")
|
||||||
|
return
|
||||||
|
|
||||||
# 如果是相对路径,则相对于脚本所在目录
|
# 确定目标文件路径
|
||||||
if not target_path.is_absolute():
|
target_path = Path(TARGET_FILE)
|
||||||
target_path = script_dir / target_path
|
if not target_path.is_absolute():
|
||||||
|
target_path = script_dir / target_path
|
||||||
|
|
||||||
if not target_path.exists():
|
if not target_path.exists():
|
||||||
print(f"错误: 配置的元数据文件不存在: {target_path}")
|
print(f"错误: 配置的文件不存在: {target_path}")
|
||||||
return
|
return
|
||||||
|
|
||||||
if not target_path.name == METADATA_FILE:
|
file_type = get_file_type(target_path)
|
||||||
print(f"警告: 文件名不是 {METADATA_FILE}: {target_path.name}")
|
|
||||||
|
|
||||||
metadata_path = target_path
|
# 确定 VBA 代码目录
|
||||||
print(f"使用配置的元数据文件: {metadata_path.name}")
|
if VBA_OUTPUT_DIR:
|
||||||
print()
|
vba_path = Path(VBA_OUTPUT_DIR)
|
||||||
|
if not vba_path.is_absolute():
|
||||||
|
vba_path = script_dir / vba_path
|
||||||
else:
|
else:
|
||||||
# 默认模式:查找脚本所在目录下的 VBA 文件夹
|
# 根据文件类型使用不同的默认文件夹
|
||||||
vba_dir = script_dir / VBA_DIR_NAME
|
default_dir = "VBA-Access" if file_type == "access" else "VBA-Excel"
|
||||||
metadata_path = vba_dir / METADATA_FILE
|
vba_path = target_path.parent / default_dir
|
||||||
|
|
||||||
if not metadata_path.exists():
|
if not vba_path.exists():
|
||||||
print(f"错误: 找不到元数据文件: {metadata_path}")
|
print(f"错误: VBA 代码目录不存在: {vba_path}")
|
||||||
print()
|
print()
|
||||||
print("提示:")
|
print("提示:")
|
||||||
print(" 1. 确保已运行 extract_vba.py 提取 VBA 代码")
|
print(" 1. 确保已运行 extract_vba.py 提取 VBA 代码")
|
||||||
print(" 2. 或在脚本顶部配置 TARGET_METADATA_FILE 指定元数据文件路径")
|
print(" 2. 或在 .env 文件中设置 VBA_OUTPUT_DIR 指定代码目录")
|
||||||
return
|
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("=" * 60)
|
||||||
print("警告: 此操作将覆盖目标 Excel 文件中的 VBA 代码。")
|
print(f"警告: 此操作将覆盖目标{type_label}文件中的 VBA 代码。")
|
||||||
choice = input("\n确认继续? (y/n): ").lower().strip()
|
choice = input("\n确认继续? (y/n): ").lower().strip()
|
||||||
|
|
||||||
if choice != 'y':
|
if choice != 'y':
|
||||||
print("操作已取消")
|
print("操作已取消")
|
||||||
return
|
return
|
||||||
|
|
||||||
importer = VBAImporter(str(metadata_path))
|
importer = VBAImporter(str(vba_path), str(target_path))
|
||||||
|
|
||||||
# 显示导入目标信息
|
# 显示模块数量
|
||||||
print()
|
modules = importer._scan_modules()
|
||||||
print(f"目标 Excel 文件: {importer.target_file.name}")
|
print(f"找到 {len(modules)} 个模块文件")
|
||||||
print(f"VBA 代码目录: {importer.vba_dir}")
|
|
||||||
print(f"模块数量: {len(importer.metadata.get('modules', {}))}")
|
|
||||||
print()
|
print()
|
||||||
print("=" * 60)
|
print("=" * 60)
|
||||||
print()
|
print()
|
||||||
|
|
||||||
success = importer.import_vba()
|
# 根据文件类型选择导入方法
|
||||||
|
if file_type == 'access':
|
||||||
|
success = importer.import_vba_access()
|
||||||
|
else:
|
||||||
|
success = importer.import_vba()
|
||||||
|
|
||||||
print()
|
print()
|
||||||
print("=" * 60)
|
print("=" * 60)
|
||||||
@@ -349,4 +522,4 @@ def main():
|
|||||||
print("=" * 60)
|
print("=" * 60)
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
|
|||||||
Reference in New Issue
Block a user