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>
This commit is contained in:
256
import_vba.py
256
import_vba.py
@@ -1,17 +1,14 @@
|
|||||||
"""
|
"""
|
||||||
VBA代码导入工具 (最终修正版)
|
VBA代码导入工具
|
||||||
1. 修复路径识别问题
|
使用 .env 配置直接导入 VBA 代码,无需元数据文件
|
||||||
2. 修复中文乱码问题 (GB18030)
|
|
||||||
3. 修复类模块(Class)被错误导入为标准模块的问题 (补全 VERSION 1.0 CLASS 头)
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
import json
|
|
||||||
import shutil
|
import shutil
|
||||||
import tempfile
|
import tempfile
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Dict, Any
|
from typing import Dict, List, Tuple
|
||||||
|
|
||||||
# 加载 .env 配置文件
|
# 加载 .env 配置文件
|
||||||
try:
|
try:
|
||||||
@@ -29,44 +26,68 @@ except ImportError:
|
|||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
# ==================== 配置区域 ====================
|
# ==================== 配置区域 ====================
|
||||||
# 从 .env 文件读取配置,如果未设置则使用 None(默认模式)
|
# 从 .env 文件读取配置
|
||||||
TARGET_METADATA_FILE = os.getenv("TARGET_METADATA_FILE", "").strip() or None
|
# 目标 xlsm 文件路径(用于导入VBA代码)
|
||||||
|
TARGET_XLSM_FILE = os.getenv("TARGET_XLSM_FILE", "").strip() or None
|
||||||
|
# VBA代码输出目录(如果未设置,则使用源文件同目录下的VBA文件夹)
|
||||||
|
VBA_OUTPUT_DIR = os.getenv("VBA_OUTPUT_DIR", "").strip() or None
|
||||||
# =================================================
|
# =================================================
|
||||||
|
|
||||||
# 常量定义
|
# 常量定义
|
||||||
METADATA_FILE = "vba_metadata.json"
|
STANDARD_MODULE_DIR = "Modules"
|
||||||
VBA_DIR_NAME = "VBA"
|
CLASS_MODULE_DIR = "ClassModules"
|
||||||
|
DOCUMENT_MODULE_DIR = "DocumentModules"
|
||||||
|
FORMS_DIR = "Forms"
|
||||||
|
|
||||||
class VBAImporter:
|
class VBAImporter:
|
||||||
"""VBA代码导入器"""
|
"""VBA代码导入器"""
|
||||||
|
|
||||||
def __init__(self, metadata_path: str):
|
def __init__(self, vba_dir: str, target_file: str):
|
||||||
self.metadata_path = Path(metadata_path).resolve()
|
"""
|
||||||
self.vba_dir = self.metadata_path.parent
|
初始化VBA导入器
|
||||||
self.project_root = self.vba_dir.parent
|
|
||||||
|
|
||||||
with open(self.metadata_path, 'r', encoding='utf-8') as f:
|
|
||||||
self.metadata = json.load(f)
|
|
||||||
|
|
||||||
# 处理 Excel 文件路径
|
|
||||||
raw_path = self.metadata.get("source_file", "")
|
|
||||||
candidate_path = Path(raw_path)
|
|
||||||
|
|
||||||
if not candidate_path.is_absolute():
|
Args:
|
||||||
candidate_path = self.project_root / candidate_path
|
vba_dir: VBA代码目录(包含 Modules, ClassModules 等子目录)
|
||||||
|
target_file: 目标 Excel 文件路径
|
||||||
|
"""
|
||||||
|
self.vba_dir = Path(vba_dir).resolve()
|
||||||
|
self.target_file = Path(target_file).resolve()
|
||||||
|
|
||||||
self.target_file = candidate_path
|
def _scan_modules(self) -> List[Dict]:
|
||||||
|
"""
|
||||||
|
扫描 VBA 目录,收集所有模块信息
|
||||||
|
|
||||||
if not self.target_file.exists():
|
Returns:
|
||||||
print(f"提示: 在路径 {self.target_file} 未找到文件,尝试搜索...")
|
模块信息列表,每个元素包含:
|
||||||
excel_dir = self.project_root / "Excel"
|
- name: 模块名称
|
||||||
if excel_dir.exists():
|
- type: 模块类型目录
|
||||||
files = list(excel_dir.glob("*.xlsm"))
|
- file_path: 源文件完整路径
|
||||||
if files:
|
- ext: 文件扩展名
|
||||||
print(f" -> 找到替代文件: {files[0].name}")
|
"""
|
||||||
self.target_file = files[0]
|
modules = []
|
||||||
|
|
||||||
self.target_file = self.target_file.resolve()
|
# 定义扫描目录和对应的扩展名
|
||||||
|
scan_dirs = [
|
||||||
|
(STANDARD_MODULE_DIR, ".bas"),
|
||||||
|
(CLASS_MODULE_DIR, ".cls"),
|
||||||
|
(DOCUMENT_MODULE_DIR, ".cls"),
|
||||||
|
(FORMS_DIR, ".frm"),
|
||||||
|
]
|
||||||
|
|
||||||
|
for dir_name, ext in scan_dirs:
|
||||||
|
dir_path = self.vba_dir / dir_name
|
||||||
|
if not dir_path.exists():
|
||||||
|
continue
|
||||||
|
|
||||||
|
for file_path in dir_path.glob(f"*{ext}"):
|
||||||
|
modules.append({
|
||||||
|
"name": file_path.stem, # 文件名不含扩展名
|
||||||
|
"type": dir_name,
|
||||||
|
"file_path": file_path,
|
||||||
|
"ext": ext
|
||||||
|
})
|
||||||
|
|
||||||
|
return modules
|
||||||
|
|
||||||
def _clean_component_name(self, name: str) -> str:
|
def _clean_component_name(self, name: str) -> str:
|
||||||
"""去除模块名称中的扩展名"""
|
"""去除模块名称中的扩展名"""
|
||||||
@@ -76,7 +97,7 @@ class VBAImporter:
|
|||||||
return name[:-len(ext)]
|
return name[:-len(ext)]
|
||||||
return name
|
return name
|
||||||
|
|
||||||
def _reconstruct_file_content(self, code_path: Path, attributes: Dict[str, str], module_name: str, module_type: str) -> Path:
|
def _reconstruct_file_content(self, code_path: Path, module_name: str, module_type: str) -> Path:
|
||||||
"""
|
"""
|
||||||
读取纯代码文件,重建完整的导入文件
|
读取纯代码文件,重建完整的导入文件
|
||||||
关键逻辑:
|
关键逻辑:
|
||||||
@@ -90,39 +111,31 @@ class VBAImporter:
|
|||||||
# 创建临时文件
|
# 创建临时文件
|
||||||
temp_dir = Path(tempfile.gettempdir()) / "vba_import_temp"
|
temp_dir = Path(tempfile.gettempdir()) / "vba_import_temp"
|
||||||
temp_dir.mkdir(exist_ok=True)
|
temp_dir.mkdir(exist_ok=True)
|
||||||
|
|
||||||
# 确定扩展名
|
# 确定扩展名
|
||||||
orig_ext = code_path.suffix
|
orig_ext = code_path.suffix
|
||||||
temp_file_path = temp_dir / f"{module_name}{orig_ext}"
|
temp_file_path = temp_dir / f"{module_name}{orig_ext}"
|
||||||
|
|
||||||
content_lines = []
|
content_lines = []
|
||||||
|
|
||||||
# -----------------------------------------------------------
|
# -----------------------------------------------------------
|
||||||
# 【关键修复】如果是类模块,必须添加 VERSION 头部块
|
# 【关键修复】如果是类模块,必须添加 VERSION 头部块
|
||||||
# -----------------------------------------------------------
|
# -----------------------------------------------------------
|
||||||
if module_type == "ClassModules":
|
if module_type == CLASS_MODULE_DIR:
|
||||||
content_lines.append("VERSION 1.0 CLASS")
|
content_lines.append("VERSION 1.0 CLASS")
|
||||||
content_lines.append("BEGIN")
|
content_lines.append("BEGIN")
|
||||||
content_lines.append(" MultiUse = -1 'True")
|
content_lines.append(" MultiUse = -1 'True")
|
||||||
content_lines.append("END")
|
content_lines.append("END")
|
||||||
# 注意:Attribute VB_Name 必须紧跟在 END 之后
|
|
||||||
|
|
||||||
# 2. 重建 Attribute VB_Name
|
# 2. 重建 Attribute VB_Name
|
||||||
content_lines.append(f'Attribute VB_Name = "{module_name}"')
|
content_lines.append(f'Attribute VB_Name = "{module_name}"')
|
||||||
|
|
||||||
# 3. 重建其他 Attribute
|
# 注意:由于我们移除了元数据,不再有其他属性信息
|
||||||
for key, value in attributes.items():
|
# 如果需要其他属性,需要从源文件中解析或在代码中显式声明
|
||||||
if key == "VB_Name":
|
|
||||||
continue
|
content_lines.append("")
|
||||||
if value.lower() in ['true', 'false']:
|
|
||||||
line = f'Attribute {key} = {value}'
|
|
||||||
else:
|
|
||||||
line = f'Attribute {key} = "{value}"'
|
|
||||||
content_lines.append(line)
|
|
||||||
|
|
||||||
content_lines.append("")
|
|
||||||
content_lines.append(code_body)
|
content_lines.append(code_body)
|
||||||
|
|
||||||
# 4. 写入临时文件 (GB18030 防止乱码)
|
# 4. 写入临时文件 (GB18030 防止乱码)
|
||||||
try:
|
try:
|
||||||
with open(temp_file_path, 'w', encoding='gb18030', errors='replace') as f:
|
with open(temp_file_path, 'w', encoding='gb18030', errors='replace') as f:
|
||||||
@@ -131,7 +144,7 @@ class VBAImporter:
|
|||||||
print(f" [警告] 编码转换失败,尝试回退到 utf-8: {e}")
|
print(f" [警告] 编码转换失败,尝试回退到 utf-8: {e}")
|
||||||
with open(temp_file_path, 'w', encoding='utf-8') as f:
|
with open(temp_file_path, 'w', encoding='utf-8') as f:
|
||||||
f.write('\n'.join(content_lines))
|
f.write('\n'.join(content_lines))
|
||||||
|
|
||||||
return temp_file_path
|
return temp_file_path
|
||||||
|
|
||||||
def import_vba(self):
|
def import_vba(self):
|
||||||
@@ -140,17 +153,21 @@ class VBAImporter:
|
|||||||
print(f"错误: 找不到目标 Excel 文件: {self.target_file}")
|
print(f"错误: 找不到目标 Excel 文件: {self.target_file}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
if not self.vba_dir.exists():
|
||||||
|
print(f"错误: 找不到 VBA 代码目录: {self.vba_dir}")
|
||||||
|
return False
|
||||||
|
|
||||||
print(f"正在打开 Excel 文件: {self.target_file.name} ...")
|
print(f"正在打开 Excel 文件: {self.target_file.name} ...")
|
||||||
|
|
||||||
excel = None
|
excel = None
|
||||||
workbook = None
|
workbook = None
|
||||||
try:
|
try:
|
||||||
excel = win32.Dispatch("Excel.Application")
|
excel = win32.Dispatch("Excel.Application")
|
||||||
excel.Visible = False
|
excel.Visible = False
|
||||||
excel.DisplayAlerts = False
|
excel.DisplayAlerts = False
|
||||||
|
|
||||||
workbook = excel.Workbooks.Open(str(self.target_file))
|
workbook = excel.Workbooks.Open(str(self.target_file))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
vb_project = workbook.VBProject
|
vb_project = workbook.VBProject
|
||||||
except Exception:
|
except Exception:
|
||||||
@@ -158,22 +175,19 @@ class VBAImporter:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
print("开始导入模块...\n")
|
print("开始导入模块...\n")
|
||||||
|
|
||||||
modules = self.metadata.get("modules", {})
|
# 扫描所有模块
|
||||||
|
modules = self._scan_modules()
|
||||||
temp_files_created = []
|
temp_files_created = []
|
||||||
|
|
||||||
for file_key, info in modules.items():
|
if not modules:
|
||||||
raw_module_name = info["name"]
|
print("警告: 未找到任何 VBA 模块文件")
|
||||||
module_name = self._clean_component_name(raw_module_name)
|
return False
|
||||||
module_type_dir = info["type"] # e.g., "Modules", "ClassModules"
|
|
||||||
rel_path = info["file"]
|
for module_info in modules:
|
||||||
attributes = info.get("attributes", {})
|
module_name = module_info["name"]
|
||||||
|
module_type_dir = module_info["type"]
|
||||||
source_code_path = self.vba_dir / rel_path
|
source_code_path = module_info["file_path"]
|
||||||
|
|
||||||
if not source_code_path.exists():
|
|
||||||
print(f" [跳过] 找不到源文件: {rel_path}")
|
|
||||||
continue
|
|
||||||
|
|
||||||
component = None
|
component = None
|
||||||
try:
|
try:
|
||||||
@@ -182,8 +196,8 @@ class VBAImporter:
|
|||||||
component = None
|
component = None
|
||||||
|
|
||||||
# 标准模块和类模块支持删除重建
|
# 标准模块和类模块支持删除重建
|
||||||
is_reloadable = module_type_dir in ["Modules", "ClassModules"]
|
is_reloadable = module_type_dir in [STANDARD_MODULE_DIR, CLASS_MODULE_DIR]
|
||||||
|
|
||||||
# ---------------------------------------------------------
|
# ---------------------------------------------------------
|
||||||
# 策略 A: 导入文件模式 (Modules, ClassModules)
|
# 策略 A: 导入文件模式 (Modules, ClassModules)
|
||||||
# ---------------------------------------------------------
|
# ---------------------------------------------------------
|
||||||
@@ -193,33 +207,32 @@ class VBAImporter:
|
|||||||
vb_project.VBComponents.Remove(component)
|
vb_project.VBComponents.Remove(component)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f" [警告] 无法移除 {module_name}: {e},将尝试仅更新代码")
|
print(f" [警告] 无法移除 {module_name}: {e},将尝试仅更新代码")
|
||||||
is_reloadable = False
|
is_reloadable = False
|
||||||
|
|
||||||
if is_reloadable:
|
if is_reloadable:
|
||||||
# 生成临时导入文件
|
# 生成临时导入文件
|
||||||
# 【修改】传入 module_type_dir 以判断是否需要加 Class 头
|
temp_file = self._reconstruct_file_content(source_code_path, module_name, module_type_dir)
|
||||||
temp_file = self._reconstruct_file_content(source_code_path, attributes, module_name, module_type_dir)
|
|
||||||
temp_files_created.append(temp_file)
|
temp_files_created.append(temp_file)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
vb_project.VBComponents.Import(str(temp_file))
|
vb_project.VBComponents.Import(str(temp_file))
|
||||||
print(f" [导入] {module_name} ({module_type_dir})")
|
print(f" [导入] {module_name} ({module_type_dir})")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f" [错误] 导入 {module_name} 失败: {e}")
|
print(f" [错误] 导入 {module_name} 失败: {e}")
|
||||||
|
|
||||||
# ---------------------------------------------------------
|
# ---------------------------------------------------------
|
||||||
# 策略 B: 字符串注入模式 (Sheet, Workbook, Forms)
|
# 策略 B: 字符串注入模式 (Sheet, Workbook, Forms)
|
||||||
# ---------------------------------------------------------
|
# ---------------------------------------------------------
|
||||||
if not is_reloadable:
|
if not is_reloadable:
|
||||||
if not component:
|
if not component:
|
||||||
if module_type_dir == "Forms":
|
if module_type_dir == FORMS_DIR:
|
||||||
print(f" [警告] 无法恢复 UserForm '{module_name}',跳过。")
|
print(f" [警告] 无法恢复 UserForm '{module_name}',跳过。")
|
||||||
continue
|
continue
|
||||||
elif module_type_dir == "DocumentModules":
|
elif module_type_dir == DOCUMENT_MODULE_DIR:
|
||||||
print(f" [警告] 找不到文档对象 '{module_name}',跳过。")
|
print(f" [警告] 找不到文档对象 '{module_name}',跳过。")
|
||||||
continue
|
continue
|
||||||
try:
|
try:
|
||||||
component = vb_project.VBComponents.Add(1)
|
component = vb_project.VBComponents.Add(1)
|
||||||
component.Name = module_name
|
component.Name = module_name
|
||||||
except:
|
except:
|
||||||
print(f" [错误] 无法创建组件 {module_name}")
|
print(f" [错误] 无法创建组件 {module_name}")
|
||||||
@@ -230,14 +243,14 @@ class VBAImporter:
|
|||||||
num_lines = code_module.CountOfLines
|
num_lines = code_module.CountOfLines
|
||||||
if num_lines > 0:
|
if num_lines > 0:
|
||||||
code_module.DeleteLines(1, num_lines)
|
code_module.DeleteLines(1, num_lines)
|
||||||
|
|
||||||
# 直接读取 UTF-8 字符串到内存
|
# 直接读取 UTF-8 字符串到内存
|
||||||
with open(source_code_path, 'r', encoding='utf-8') as f:
|
with open(source_code_path, 'r', encoding='utf-8') as f:
|
||||||
new_code = f.read()
|
new_code = f.read()
|
||||||
|
|
||||||
if new_code.strip():
|
if new_code.strip():
|
||||||
code_module.AddFromString(new_code)
|
code_module.AddFromString(new_code)
|
||||||
|
|
||||||
print(f" [更新] {module_name} ({module_type_dir}) - 代码已更新")
|
print(f" [更新] {module_name} ({module_type_dir}) - 代码已更新")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f" [错误] 更新代码 {module_name} 失败: {e}")
|
print(f" [错误] 更新代码 {module_name} 失败: {e}")
|
||||||
@@ -267,7 +280,7 @@ class VBAImporter:
|
|||||||
import traceback
|
import traceback
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
return False
|
return False
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
if workbook:
|
if workbook:
|
||||||
try: workbook.Close(SaveChanges=False)
|
try: workbook.Close(SaveChanges=False)
|
||||||
@@ -278,47 +291,52 @@ class VBAImporter:
|
|||||||
|
|
||||||
def main():
|
def main():
|
||||||
print("=" * 60)
|
print("=" * 60)
|
||||||
print("VBA代码导入工具 (V3.0 最终版)")
|
print("VBA代码导入工具 (V4.0 - 基于配置)")
|
||||||
print("=" * 60)
|
print("=" * 60)
|
||||||
print()
|
print()
|
||||||
|
|
||||||
script_dir = Path(__file__).parent
|
script_dir = Path(__file__).parent
|
||||||
|
|
||||||
# 检查是否配置了目标元数据文件
|
# 检查配置
|
||||||
if TARGET_METADATA_FILE and TARGET_METADATA_FILE.strip():
|
if not TARGET_XLSM_FILE:
|
||||||
# 使用配置的元数据文件路径
|
print("错误: 未配置 TARGET_XLSM_FILE")
|
||||||
target_path = Path(TARGET_METADATA_FILE)
|
print("请在 .env 文件中设置目标 Excel 文件路径")
|
||||||
|
return
|
||||||
|
|
||||||
# 如果是相对路径,则相对于脚本所在目录
|
# 确定目标文件路径
|
||||||
if not target_path.is_absolute():
|
target_path = Path(TARGET_XLSM_FILE)
|
||||||
target_path = script_dir / target_path
|
if not target_path.is_absolute():
|
||||||
|
target_path = script_dir / target_path
|
||||||
|
|
||||||
if not target_path.exists():
|
if not target_path.exists():
|
||||||
print(f"错误: 配置的元数据文件不存在: {target_path}")
|
print(f"错误: 配置的文件不存在: {target_path}")
|
||||||
return
|
return
|
||||||
|
|
||||||
if not target_path.name == METADATA_FILE:
|
if not target_path.suffix.lower() == '.xlsm':
|
||||||
print(f"警告: 文件名不是 {METADATA_FILE}: {target_path.name}")
|
print(f"警告: 文件扩展名不是 .xlsm: {target_path.name}")
|
||||||
|
|
||||||
metadata_path = target_path
|
# 确定 VBA 代码目录
|
||||||
print(f"使用配置的元数据文件: {metadata_path.name}")
|
if VBA_OUTPUT_DIR:
|
||||||
print()
|
vba_path = Path(VBA_OUTPUT_DIR)
|
||||||
|
if not vba_path.is_absolute():
|
||||||
|
vba_path = script_dir / vba_path
|
||||||
else:
|
else:
|
||||||
# 默认模式:查找脚本所在目录下的 VBA 文件夹
|
# 使用目标文件同目录下的 VBA 文件夹
|
||||||
vba_dir = script_dir / VBA_DIR_NAME
|
vba_path = target_path.parent / "VBA"
|
||||||
metadata_path = vba_dir / METADATA_FILE
|
|
||||||
|
|
||||||
if not metadata_path.exists():
|
if not vba_path.exists():
|
||||||
print(f"错误: 找不到元数据文件: {metadata_path}")
|
print(f"错误: VBA 代码目录不存在: {vba_path}")
|
||||||
print()
|
print()
|
||||||
print("提示:")
|
print("提示:")
|
||||||
print(" 1. 确保已运行 extract_vba.py 提取 VBA 代码")
|
print(" 1. 确保已运行 extract_vba.py 提取 VBA 代码")
|
||||||
print(" 2. 或在脚本顶部配置 TARGET_METADATA_FILE 指定元数据文件路径")
|
print(" 2. 或在 .env 文件中设置 VBA_OUTPUT_DIR 指定代码目录")
|
||||||
return
|
return
|
||||||
|
|
||||||
print(f"读取元数据: {metadata_path}")
|
print(f"目标 Excel 文件: {target_path.name}")
|
||||||
|
print(f"VBA 代码目录: {vba_path}")
|
||||||
|
print()
|
||||||
|
|
||||||
# 显示目标文件信息
|
# 确认操作
|
||||||
print("=" * 60)
|
print("=" * 60)
|
||||||
print("警告: 此操作将覆盖目标 Excel 文件中的 VBA 代码。")
|
print("警告: 此操作将覆盖目标 Excel 文件中的 VBA 代码。")
|
||||||
choice = input("\n确认继续? (y/n): ").lower().strip()
|
choice = input("\n确认继续? (y/n): ").lower().strip()
|
||||||
@@ -327,13 +345,11 @@ def main():
|
|||||||
print("操作已取消")
|
print("操作已取消")
|
||||||
return
|
return
|
||||||
|
|
||||||
importer = VBAImporter(str(metadata_path))
|
importer = VBAImporter(str(vba_path), str(target_path))
|
||||||
|
|
||||||
# 显示导入目标信息
|
# 显示模块数量
|
||||||
print()
|
modules = importer._scan_modules()
|
||||||
print(f"目标 Excel 文件: {importer.target_file.name}")
|
print(f"找到 {len(modules)} 个模块文件")
|
||||||
print(f"VBA 代码目录: {importer.vba_dir}")
|
|
||||||
print(f"模块数量: {len(importer.metadata.get('modules', {}))}")
|
|
||||||
print()
|
print()
|
||||||
print("=" * 60)
|
print("=" * 60)
|
||||||
print()
|
print()
|
||||||
@@ -349,4 +365,4 @@ def main():
|
|||||||
print("=" * 60)
|
print("=" * 60)
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
|
|||||||
Reference in New Issue
Block a user