Compare commits

...

19 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
Misaka_Company
3569c76e53 fix: skip empty VBA modules during extraction to avoid creating unnecessary files
- Add check in _process_module() to detect modules with no actual code content
- Display skip message for empty modules instead of creating files
- Prevent empty modules from being added to metadata

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-28 11:24:01 +08:00
Misaka_Company
69b92884a1 refactor: migrate configuration to .env file for better environment management
- Replace hardcoded file paths with environment variables in extract_vba.py and import_vba.py
- Add .env file for local configuration (excluded from git via .gitignore)
- Add python-dotenv dependency for environment variable loading
- Maintain fallback to interactive mode when environment variables are not set

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-28 11:20:50 +08:00
Misaka_Company
d8e843001b add: enhance metadata file handling in import_vba.py for improved path configuration and error messages 2026-01-20 08:41:34 +08:00
Misaka_Company
5bc1dbc3f5 add: enhance TARGET_XLSM_FILE configuration and improve file selection process in extract_vba.py 2026-01-20 08:38:19 +08:00
Misaka_Company
4b8ba2d144 add: create CLAUDE.md for project documentation and guidance on VBA code extraction and management 2026-01-19 17:39:00 +08:00
Misaka_Company
33ae440f6e fix: improve line handling in VBA code extraction to avoid extra blank lines 2026-01-19 17:31:07 +08:00
Misaka_Company
c80e03e1ea add import_vba.py for VBA code import tool with path recognition and encoding fixes 2026-01-19 17:06:15 +08:00
Misaka_Company
7d1d41eb66 add settings.json to associate .cls files with Visual Basic syntax highlighting 2026-01-19 16:19:02 +08:00
Misaka_Company
75c90937fa add extract_vba.py for VBA code extraction and update requirements.txt 2026-01-19 16:13:52 +08:00
Misaka_Company
8f3d015ed2 add .gitignore to exclude build artifacts and temporary files 2026-01-19 15:46:54 +08:00
8 changed files with 1915 additions and 0 deletions

19
.gitignore vendored Normal file
View File

@@ -0,0 +1,19 @@
__pycache__/
.venv
build
dist
log
*.spec
temp
.env
# Claude 临时文件
.claude/
tmpclaude-*
*.log
*workspace*
*.png
data/
Excel/
Access/
VBA/

6
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,6 @@
{
// 将.cls文件关联到Visual Basic语法高亮
"files.associations": {
"*.cls": "vb"
}
}

145
CLAUDE.md Normal file
View File

