前言

雖然儘量避免,但偶爾還是需要管理大量的 Excel 或 Excel VBA 的情況,您是否有過這樣的經驗呢?

如果是 App Script,還有幾種管理的方法可供選擇。

不過,對於 Excel 和 Excel VBA,似乎沒有太多的管理方式,最終常常會找到雲端上的某個硬碟來存放。

經過一番思考,我認為最好的管理方式還是使用 Git,因此撰寫了這篇文章。

使用 Git 管理 Excel 和 Excel VBA

將 Excel 和 Excel VBA 使用 Git 管理,最終會變成二進位管理。

為了尋找解決方案,從 Chart GPT 獲得了推薦 Git XL,但發現更新停滯,使用上讓人感到不安。

所以總的來說,Excel 本身似乎無法脫離二進位管理。。。這就是我的結論。

我希望至少妥善管理 VBA 部分。因此查找資訊後,找到以下的文章。

透過 Excel 的 commit 時機,提取出 VBA 的部分,並將其混入現有的 commit 中。
這聽起來「不錯」,於是我馬上就挑戰了一下。

使用的工具是 Python,並且僅用到 pre-commitoletools 這兩個函式庫。
這兩者在 2025.09 現在的版本中,都得到了妥善的維護。

以下是 pre-commit 的 YAML 配置檔。

# .pre-commit-config.yaml
repos:
  - repo: local
    hooks:
      - id: extract-vba-macros
        name: 從 Excel 檔案中提取 VBA 巨集
        entry: uv run python .githooks/pre-commit.py
        language: system
        files: \.(xlsb|xls|xlsm|xla|xlt|xlam)$
        pass_filenames: false
        always_run: false

以下是提交時觸發的原始程式碼。

# .githooks/pre-commit.py
import os
import shutil
import sys
import subprocess
from oletools.olevba3 import VBA_Parser

EXCEL_FILE_EXTENSIONS = ('xlsb', 'xls', 'xlsm', 'xla', 'xlt', 'xlam')
KEEP_NAME = False
VBA_ROOT_PATH = 'src.vba'

def get_staged_excel_files() -> list[str]:
    try:
        result = subprocess.run(
            ['git', 'diff', '--cached', '--name-only', '-z'],
            capture_output=True,
            text=True,
            check=True
        )
        # git diff --cached --name-only -z 取得的檔案名是以 null 字符(\0) 分隔
        # 例如: "src.vba/轉換工具/Sheet1.cls\0src.vba/轉換工具/Module1.bas\0"
        # 因此,我們獲取以 null 字符(\0) 隔開的檔案名,排除空字串,取得檔案名
        staged_files = result.stdout.strip().split('\0') if result.stdout.strip() else []
        staged_files = [f for f in staged_files if f]  # 排除空字串

        # 取得 Excel 檔案(排除臨時檔案)
        excel_files = [
            f for f in staged_files 
            if f.endswith(EXCEL_FILE_EXTENSIONS) and os.path.exists(f) and not os.path.basename(f).startswith('~$')
        ]
        return excel_files
    except subprocess.CalledProcessError:
        # 如果 git 命令失敗,回退到掃描所有檔案
        return []

def parse_excel_file(excel_file: str) -> bool:
    excel_filename = os.path.splitext(os.path.basename(excel_file))[0]
    vba_file_dir = os.path.join(VBA_ROOT_PATH, excel_filename)

    try:
        vba_parser = VBA_Parser(excel_file)
        if not vba_parser.detect_vba_macros():
            print(f"{excel_file} 中沒有發現 VBA 巨集")
            return True

        vba_modules = vba_parser.extract_all_macros()

        if not vba_modules:
            print(f"{excel_file} 中未提取到 VBA 模組")
            return True

        print(f"正在從 {excel_file} 中提取 VBA 巨集...")

        for _, _, filename, content in vba_modules:
            lines = content.split('\r\n') if '\r\n' in content else content.split('\n')

            if not lines:
                continue

            filtered_content = []
            for line in lines:
                if line.startswith('Attribute') and 'VB_' in line:
                    if 'VB_Name' in line and KEEP_NAME:
                        filtered_content.append(line)
                else:
                    filtered_content.append(line)

            if filtered_content and filtered_content[-1] == '':
                filtered_content.pop()

            non_empty_lines = [line for line in filtered_content if line.strip()]

            if non_empty_lines:
                if not os.path.exists(vba_file_dir):
                    os.makedirs(vba_file_dir)

                output_file = os.path.join(vba_file_dir, filename)
                with open(output_file, 'w', encoding='utf-8') as f:
                    f.write('\n'.join(filtered_content))

                print(f"  → 提取完成: {output_file}")

                # 將提取出的 VBA 檔案加入階段
                subprocess.run(['git', 'add', output_file], check=False)

        vba_parser.close()
        return True
    except Exception as e:
        print(f"處理 {excel_file} 時發生錯誤: {e}")
        return False

def clean_old_vba_files(staged_excel_files: list[str]) -> None:
    excel_file_names = [
        os.path.splitext(os.path.basename(excel_file))[0]
        for excel_file in staged_excel_files
    ]

    for excel_filename in excel_file_names:
        vba_file_dir = os.path.join(VBA_ROOT_PATH, excel_filename)
        if not os.path.exists(vba_file_dir):
            try:
                print(f"移除舊的 VBA 目錄: {vba_file_dir}")
                shutil.rmtree(vba_file_dir)
            except Exception as e:
                print(f"移除舊的 VBA 目錄時發生錯誤: {e}")

def main():
    staged_excel_files = get_staged_excel_files()

    if not staged_excel_files:
        print("未找到任何 Excel 檔案。")
        return 0

    clean_old_vba_files(staged_excel_files)

    success = True
    for excel_file in staged_excel_files:
        if not parse_excel_file(excel_file):
            success = False

    if success:
        print("VBA 巨集提取成功完成。")
        return 0
    else:
        print("有些檔案處理失敗。")
        return 1

if __name__ == '__main__':
    sys.exit(main())

可惜的是,對於 Excel 仍然需要持續進行二進位管理,但對於 VBA,已經可以脫離 Excel,使用 Git 來進行源碼管理。
至少,相較於「儲存在某個雲端上」,此管理方式算是好一些的選擇。

image-1757260595664.png

最後

對於非工程師來說,Excel 和 Excel VBA 是非常方便的工具,容易產生大量的檔案。

然而,對於管理者來說,Excel 和 Excel VBA 是難以管理的,盡可能地應該避免使用這些工具。

特別是當 Excel 和 Excel VBA 作為系統使用的一部分時,能夠盡可能地與源碼一起管理會是最理想的。
此次針對 Excel 和 Excel VBA 的管理方式,進行了一番研究和嘗試。


原文出處:https://qiita.com/Kodak_tmo/items/04de2f6cfee17bb1119e


精選技術文章翻譯,幫助開發者持續吸收新知。

共有 0 則留言


精選技術文章翻譯,幫助開發者持續吸收新知。
🏆 本月排行榜
🥇
站長阿川
📝10   💬6   ❤️11
423
🥈
我愛JS
📝1   💬5   ❤️4
89
🥉
AppleLily
📝1   💬4   ❤️1
48
#4
💬2  
6
#5
💬1  
5
評分標準:發文×10 + 留言×3 + 獲讚×5 + 點讚×1 + 瀏覽數÷10
本數據每小時更新一次