Files
VBAExtractor/extract_vba.py
2026-05-11 10:01:37 +08:00

545 lines
18 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
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()