Files
VBAExtractor/docs/plans/2026-05-11-access-support-plan.md
2026-05-11 09:57:26 +08:00

20 KiB
Raw Permalink Blame History

Access (.accdb) VBA Support Implementation Plan

For Claude: REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.

Goal: Add Access .accdb VBA extraction and import support alongside existing Excel .xlsm support, using a unified TARGET_FILE config variable.

Architecture: Extend the existing VBAExtractor and VBAImporter classes with Access COM methods. File type auto-detected by extension (.xlsm → Excel, .accdb → Access). Reuse existing module processing pipeline (parse_attributes, _process_module, _scan_modules, _reconstruct_file_content).

Tech Stack: pywin32 (COM automation), Access.Application ProgID


Task 1: Update .env config variable

Files:

  • Modify: .env:8
  • Modify: .env:5-8 (comments)

Step 1: Rename variable and update comments

Change .env from:

TARGET_XLSM_FILE=C:\Users\Administrator\Desktop\生产周期核对\常规产品生产周期.xlsm

To:

TARGET_FILE=C:\Users\Administrator\Desktop\生产周期核对\常规产品生产周期.xlsm

Update the comment block (lines 5-7) to reflect the new unified variable that supports both .xlsm and .accdb.

Step 2: Commit

git add .env
git commit -m "refactor: rename TARGET_XLSM_FILE to TARGET_FILE for unified file type support"

Task 2: Update extract_vba.py — config, constructor, and file type helper

Files:

  • Modify: extract_vba.py:23 (config var)
  • Modify: extract_vba.py:38-69 (constructor — rename self.xlsm_path to self.source_path)
  • Modify: extract_vba.py:129,171 (references to self.xlsm_path)
  • Add: file type helper function after constants block

Step 1: Replace TARGET_XLSM_FILE with TARGET_FILE

Line 23: TARGET_XLSM_FILETARGET_FILE Lines 21-26 — update comment block accordingly.

Step 2: Add file type helper function

Add after the constants block (after line 32), before the class:

# 支持的文件类型
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'

Step 3: Rename self.xlsm_path to self.source_path throughout the class

In __init__: self.xlsm_pathself.source_path (line 46, 58) In extract_vba_modules_olevba: self.xlsm_path.nameself.source_path.name and str(self.xlsm_path)str(self.source_path) (lines 129, 132) In extract_vba_modules_com: self.xlsm_path.nameself.source_path.name and str(self.xlsm_path.absolute())str(self.source_path.absolute()) (lines 171, 178) Also update the docstring in __init__ (line 43-44): xlsm_path: xlsm文件路径source_path: 源文件路径(支持.xlsm和.accdb Rename the parameter from xlsm_path to source_path.

Step 4: Commit

git add extract_vba.py
git commit -m "refactor: rename xlsm_path to source_path and add file type detection helper"

Task 3: Add extract_vba_modules_access_com() to VBAExtractor

Files:

  • Modify: extract_vba.py — add method after extract_vba_modules_com() (after line 245)

Step 1: Add the Access COM extraction method

Insert after line 245 (end of extract_vba_modules_com):

    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),按类型分类
                # 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

Step 2: Commit

git add extract_vba.py
git commit -m "feat: add Access COM extraction method to VBAExtractor"

Task 4: Update extract_vba.py main() for unified file dispatch

Files:

  • Modify: extract_vba.py:342-438 (entire main() function)

Step 1: Rewrite main() to support both file types

Replace the entire main() function. Key changes:

  • TARGET_XLSM_FILETARGET_FILE
  • File validation accepts both .xlsm and .accdb
  • Interactive mode scans for both extensions
  • Auto-selects COM method for .accdb (skips olevba prompt)
  • Instantiates VBAExtractor with source_path parameter
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:
        # 交互模式:查找支持的文件
        excel_dir = Path("Excel")
        if not excel_dir.exists():
            print("错误: 未找到Excel文件夹")
            return

        # 同时扫描 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(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)

Step 2: Commit

git add extract_vba.py
git commit -m "feat: update extract_vba.py main() to support both Excel and Access files"

Task 5: Update import_vba.py — config and add Access import method

Files:

  • Modify: import_vba.py:31 (config var)
  • Modify: import_vba.py:42-290 (class — add import_vba_access() method)

Step 1: Replace TARGET_XLSM_FILE with TARGET_FILE

Line 31: TARGET_XLSM_FILETARGET_FILE Lines 28-34 — update comment block accordingly.

Step 2: Add import_vba_access() method to VBAImporter

