Compare commits

..

10 Commits

Author SHA1 Message Date
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
6 changed files with 978 additions and 0 deletions

18
.gitignore vendored Normal file
View File

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

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

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

139
CLAUDE.md Normal file
View File

@@ -0,0 +1,139 @@
# 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` files, manage it externally, and import it back into Excel.
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
├── VBA/ # Extracted VBA code - gitignored
│ ├── Modules/ # Standard modules (.bas)
│ ├── ClassModules/ # Class modules (.cls)
│ ├── DocumentModules/# Sheet/workbook modules (.cls)
│ ├── Forms/ # User forms
│ └── vba_metadata.json # Module metadata for import
├── extract_vba.py # Extract VBA from Excel files
├── import_vba.py # Import VBA back to Excel files
├── 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 automation (Windows only)
- `oletools>=0.60` - Alternative VBA extraction without Excel dependency
## Common Commands
### Extract VBA Code
```bash
# Interactive extraction - will prompt for file and method
python extract_vba.py
```
Two extraction methods available:
1. **COM Interface** (recommended) - Requires Microsoft Excel, more reliable
2. **olevba Library** - No Excel required, uses oletools
For COM method, ensure Excel trusts VBA access:
- Excel > Options > Trust Center > Trust Center Settings
- Check "Trust access to the VBA project object model"
### Import VBA Code
```bash
# Import from VBA/ directory back to Excel
python import_vba.py VBA/vba_metadata.json
```
## 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)
- 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)
### VBA Import (import_vba.py)
- Uses Windows COM to interact with Excel
- **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 COM-based extraction/import
- Cross-platform extraction possible with oletools (no Excel needed)
## Git Workflow
The `.gitignore` excludes:
- Virtual environment (`.venv/`)
- Build artifacts (`build/`, `dist/`)
- Project data (`Excel/`, `VBA/`)
- Claude temporary files (`.claude/`, `tmpclaude-*`)
Only commit code changes, not extracted VBA or Excel files.

453
extract_vba.py Normal file
View File

