feat: add Access (.accdb) extraction support to extract_vba.py
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
189
extract_vba.py
189
extract_vba.py
@@ -20,7 +20,7 @@ except ImportError:
|
||||
|
||||
# ==================== 配置区域 ====================
|
||||
# 从 .env 文件读取配置,如果未设置则使用 None(交互模式)
|
||||
TARGET_XLSM_FILE = os.getenv("TARGET_XLSM_FILE", "").strip() or None
|
||||
TARGET_FILE = os.getenv("TARGET_FILE", "").strip() or None
|
||||
# VBA代码输出目录(如果未设置,则使用源文件同目录下的VBA文件夹)
|
||||
VBA_OUTPUT_DIR = os.getenv("VBA_OUTPUT_DIR", "").strip() or None
|
||||
# =================================================
|
||||
@@ -31,19 +31,36 @@ 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'
|
||||
"""
|
||||
ext = file_path.suffix.lower()
|
||||
if ext in ACCESS_EXTENSIONS:
|
||||
return 'access'
|
||||
return 'excel'
|
||||
|
||||
|
||||
class VBAExtractor:
|
||||
"""VBA代码提取器"""
|
||||
|
||||
def __init__(self, xlsm_path: str, output_dir: str = None):
|
||||
def __init__(self, source_path: str, output_dir: str = None):
|
||||
"""
|
||||
初始化VBA提取器
|
||||
|
||||
Args:
|
||||
xlsm_path: xlsm文件路径
|
||||
source_path: 源文件路径(支持.xlsm/.accdb等)
|
||||
output_dir: 输出目录(如果未指定,则使用VBA_OUTPUT_DIR配置或源文件同目录)
|
||||
"""
|
||||
self.xlsm_path = Path(xlsm_path)
|
||||
self.source_path = Path(source_path)
|
||||
|
||||
# 确定输出目录的优先级:
|
||||
# 1. 参数指定的 output_dir
|
||||
@@ -55,7 +72,7 @@ class VBAExtractor:
|
||||
self.output_dir = Path(VBA_OUTPUT_DIR)
|
||||
else:
|
||||
# 使用目标文件同目录下的VBA文件夹
|
||||
self.output_dir = self.xlsm_path.parent / "VBA"
|
||||
self.output_dir = self.source_path.parent / "VBA"
|
||||
|
||||
# 创建输出目录结构
|
||||
self.modules_dir = self.output_dir / STANDARD_MODULE_DIR
|
||||
@@ -126,10 +143,10 @@ class VBAExtractor:
|
||||
print("请运行: pip install oletools")
|
||||
return False
|
||||
|
||||
print(f"正在解析文件: {self.xlsm_path.name}")
|
||||
print(f"正在解析文件: {self.source_path.name}")
|
||||
|
||||
try:
|
||||
vba_parser = VBA_Parser(str(self.xlsm_path))
|
||||
vba_parser = VBA_Parser(str(self.source_path))
|
||||
|
||||
if vba_parser.detect_vba_macros():
|
||||
print("发现VBA代码,开始提取...\n")
|
||||
@@ -168,14 +185,14 @@ class VBAExtractor:
|
||||
print("请运行: pip install pywin32")
|
||||
return False
|
||||
|
||||
print(f"正在使用COM接口解析: {self.xlsm_path.name}")
|
||||
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.xlsm_path.absolute()))
|
||||
workbook = excel.Workbooks.Open(str(self.source_path.absolute()))
|
||||
|
||||
# 获取VBA项目
|
||||
if not workbook.VBProject:
|
||||
@@ -244,6 +261,84 @@ class VBAExtractor:
|
||||
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:
|
||||
"""
|
||||
根据模块名称和流路径确定模块类型
|
||||
@@ -347,10 +442,10 @@ def main():
|
||||
print()
|
||||
|
||||
# 检查是否配置了目标文件
|
||||
if TARGET_XLSM_FILE and TARGET_XLSM_FILE.strip():
|
||||
if TARGET_FILE and TARGET_FILE.strip():
|
||||
# 使用配置的文件路径
|
||||
script_dir = Path(__file__).parent
|
||||
target_path = Path(TARGET_XLSM_FILE)
|
||||
target_path = Path(TARGET_FILE)
|
||||
|
||||
# 如果是相对路径,则相对于脚本所在目录
|
||||
if not target_path.is_absolute():
|
||||
@@ -360,69 +455,75 @@ def main():
|
||||
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}")
|
||||
file_type = get_file_type(target_path)
|
||||
source_file = target_path
|
||||
print(f"使用配置文件: {source_file.name} ({file_type})")
|
||||
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文件")
|
||||
# 同时扫描 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(xlsm_files) > 1:
|
||||
print("发现多个xlsm文件:")
|
||||
for i, f in enumerate(xlsm_files, 1):
|
||||
print(f" {i}. {f.name}")
|
||||
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:
|
||||
xlsm_file = xlsm_files[0]
|
||||
source_file = all_files[0]
|
||||
else:
|
||||
try:
|
||||
idx = int(choice) - 1
|
||||
xlsm_file = xlsm_files[idx]
|
||||
source_file = all_files[idx]
|
||||
except:
|
||||
print("无效选择,使用第一个文件")
|
||||
xlsm_file = xlsm_files[0]
|
||||
source_file = all_files[0]
|
||||
else:
|
||||
xlsm_file = xlsm_files[0]
|
||||
source_file = all_files[0]
|
||||
|
||||
file_type = get_file_type(source_file)
|
||||
print()
|
||||
print(f"选择文件: {xlsm_file.name}")
|
||||
print(f"选择文件: {source_file.name} ({file_type})")
|
||||
print()
|
||||
|
||||
# 创建提取器
|
||||
# 输出目录优先级: 1. .env中的VBA_OUTPUT_DIR配置 2. 源文件同目录下的VBA文件夹
|
||||
extractor = VBAExtractor(str(xlsm_file))
|
||||
extractor = VBAExtractor(str(source_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()
|
||||
# 根据文件类型选择提取方法
|
||||
if file_type == 'access':
|
||||
# Access 只支持COM方法
|
||||
print("Access文件仅支持COM接口提取...")
|
||||
success = extractor.extract_vba_modules_access_com()
|
||||
else:
|
||||
print("\n使用COM接口提取...")
|
||||
success = extractor.extract_vba_modules_com()
|
||||
# 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)
|
||||
|
||||
Reference in New Issue
Block a user