Insert after import_vba() method (after line 290, before the finally block closes and main() starts). Actually, insert as a new method on the class after import_vba():

    def import_vba_access(self):
        """使用Access COM导入VBA代码"""
        if not self.target_file.exists():
            print(f"错误: 找不到目标 Access 文件: {self.target_file}")
            return False

        if not self.vba_dir.exists():
            print(f"错误: 找不到 VBA 代码目录: {self.vba_dir}")
            return False

        print(f"正在打开 Access 数据库: {self.target_file.name} ...")

        access = None
        try:
            access = win32.Dispatch("Access.Application")
            access.Visible = False
            access.OpenCurrentDatabase(str(self.target_file))

            try:
                vb_project = access.VBE.VBProjects(1)
            except Exception:
                print("错误: 无法访问 VBA 项目。请确保信任对 VBA 工程对象模型的访问。")
                return False

            print("开始导入模块...\n")

            # 扫描所有模块Access 只导入标准模块和类模块)
            scan_dirs = [
                (STANDARD_MODULE_DIR, ".bas"),
                (CLASS_MODULE_DIR, ".cls"),
            ]
            modules = []
            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
                    })

            if not modules:
                print("警告: 未找到任何 VBA 模块文件")
                return False

            temp_files_created = []

            for module_info in modules:
                module_name = module_info["name"]
                module_type_dir = module_info["type"]
                source_code_path = module_info["file_path"]

                component = None
                try:
                    component = vb_project.VBComponents(module_name)
                except:
                    component = None

                # 移除已存在的组件
                if component:
                    try:
                        vb_project.VBComponents.Remove(component)
                    except Exception as e:
                        print(f"  [警告] 无法移除 {module_name}: {e},将尝试更新代码")
                        # 回退到字符串注入
                        try:
                            code_module = component.CodeModule
                            num_lines = code_module.CountOfLines
                            if num_lines > 0:
                                code_module.DeleteLines(1, num_lines)
                            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 e2:
                            print(f"  [错误] 更新代码 {module_name} 失败: {e2}")
                        continue

                # 生成临时导入文件并导入
                temp_file = self._reconstruct_file_content(source_code_path, 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}")

            # 清理临时文件
            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正在保存...")
            try:
                # Access 使用 RunCommand 保存
                import time
                time.sleep(1)  # 等待 VBE 完成
                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 access:
                try:
                    access.CloseCurrentDatabase()
                except:
                    pass
                try:
                    access.Quit()
                except:
                    pass

Step 3: Commit

git add import_vba.py
git commit -m "feat: add Access COM import method to VBAImporter"

Task 6: Update import_vba.py main() for unified file dispatch

Files:

  • Modify: import_vba.py:292-368 (entire main() function)

Step 1: Rewrite main() to support both file types

Replace the entire main() function. Key changes:

  • TARGET_XLSM_FILETARGET_FILE
  • File validation accepts both .xlsm and .accdb
  • Auto-selects import method based on file type
  • Display appropriate warning message
def main():
    print("=" * 60)
    print("VBA代码导入工具 (V5.0 - 支持Excel和Access)")
    print("=" * 60)
    print()

    script_dir = Path(__file__).parent

    # 检查配置
    if not TARGET_FILE:
        print("错误: 未配置 TARGET_FILE")
        print("请在 .env 文件中设置目标文件路径(支持.xlsm和.accdb")
        return

    # 确定目标文件路径
    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)

    # 确定 VBA 代码目录
    if VBA_OUTPUT_DIR:
        vba_path = Path(VBA_OUTPUT_DIR)
        if not vba_path.is_absolute():
            vba_path = script_dir / vba_path
    else:
        # 使用目标文件同目录下的 VBA 文件夹
        vba_path = target_path.parent / "VBA"

    if not vba_path.exists():
        print(f"错误: VBA 代码目录不存在: {vba_path}")
        print()
        print("提示:")
        print("  1. 确保已运行 extract_vba.py 提取 VBA 代码")
        print("  2. 或在 .env 文件中设置 VBA_OUTPUT_DIR 指定代码目录")
        return

    type_label = "Access" if file_type == "access" else "Excel"
    print(f"目标文件: {target_path.name} ({type_label})")
    print(f"VBA 代码目录: {vba_path}")
    print()

    # 确认操作
    print("=" * 60)
    print(f"警告: 此操作将覆盖目标{type_label}文件中的 VBA 代码。")
    choice = input("\n确认继续? (y/n): ").lower().strip()

    if choice != 'y':
        print("操作已取消")
        return

    importer = VBAImporter(str(vba_path), str(target_path))

    # 显示模块数量
    modules = importer._scan_modules()
    print(f"找到 {len(modules)} 个模块文件")
    print()
    print("=" * 60)
    print()

    # 根据文件类型选择导入方法
    if file_type == 'access':
        success = importer.import_vba_access()
    else:
        success = importer.import_vba()

    print()
    print("=" * 60)
    if success:
        print("导入成功完成!")
    else:
        print("导入失败")
    print("=" * 60)

Note: main() in import_vba.py also needs the get_file_type helper. Add the same helper function and constants to import_vba.py (or extract to a shared module — but per the design, we keep it simple with duplication since the helper is tiny).

Step 2: Add the file type helper to import_vba.py

Add after the constants block (after line 40), same as in extract_vba.py:

# 支持的文件类型
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'

Step 3: Commit

git add import_vba.py
git commit -m "feat: update import_vba.py main() to support both Excel and Access files"

Task 7: Update .gitignore and CLAUDE.md docs

Files:

  • Modify: .gitignore:17 (add Access/ directory)
  • Modify: CLAUDE.md (update docs to reflect Access support)

Step 1: Add Access directory to .gitignore

After line 17 (Excel/), add:

Access/

Step 2: Update CLAUDE.md

Update the project overview to mention Access support:

  • Project description: mention .accdb alongside .xlsm
  • Directory structure: add Access/ source directory
  • Common commands: update descriptions to mention Access files
  • Platform requirements: add "Microsoft Access" as optional

Step 3: Commit

git add .gitignore CLAUDE.md
git commit -m "docs: update documentation for Access file support"