Compare commits
10 Commits
f40c2f01fc
...
3569c76e53
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3569c76e53 | ||
|
|
69b92884a1 | ||
|
|
d8e843001b | ||
|
|
5bc1dbc3f5 | ||
|
|
4b8ba2d144 | ||
|
|
33ae440f6e | ||
|
|
c80e03e1ea | ||
|
|
7d1d41eb66 | ||
|
|
75c90937fa | ||
|
|
8f3d015ed2 |
18
.gitignore
vendored
Normal file
18
.gitignore
vendored
Normal 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
6
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
// 将.cls文件关联到Visual Basic语法高亮
|
||||
"files.associations": {
|
||||
"*.cls": "vb"
|
||||
}
|
||||
}
|
||||
139
CLAUDE.md
Normal file
139
CLAUDE.md
Normal 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
453
extract_vba.py
Normal 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
352
import_vba.py
Normal 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
10
requirements.txt
Normal file
@@ -0,0 +1,10 @@
|
||||
# VBA提取工具依赖
|
||||
|
||||
# COM接口方法 (推荐) - 需要安装Microsoft Excel
|
||||
pywin32>=306; sys_platform == 'win32'
|
||||
|
||||
# olevba库方法 - 不需要Excel
|
||||
oletools>=0.60
|
||||
|
||||
# 环境变量管理
|
||||
python-dotenv>=1.0.0
|
||||
Reference in New Issue
Block a user