@@ -0,0 +1,145 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## 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` (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
- **clsCategory**: Represents material categories with hierarchical parent-child relationships
- **clsMaterialItem**: Represents individual materials with code, name, quantity, and selection conditions
## Directory Structure
```
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/Access files
├── import_vba.py # Import VBA back to Excel/Access files
├── main.py # Empty placeholder
└── requirements.txt # Python dependencies
```
## Development Setup
```bash
# Create and activate virtual environment
python -m venv .venv
.venv\Scripts\activate # Windows
# Install dependencies
pip install -r requirements.txt
```
**Dependencies:**
- `pywin32>=306` - Windows COM interface for Excel/Access automation (Windows only)
- `oletools>=0.60` - Alternative VBA extraction without Excel dependency (Excel only)
## Common Commands
### Extract VBA Code
```bash
# Interactive extraction - will prompt for file and method
python extract_vba.py
```
**Excel (.xlsm)** - Two extraction methods available:
1. **COM Interface** (recommended) - Requires Microsoft Excel, more reliable
2. **olevba Library** - No Excel required, uses oletools
**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 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
The VBA system implements a hierarchical category-based material management:
1. **Two-source loading pattern**:
- `[平台配置清单]` sheet: Contains all material info (code, name, quantity, condition)
- `[领料配置]` sheet: Defines categories and which materials require picking
2. **Category hierarchy**:
- Materials organized in parent-child category relationships
- `useParent=True`: Pick assembled components from parent category (default)
- `useParent=False`: Pick individual parts from child categories (fallback when stock insufficient)
3. **Data structures**:
- `dictCategories`: Dictionary for fast category lookup by name
- `dictAllMaterials`: Dictionary for fast material lookup by code
- `rootCategories`: Collection of top-level categories for tree traversal
### Module Types
- **Modules**: Standard VBA modules (`.bas` files)
- **ClassModules**: Class definitions (`.cls` files) - clsBOMManager, clsCategory, clsMaterialItem
- **DocumentModules**: Sheet and workbook code-behind (`.cls` files)
- **Forms**: UserForm definitions
## Important Implementation Details
### VBA Extraction (extract_vba.py)
- Cleans `Attribute` statements from exported code for readability
- Automatically categorizes modules by type (Standard/Class/Document/Form)
- 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 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
- Two import strategies:
- **Modules/ClassModules**: Remove and re-import via file
- **DocumentModules/Forms**: Update code in-place via string injection
### Module Naming Convention
The code determines module type by naming prefix:
- `mod_*` or `mod*` → Standard Modules
- `cls*` or `class*` → Class Modules
- `sheet*` or `thisworkbook` → Document Modules
## VS Code Configuration
The `.vscode/settings.json` associates `.cls` files with Visual Basic syntax highlighting for better editing experience.
## Platform Requirements
- **Windows required** for import functionality (COM interface)
- **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/`, `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"
```

545
extract_vba.py Normal file
View File

@@ -0,0 +1,545 @@
"""
VBA代码提取工具
从Excel(.xlsm)或Access(.accdb)文件中提取VBA代码分类保存到VBA文件夹
自动清理Attribute信息
"""
import os
import sys
import re
from pathlib import Path
from typing import Tuple, Dict
# 加载 .env 配置文件
try:
from dotenv import load_dotenv
load_dotenv()
except ImportError:
print("警告: 未安装 python-dotenv 库,将使用默认配置")
print("建议运行: pip install python-dotenv")
# ==================== 配置区域 ====================
# 从 .env 文件读取配置,如果未设置则使用 None交互模式
TARGET_FILE = os.getenv("TARGET_FILE", "").strip() or None
# VBA代码输出目录如果未设置则使用源文件同目录下的VBA文件夹
VBA_OUTPUT_DIR = os.getenv("VBA_OUTPUT_DIR", "").strip() or None
# =================================================
# 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 VBAExtractor:
"""VBA代码提取器"""
def __init__(self, source_path: str, output_dir: str = None):
"""
初始化VBA提取器
Args:
source_path: 源文件路径(支持.xlsm/.accdb等
output_dir: 输出目录如果未指定则使用VBA_OUTPUT_DIR配置或源文件同目录
"""
self.source_path = Path(source_path)
# 确定输出目录的优先级:
# 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
self.class_modules_dir = self.output_dir / CLASS_MODULE_DIR
self.document_modules_dir = self.output_dir / DOCUMENT_MODULE_DIR
self.forms_dir = self.output_dir / FORMS_DIR
self.modules_dir.mkdir(parents=True, exist_ok=True)
self.class_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)
def parse_attributes(self, code: str) -> Tuple[Dict[str, str], str]:
"""
解析VBA代码中的Attribute信息
Args:
code: VBA代码包含Attribute行
Returns:
(attributes_dict, clean_code) - 属性字典和清理后的代码
"""
attributes = {}
# [修复] 使用 splitlines() 自动处理 \r\n避免保留 \r 导致的多余空行
lines = code.splitlines()
clean_lines = []
in_attributes = True
for line in lines:
# 检查是否为Attribute行
attr_match = re.match(r'^Attribute\s+(\w+)\s*=\s*(.+)$', line.strip())
if attr_match:
attr_name = attr_match.group(1)
attr_value = attr_match.group(2).strip().strip('"')
attributes[attr_name] = attr_value
# 继续收集Attribute暂不添加到clean_lines
continue
# 遇到非Attribute行Attribute收集结束
if not line.strip().startswith('Attribute'):
in_attributes = False
# 添加到清理后的代码跳过空行和Attribute
if not in_attributes or (line.strip() and not line.strip().startswith('Attribute')):
if not in_attributes:
# 使用 rstrip() 去除行尾可能存在的空白符,保持代码整洁
clean_lines.append(line.rstrip())
# 去除开头的空行
while clean_lines and not clean_lines[0].strip():
clean_lines.pop(0)
clean_code = '\n'.join(clean_lines)
return attributes, clean_code
def extract_vba_modules_olevba(self):
"""
使用olevba库提取VBA代码
需要安装: pip install oletools
"""
try:
from oletools.olevba import VBA_Parser
except ImportError:
print("错误: 未安装oletools库")
print("请运行: pip install oletools")
return False
print(f"正在解析文件: {self.source_path.name}")
try:
vba_parser = VBA_Parser(str(self.source_path))
if vba_parser.detect_vba_macros():
print("发现VBA代码开始提取...\n")
# 遍历所有VBA模块
for (filename, stream_path, vba_filename, vba_code) in vba_parser.extract_macros():
self._process_module(vba_filename, vba_code, stream_path)
vba_parser.close()
print(f"\n提取完成!")
print(f"- 标准模块: {self.modules_dir}")
print(f"- 类模块: {self.class_modules_dir}")
print(f"- 文档模块: {self.document_modules_dir}")
return True
else:
print("未在文件中发现VBA代码")
vba_parser.close()
return False
except Exception as e:
print(f"提取VBA代码时出错: {e}")
return False
def extract_vba_modules_com(self):
"""
使用COM接口提取VBA代码需要安装Excel
优点: 更可靠,支持更多特性
缺点: 需要安装Microsoft Excel
"""
try:
import win32com.client as win32
except ImportError:
print("错误: 未安装pywin32库")
print("请运行: pip install pywin32")
return False
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.source_path.absolute()))
# 获取VBA项目
if not workbook.VBProject:
print("错误: 无法访问VBA项目")
print("请确保: 1) Excel信任中心设置'信任对VBA工程对象模型的访问'")
print(" 2) 文件中包含VBA代码")
workbook.Close(False)
excel.Quit()
return False
vb_project = workbook.VBProject
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 = ""
# 根据类型分类保存
# 1=标准模块, 2=类模块, 3=MSForm, 11=Document/工作表/工作簿
type_name = {
1: STANDARD_MODULE_DIR,
2: CLASS_MODULE_DIR,
3: FORMS_DIR,
11: DOCUMENT_MODULE_DIR
}.get(module_type, "Unknown")
target_dir = {
1: self.modules_dir,
2: self.class_modules_dir,
3: self.forms_dir,
11: self.document_modules_dir
}.get(module_type, self.modules_dir)
target_dir.mkdir(exist_ok=True)
self._process_module(module_name, vba_code, type_name)
workbook.Close(False)
excel.Quit()
print(f"\n提取完成!")
print(f"- 标准模块: {self.modules_dir}")
print(f"- 类模块: {self.class_modules_dir}")
print(f"- 文档模块: {self.document_modules_dir}")
return True
except Exception as e:
print(f"使用COM提取VBA代码时出错: {e}")
print("\n提示:")
print("1. 确保已安装Microsoft Excel")
print("2. 打开Excel -> 文件 -> 选项 -> 信任中心 -> 信任中心设置")
print("3. 勾选'信任对VBA工程对象模型的访问'")
try:
excel.Quit()
except:
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:
"""
根据模块名称和流路径确定模块类型
Args:
module_name: 模块名称
stream_path: 流路径
Returns:
模块类型: Modules, ClassModules, DocumentModules, Forms
"""
name_lower = module_name.lower()
# 工作表和工作簿模块
if name_lower.startswith('sheet') or name_lower == 'thisworkbook':
return DOCUMENT_MODULE_DIR
# 标准模块
if name_lower.startswith('mod_') or name_lower.startswith('mod'):
return STANDARD_MODULE_DIR
# 类模块
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()
if 'sheet' in path_lower or 'thisworkbook' in path_lower:
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
def _process_module(self, module_name: str, vba_code: str, category: str):
"""
处理模块:解析属性、清理代码、保存文件
Args:
module_name: 模块名称
vba_code: VBA代码内容
category: 模块类别可能是stream_path或类型名称
"""
# 解析Attribute信息
attributes, clean_code = self.parse_attributes(vba_code)
# 检查是否有实际代码内容(跳过空模块)
if not clean_code.strip():
print(f" [跳过] {module_name} - 无实际代码内容")
return
# 确定实际的模块类型
module_type = self._determine_module_type(module_name, category)
# 确定目标目录
target_dir = {
STANDARD_MODULE_DIR: self.modules_dir,
CLASS_MODULE_DIR: self.class_modules_dir,
DOCUMENT_MODULE_DIR: self.document_modules_dir,
FORMS_DIR: self.forms_dir
}.get(module_type, self.modules_dir)
# 确定文件扩展名
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('\\', '_')
# 移除已存在的扩展名
for suffix in ['.cls', '.bas', '.frm']:
if clean_name.endswith(suffix):
clean_name = clean_name[:-len(suffix)]
break
clean_name += ext
# 保存清理后的代码
file_path = target_dir / clean_name
with open(file_path, 'w', encoding='utf-8') as f:
f.write(clean_code)
print(f" [OK] 已保存: {clean_name} ({module_type})")
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:
# 交互模式:查找支持的文件
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))
if not all_files:
print("错误: 未找到.xlsm或.accdb文件请检查Excel/或Access/文件夹)")
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)
if __name__ == "__main__":
main()

525
import_vba.py Normal file
View File

@@ -0,0 +1,525 @@
"""
VBA代码导入工具
使用 .env 配置直接导入 VBA 代码,无需元数据文件
"""
import os
import sys
import shutil
import tempfile
from pathlib import Path
from typing import Dict, List, Tuple
# 加载 .env 配置文件
try:
from dotenv import load_dotenv
load_dotenv()
except ImportError:
print("警告: 未安装 python-dotenv 库,将使用默认配置")
print("建议运行: pip install python-dotenv")
try:
import win32com.client as win32
except ImportError:
print("错误: 未安装 pywin32 库")
print("请运行: pip install pywin32")
sys.exit(1)
# ==================== 配置区域 ====================
# 从 .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
# =================================================
# 常量定义
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, vba_dir: str, target_file: str):
"""
初始化VBA导入器
Args:
vba_dir: VBA代码目录包含 Modules, ClassModules 等子目录)
target_file: 目标 Excel 文件路径
"""
self.vba_dir = Path(vba_dir).resolve()
self.target_file = Path(target_file).resolve()
def _scan_modules(self) -> List[Dict]:
"""
扫描 VBA 目录,收集所有模块信息
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:
"""去除模块名称中的扩展名"""
lower_name = name.lower()
for ext in ['.cls', '.bas', '.frm']:
if lower_name.endswith(ext):
return name[:-len(ext)]
return name
def _reconstruct_file_content(self, code_path: Path, module_name: str, module_type: str) -> Path:
"""
读取纯代码文件,重建完整的导入文件
关键逻辑:
1. ClassModules 需要 'VERSION 1.0 CLASS' 头部,否则会被识别为标准模块。
2. 使用 GB18030 编码写入,防止中文乱码。
"""
# 1. 读取源代码 (UTF-8)
with open(code_path, 'r', encoding='utf-8') as f:
code_body = f.read()
# 创建临时文件
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 == CLASS_MODULE_DIR:
content_lines.append("VERSION 1.0 CLASS")
content_lines.append("BEGIN")
content_lines.append(" MultiUse = -1 'True")
content_lines.append("END")
# 2. 重建 Attribute VB_Name
content_lines.append(f'Attribute VB_Name = "{module_name}"')
# 注意:由于我们移除了元数据,不再有其他属性信息
# 如果需要其他属性,需要从源文件中解析或在代码中显式声明
content_lines.append("")
content_lines.append(code_body)
# 4. 写入临时文件 (GB18030 防止乱码)
try:
with open(temp_file_path, 'w', encoding='gb18030', errors='replace') as f:
f.write('\n'.join(content_lines))
except Exception as e:
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):
"""执行导入过程"""
if not self.target_file.exists():
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:
print("错误: 无法访问 VBA 项目。请确保信任对 VBA 工程对象模型的访问。")
return False
print("开始导入模块...\n")
# 扫描所有模块
modules = self._scan_modules()
temp_files_created = []
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:
component = vb_project.VBComponents(module_name)
except:
component = None
# 标准模块和类模块支持删除重建
is_reloadable = module_type_dir in [STANDARD_MODULE_DIR, CLASS_MODULE_DIR]
# ---------------------------------------------------------
# 策略 A: 导入文件模式 (Modules, ClassModules)
# ---------------------------------------------------------
if is_reloadable:
if component:
try:
vb_project.VBComponents.Remove(component)
except Exception as e:
print(f" [警告] 无法移除 {module_name}: {e},将尝试仅更新代码")
is_reloadable = False
if is_reloadable:
# 生成临时导入文件
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_DIR:
print(f" [警告] 无法恢复 UserForm '{module_name}',跳过。")
continue
elif module_type_dir == DOCUMENT_MODULE_DIR:
print(f" [警告] 找不到文档对象 '{module_name}',跳过。")
continue
try:
component = vb_project.VBComponents.Add(1)
component.Name = module_name
except:
print(f" [错误] 无法创建组件 {module_name}")
continue
try:
code_module = component.CodeModule
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}")
# 清理
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正在编译 VBA 项目...")
try:
workbook.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 workbook:
try: workbook.Close(SaveChanges=False)
except: pass
if excel:
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代码导入工具 (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:
# 根据文件类型使用不同的默认文件夹
default_dir = "VBA-Access" if file_type == "access" else "VBA-Excel"
vba_path = target_path.parent / default_dir
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)
if __name__ == "__main__":
main()

10
requirements.txt Normal file
View File

@@ -0,0 +1,10 @@
# VBA提取工具依赖
# COM接口方法 (推荐) - 需要安装Microsoft Excel
pywin32>=306; sys_platform == 'win32'
# olevba库方法 - 不需要Excel
oletools>=0.60
# 环境变量管理
python-dotenv>=1.0.0