はじめに
可能な限り避けたいのですが、稀に大量のExcelやExcel VBAを管理しなくてはならないときってありませんか?
App Scriptであれば、まだ管理する方法は幾つかあります。
しかし、ExcelやExcel VBAだと管理する方法が無く、どこかクラウド上のドライブで保管する。に行き着くことが多いです。
なにか良い管理方法はないかと色々と考えた結果、やはりGitで管理するのが良さそうだと思ったので、記事にしました。
Excel、Excel VBAをGitで管理する
Excel、Excel VBAをGitで管理すると、結局バイナリ管理になります。
これらを解決するための手段として、Chart GPT
から、Git XL
をオススメされましたが、更新が停滞しており、使用に不安を感じました。
やはり、Excel自体はバイナリ管理するしかなさそう。。。というのが結論でした。
VBAの方だけでも、ちゃんとGitで管理したい。そう思って色々と調べてたどり着いたのが以下記事。
Excel の commit タイミングで、VBAの部分だけを抽出し、既存コミットに混ぜてしまう。というもの。
これは「良さそう」ということで早速チャレンジしてみました。
なお、使用するのはPythonと、ライブラリは pre-commit
と oletools
の2つのみ。
両方とも 2025.09
現時点で、ちゃんとメンテナンスされています。
以下、pre-commit
のYAMLファイル。
# .pre-commit-config.yaml
repos:
- repo: local
hooks:
- id: extract-vba-macros
name: Extract VBA Macros from Excel files
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:
# Fallback to scanning all files if git command fails
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"No VBA macros found in {excel_file}")
return True
vba_modules = vba_parser.extract_all_macros()
if not vba_modules:
print(f"No VBA modules extracted from {excel_file}")
return True
print(f"Extracting VBA macros from {excel_file}...")
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" → Extracted: {output_file}")
# Stage the extracted VBA file
subprocess.run(['git', 'add', output_file], check=False)
vba_parser.close()
return True
except Exception as e:
print(f"Error processing {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"Removing old VBA directory: {vba_file_dir}")
shutil.rmtree(vba_file_dir)
except Exception as e:
print(f"Error removing old VBA directory: {e}")
def main():
staged_excel_files = get_staged_excel_files()
if not staged_excel_files:
print("No Excel files found.")
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 macro extraction completed successfully.")
return 0
else:
print("Some files failed to process.")
return 1
if __name__ == '__main__':
sys.exit(main())
残念ながら、Excelについては継続してバイナリ管理となってしまいますが、VBAについてはExcelから切り離してGitを使ってのソースコード管理を行うことができるようになりました。
少なくとも「どこかクラウド上のドライブで保管する」よりは幾分マシな方法で管理できるのは、良いと思いました。
さいごに
ExcelやExcel VBAは、非エンジニアにとっては大変便利なもので、つい量産されてしまいがちです。
ただ、管理する側にとって見れば、ExcelやExcel VBAは管理し辛く、可能な限り避けたいものです。
特に、システム利用の一部としてExcelやExcel VBAを使っているものであれば、可能な限りソースコードといっしょに管理したいですよね。
今回はそんなExcelやExcel VBAの管理方法について、色々と調べて、試してみました。
👋👋👋