@@ -0,0 +1,453 @@
"""
VBA代码提取工具
从xlsm文件中提取模块和类模块代码分类保存到VBA文件夹
自动清理Attribute信息并生成元数据JSON文件
"""
import os
import sys
import json
import re
from pathlib import Path
from typing import Dict, List, Tuple, Optional
# 加载 .env 配置文件
try:
from dotenv import load_dotenv
load_dotenv()
except ImportError:
print("警告: 未安装 python-dotenv 库,将使用默认配置")
print("建议运行: pip install python-dotenv")
# ==================== 配置区域 ====================
# 从 .env 文件读取配置,如果未设置则使用 None交互模式
TARGET_XLSM_FILE = os.getenv("TARGET_XLSM_FILE", "").strip() or None
# =================================================
# VBA项目相关常量
STANDARD_MODULE_DIR = "Modules"
CLASS_MODULE_DIR = "ClassModules"
DOCUMENT_MODULE_DIR = "DocumentModules"
FORMS_DIR = "Forms"
METADATA_FILE = "vba_metadata.json"
class VBAExtractor:
"""VBA代码提取器"""
def __init__(self, xlsm_path: str, output_dir: str = None, use_same_dir: bool = True):
"""
初始化VBA提取器
Args:
xlsm_path: xlsm文件路径
output_dir: 输出目录当use_same_dir=False时有效
use_same_dir: 是否使用目标文件同目录下的VBA文件夹默认True
"""
self.xlsm_path = Path(xlsm_path)
if use_same_dir:
# 使用目标文件同目录下的VBA文件夹
self.output_dir = self.xlsm_path.parent / "VBA"
elif output_dir is None:
# 使用脚本所在目录项目根目录下的VBA文件夹
script_dir = Path(__file__).parent
self.output_dir = script_dir / "VBA"
else:
self.output_dir = Path(output_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)
# 存储模块元数据
self.metadata = {
"source_file": str(self.xlsm_path),
"modules": {}
}
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.xlsm_path.name}")
try:
vba_parser = VBA_Parser(str(self.xlsm_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()
# 保存元数据文件
self._save_metadata()
print(f"\n提取完成!")
print(f"- 标准模块: {self.modules_dir}")
print(f"- 类模块: {self.class_modules_dir}")
print(f"- 文档模块: {self.document_modules_dir}")
print(f"- 元数据: {self.output_dir / METADATA_FILE}")
return True
else:
print("未在文件中发现VBA代码")
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.xlsm_path.name}")
try:
excel = win32.Dispatch("Excel.Application")
excel.Visible = False
excel.DisplayAlerts = False
workbook = excel.Workbooks.Open(str(self.xlsm_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()
# 保存元数据文件
self._save_metadata()
print(f"\n提取完成!")
print(f"- 标准模块: {self.modules_dir}")
print(f"- 类模块: {self.class_modules_dir}")
print(f"- 文档模块: {self.document_modules_dir}")
print(f"- 元数据: {self.output_dir / METADATA_FILE}")
return True
except Exception as e:
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 _determine_module_type(self, module_name: str, stream_path: str) -> str:
"""
根据模块名称和流路径确定模块类型
Args:
module_name: 模块名称
stream_path: 流路径
Returns:
模块类型: Modules, ClassModules, DocumentModules
"""
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
# 根据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
# 默认为标准模块
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)
# 确定文件扩展名
ext = '.cls' if module_type in [CLASS_MODULE_DIR, DOCUMENT_MODULE_DIR, FORMS_DIR] else '.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)
# 保存元数据
self.metadata["modules"][clean_name] = {
"name": module_name,
"type": module_type,
"attributes": attributes,
"file": str(file_path.relative_to(self.output_dir))
}
print(f" [OK] 已保存: {clean_name} ({module_type})")
def _save_metadata(self):
"""保存元数据到JSON文件"""
metadata_path = self.output_dir / METADATA_FILE
with open(metadata_path, 'w', encoding='utf-8') as f:
json.dump(self.metadata, f, indent=2, ensure_ascii=False)
def main():
"""主函数"""
print("=" * 60)
print("VBA代码提取工具")
print("=" * 60)
print()
# 检查是否配置了目标文件
if TARGET_XLSM_FILE and TARGET_XLSM_FILE.strip():
# 使用配置的文件路径
script_dir = Path(__file__).parent
target_path = Path(TARGET_XLSM_FILE)
# 如果是相对路径,则相对于脚本所在目录
if not target_path.is_absolute():
target_path = script_dir / target_path
if not target_path.exists():
print(f"错误: 配置的文件不存在: {target_path}")
return
if not target_path.suffix.lower() == '.xlsm':
print(f"警告: 文件扩展名不是.xlsm: {target_path.name}")
xlsm_file = target_path
print(f"使用配置文件: {xlsm_file.name}")
print()
else:
# 交互模式查找xlsm文件
excel_dir = Path("Excel")
if not excel_dir.exists():
print("错误: 未找到Excel文件夹")
return
xlsm_files = list(excel_dir.glob("*.xlsm"))
if not xlsm_files:
print("错误: Excel文件夹中没有xlsm文件")
return
# 如果有多个文件,让用户选择
if len(xlsm_files) > 1:
print("发现多个xlsm文件:")
for i, f in enumerate(xlsm_files, 1):
print(f" {i}. {f.name}")
print()
choice = input("请选择文件编号 (直接回车选择第1个): ").strip()
if not choice:
xlsm_file = xlsm_files[0]
else:
try:
idx = int(choice) - 1
xlsm_file = xlsm_files[idx]
except:
print("无效选择,使用第一个文件")
xlsm_file = xlsm_files[0]
else:
xlsm_file = xlsm_files[0]
print()
print(f"选择文件: {xlsm_file.name}")
print()
# 创建提取器使用默认参数输出到目标文件同目录下的VBA文件夹
extractor = VBAExtractor(str(xlsm_file))
# 显示输出目录信息
print(f"输出目录: {extractor.output_dir}")
print()
# 选择提取方法
print("请选择提取方法:")
print(" 1. COM接口 (推荐 - 需要安装Excel)")
print(" 2. olevba库 (不需要Excel)")
print()
method = input("请选择 (直接回车使用方法1): ").strip()
if method == "2":
print("\n使用olevba库提取...")
success = extractor.extract_vba_modules_olevba()
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()

352
import_vba.py Normal file
View File

@@ -0,0 +1,352 @@
"""
VBA代码导入工具 (最终修正版)
1. 修复路径识别问题
2. 修复中文乱码问题 (GB18030)
3. 修复类模块(Class)被错误导入为标准模块的问题 (补全 VERSION 1.0 CLASS 头)
"""
import os
import sys
import json
import shutil
import tempfile
from pathlib import Path
from typing import Dict, Any
# 加载 .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 文件读取配置,如果未设置则使用 None默认模式
TARGET_METADATA_FILE = os.getenv("TARGET_METADATA_FILE", "").strip() or None
# =================================================
# 常量定义
METADATA_FILE = "vba_metadata.json"
VBA_DIR_NAME = "VBA"
class VBAImporter:
"""VBA代码导入器"""
def __init__(self, metadata_path: str):
self.metadata_path = Path(metadata_path).resolve()
self.vba_dir = self.metadata_path.parent
self.project_root = self.vba_dir.parent
with open(self.metadata_path, 'r', encoding='utf-8') as f:
self.metadata = json.load(f)
# 处理 Excel 文件路径
raw_path = self.metadata.get("source_file", "")
candidate_path = Path(raw_path)
if not candidate_path.is_absolute():
candidate_path = self.project_root / candidate_path
self.target_file = candidate_path
if not self.target_file.exists():
print(f"提示: 在路径 {self.target_file} 未找到文件,尝试搜索...")
excel_dir = self.project_root / "Excel"
if excel_dir.exists():
files = list(excel_dir.glob("*.xlsm"))
if files:
print(f" -> 找到替代文件: {files[0].name}")
self.target_file = files[0]
self.target_file = self.target_file.resolve()
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, attributes: Dict[str, str], 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 == "ClassModules":
content_lines.append("VERSION 1.0 CLASS")
content_lines.append("BEGIN")
content_lines.append(" MultiUse = -1 'True")
content_lines.append("END")
# 注意Attribute VB_Name 必须紧跟在 END 之后
# 2. 重建 Attribute VB_Name
content_lines.append(f'Attribute VB_Name = "{module_name}"')
# 3. 重建其他 Attribute
for key, value in attributes.items():
if key == "VB_Name":
continue
if value.lower() in ['true', 'false']:
line = f'Attribute {key} = {value}'
else:
line = f'Attribute {key} = "{value}"'
content_lines.append(line)
content_lines.append("")
content_lines.append(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
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.metadata.get("modules", {})
temp_files_created = []
for file_key, info in modules.items():
raw_module_name = info["name"]
module_name = self._clean_component_name(raw_module_name)
module_type_dir = info["type"] # e.g., "Modules", "ClassModules"
rel_path = info["file"]
attributes = info.get("attributes", {})
source_code_path = self.vba_dir / rel_path
if not source_code_path.exists():
print(f" [跳过] 找不到源文件: {rel_path}")
continue
component = None
try:
component = vb_project.VBComponents(module_name)
except:
component = None
# 标准模块和类模块支持删除重建
is_reloadable = module_type_dir in ["Modules", "ClassModules"]
# ---------------------------------------------------------
# 策略 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:
# 生成临时导入文件
# 【修改】传入 module_type_dir 以判断是否需要加 Class 头
temp_file = self._reconstruct_file_content(source_code_path, attributes, 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":
print(f" [警告] 无法恢复 UserForm '{module_name}',跳过。")
continue
elif module_type_dir == "DocumentModules":
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 main():
print("=" * 60)
print("VBA代码导入工具 (V3.0 最终版)")
print("=" * 60)
print()
script_dir = Path(__file__).parent
# 检查是否配置了目标元数据文件
if TARGET_METADATA_FILE and TARGET_METADATA_FILE.strip():
# 使用配置的元数据文件路径
target_path = Path(TARGET_METADATA_FILE)
# 如果是相对路径,则相对于脚本所在目录
if not target_path.is_absolute():
target_path = script_dir / target_path
if not target_path.exists():
print(f"错误: 配置的元数据文件不存在: {target_path}")
return
if not target_path.name == METADATA_FILE:
print(f"警告: 文件名不是 {METADATA_FILE}: {target_path.name}")
metadata_path = target_path
print(f"使用配置的元数据文件: {metadata_path.name}")
print()
else:
# 默认模式:查找脚本所在目录下的 VBA 文件夹
vba_dir = script_dir / VBA_DIR_NAME
metadata_path = vba_dir / METADATA_FILE
if not metadata_path.exists():
print(f"错误: 找不到元数据文件: {metadata_path}")
print()
print("提示:")
print(" 1. 确保已运行 extract_vba.py 提取 VBA 代码")
print(" 2. 或在脚本顶部配置 TARGET_METADATA_FILE 指定元数据文件路径")
return
print(f"读取元数据: {metadata_path}")
# 显示目标文件信息
print("=" * 60)
print("警告: 此操作将覆盖目标 Excel 文件中的 VBA 代码。")
choice = input("\n确认继续? (y/n): ").lower().strip()
if choice != 'y':
print("操作已取消")
return
importer = VBAImporter(str(metadata_path))
# 显示导入目标信息
print()
print(f"目标 Excel 文件: {importer.target_file.name}")
print(f"VBA 代码目录: {importer.vba_dir}")
print(f"模块数量: {len(importer.metadata.get('modules', {}))}")
print()
print("=" * 60)
print()
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