Claude CodeでVBAをPythonに移行する手順 — マクロの出力を正解にして置き換える
VBAの集計マクロをopenpyxlやxlwingsのPythonに置き換える手順です。先にマクロの出力を正解として保存し、比較テストが通るまでClaude Codeに直させます。
VBAの集計マクロをPythonに書き換えるとき、危ないのは「コードの見た目が似ているから移行完了」と判断することです。コードは読めても、丸め方や空セルの扱いが違えば、合計が1円ずれます。ずれに気づくのは、新しいPythonに切り替えた翌月の締め処理です。
この記事の進め方は逆順です。先にマクロ自身の出力を正解として保存し、新しいPythonがそれと一致するかを確かめるテストを用意します。そのテストが通るまでClaude Codeに直させる、という流れです。
最初にやること — マクロの出力を正解として保存する
置き換えの出発点は、Pythonのコードではなく、動いているマクロの実行結果です。固定の入力ブックでマクロを実行し、結果をtests/fixtures/に保存します。
正解データを作る3手順
- 1
入力を固定する
本番データのコピーから個人情報を除いた入力ブックを用意し、
input.xlsxとして保存します。件数は数十行で足ります。ただし、空欄の行、同額の行、端数が出る金額を意図的に混ぜます。 - 2
マクロを人が実行する
Excelでマクロを実行し、出力されたブックをマクロなしの
.xlsxでexpected.xlsxに保存します。Claude CodeはExcelを開いてマクロを走らせられないので、この手順は人が行います。 - 3
マクロのソースを書き出す
VBE(Visual Basic Editor)からモジュールを
.basにエクスポートし、legacy/に置きます。
3つ目は、手元にExcelがない場合、.xlsmにolevbaをかけて取り出せます。olevbaはOfficeファイルからVBAのソースを抽出するPythonのツールで、-cでソースだけを表示します。
pip install -U oletools
olevba -c legacy/monthly_report.xlsm > legacy/monthly_report.vba.txt正解を.xlsxで保存する理由は、数式セルの値にあります。openpyxlのload_workbookは、既定では数式セルを数式の文字列で返します。data_only=Trueにすると、Excelが最後に保存した計算結果を返します。つまり正解ファイルは、Excelで開いて保存し直したものでなければ、比較に使える値が入っていません。
例にするマクロと、置き換え先の選び方
以降の例は手順を示すための架空のマクロです。売上シートの2行目以降(日付・担当・商品・金額・数量)を担当者ごとに合計し、集計シートに「担当、合計、税込(合計×1.1を整数に丸めた値)」を書きます。
Excelの本体を操作するマクロなのか、ブックの中身を読み書きするだけのマクロなのかで、置き換え先が変わります。
| マクロの中身 | 置き換え先 | 動く環境 |
|---|---|---|
| ファイルを読んで集計し、別ファイルに書く | 置き換え先openpyxl | 動く環境Excelなしで動く |
| 開いているExcelの画面・ボタン・ピボットを操作する | 置き換え先xlwings | 動く環境Excelが必要(WindowsまたはmacOS) |
| 一部だけ先にPythonへ移したい | 置き換え先VBAのボタンからxlwingsのRunPythonで呼ぶ | 動く環境Excelが必要 |
xlwingsは、インストール手順に「Excelの導入が必要で、WindowsとmacOSでしか動かない」と書かれています。Python 3.11以上も要ります。サーバーやコンテナで定期実行したい集計は、openpyxlに寄せたほうが運用は楽になります。
openpyxlで.xlsmを開くときの注意もあります。keep_vba=TrueでVBAの部品を残せますが、残るだけで編集はできません。マクロ入りブックをopenpyxlで書き換えて戻す設計は避け、入力は読むだけにして、出力は新しい.xlsxに書きます。
Claude Codeに渡す依頼 — 比較テストを先に書かせる
Pythonの実装より先に、比較テストを書かせます。テストが先にあれば、Claudeは「通ったかどうか」を自分で読めるようになります。
legacy/monthly_report.vba.txt の集計マクロを、Pythonに置き換えたい。
次の順で進めて。
1. マクロが何をしているか、読み書きするシート・列・丸め方を表にする
2. tests/test_report.py に、aggregate() の出力と
tests/fixtures/expected.xlsx の「集計」シートを比べるテストを書く
3. テストが落ちることを確認してから、src/report.py に実装する
4. pytest が全部通るまで直す
expected.xlsx と input.xlsx は変更しない。「変更しない」の一文は大事です。テストが通らないとき、Claudeが正解データ側を書き換えて通すのを防ぎます。.claude/settings.jsonでtests/fixtures/への編集を拒否するのが、より確実な手段です。permissions.denyにEditの規則を入れます。
{
"permissions": {
"deny": [
"Edit(tests/fixtures/**)"
]
}
}ファイルの編集に対する規則はEdit(パス)で書きます。Write(...)で書いても受け付けられるだけで、一致判定には使われません。
テストの核は次のような形です(依頼内容から想定した例で、Claudeの実出力ではありません)。
from decimal import Decimal
from openpyxl import load_workbook
from src.report import aggregate
def read_summary(path):
ws = load_workbook(path, data_only=True)["集計"]
return [
tuple(Decimal(str(c)) if isinstance(c, (int, float)) else c for c in row)
for row in ws.iter_rows(min_row=2, values_only=True)
if row[0] is not None
]
def test_summary_matches_macro(tmp_path):
actual = tmp_path / "actual.xlsx"
aggregate("tests/fixtures/input.xlsx", actual)
expected = read_summary("tests/fixtures/expected.xlsx")
assert read_summary(actual) == expected完全一致で比べるか、許す差を決めるかは、マクロごとに判断します。出力日時のように毎回変わる列だけを除き、金額は許容誤差なしで比べる設計が基本です。
丸めと端数 — 「同じ結果」が崩れる最初の場所
置き換えで最初に落ちやすいのが、丸めです。VBAのRoundは、いわゆる銀行丸め(ちょうど半分のとき偶数側へ丸める)を返します。Microsoftの説明にも、予測しやすい結果が欲しいならワークシートの丸め関数を使うよう注意があります。同じページの例では、VBAのRound(0.12345,4)は0.1234、WorksheetFunction.Round(0.12345,4)は0.1235です。
つまり、VBAマクロの中のRoundと、セルに入っている=ROUND(...)は、同じ名前でも結果が違いえます。移行元のマクロがどちらを使っているかで、Pythonの書き方が変わります。
| 移行元の丸め | Pythonでの書き方 |
|---|---|
VBAのRound | Pythonでの書き方DecimalのROUND_HALF_EVEN |
WorksheetFunction.Round、セルのROUND | Pythonでの書き方DecimalのROUND_HALF_UP |
Pythonの組み込みroundも、ちょうど半分のときは偶数側へ丸めます。ドキュメントの例ではround(0.5)は0、round(1.5)は2です。一方でDecimalは既定の丸めがROUND_HALF_EVENで、モードを引数で明示できます。金額はfloatでなくDecimalで持ち、丸めのモードをコードに書いておくと、「どちらの丸めに合わせたか」が後から読めます。
from decimal import Decimal, ROUND_HALF_EVEN
tax_in = (total * Decimal("1.1")).quantize(Decimal("1"), rounding=ROUND_HALF_EVEN)この箇所は、Claudeに任せる前に人がマクロの該当行を確認すると確実です。テスト用の入力に、半分ちょうどになる金額を混ぜておけば、丸めの取り違えは比較テストが落ちて教えてくれます。合計が15円なら×1.1は16.5で、偶数側丸めだと16、切り上げだと17です。35円なら38.5で、38と39に分かれます。末尾が5の合計を担当者ごとに1人ずつ入れておくと、どちらの丸めに合わせたかがテストで確定します。
空セル — 0として扱うVBAと、Noneを返すopenpyxl
丸めの次に差が出やすいのが、空欄の行です。VBAでは、初期化されていないVariantの値をEmptyと呼びます。Microsoftの説明では、Emptyを含むVariantは、数値の文脈では0、文字列の文脈では長さ0の文字列("")として扱われます。空の金額セルを足し込むマクロは、エラーにならず、0円として合計に入ります。
一方、openpyxlで空のセルを読むとNoneが返ります。iter_rows(values_only=True)でも、空のセルはNoneで並びます。PythonではNoneを数値に足せないので、sumにそのまま渡すとTypeErrorで止まります。
amounts = [100, None, 250]
sum(amounts) # TypeError
sum(a or 0 for a in amounts) # 350止まるならまだ気づけます。厄介なのは、止まらずに結果が変わる書き方です。たとえば「値があるものだけ数える」ためにif row[3]で行を除くと、金額が0の行も一緒に落ちます。件数や平均を出すマクロでは、分母が変わって結果がずれます。
どちらに合わせるかは、マクロの該当行を読んで決めます。VBAが空セルを0として足しているなら、Python側も空セルを0として扱う、とコードに明示します。
検出は、先に作った入力ブックに空欄の行を混ぜておくだけで足ります。空の金額、空の担当者名、数式が入っているが値が空のセルを1行ずつ入れておき、比較テストでexpected.xlsxと突き合わせます。Claudeの実装が空欄で止まっても、黙って行を落としても、テストが落ちて知らせてくれます。
実装をテストが通るまで回す — claude -pで反復する
テストと依頼が整ったら、非対話モードのclaude -pで一周ずつ回せます。-pの実行は成功なら終了コード0、失敗なら非0で終わるので、シェルから合否を見られます。
claude -p "pytest が通るように src/report.py を直して。tests/fixtures は変更しない" \
--permission-mode acceptEdits \
--allowedTools "Read,Edit,Bash(pytest *)"acceptEditsはファイルの編集を確認なしで通し、Bash(pytest *)はpytestで始まるコマンドだけを許可します。末尾の *は前方一致で、*の前の空白が大事です。ここでBashだけを書くと、すべてのシェルコマンドを許可することになるので、移行作業では絞った形にすると安全です。
CIや定期実行で同じ手順を回すときは--bareを足すと、手元の~/.claudeにあるフックやMCPサーバーを読み込まず、どのマシンでも同じ結果になります。ただし--bareはサブスクのログインを使わず、ANTHROPIC_API_KEYが要ります。
マクロが複数あるときの進め方
マクロが10本あるなら、10本まとめて頼まず、1本ずつ同じ型で回します。1本ごとに、入力ブック、正解ブック、テストの3点を用意し、通ったら次へ進みます。
並行運転の考え方は、RPAの置き換えと同じです。段階の切り方はClaude CodeでRPAを置き換える手順にまとめています。VBA側を直しながら残す場合のCLAUDE.mdの規約は、Claude CodeでExcel VBAマクロの書き方が扱っています。
xlwingsで段階移行するなら、ボタンに紐づくVBAを残したまま、中身だけPythonに差し替えられます。
Sub MonthlyReport()
RunPython "import report; report.main()"
End SubRunPythonは、既定ではExcelファイルと同じ場所にある同名の.pyを探します。Python側ではxw.Book.caller()で呼び出し元のブックを取れます。こうしておけば、利用者のボタンの押し方は変えずに、中身を1本ずつPythonへ移せます。最終的にボタンを外し、openpyxlの定期実行へ寄せるかは、運用の都合で決められます。
比較テストが通ったあとに残る確認
テストが全部通っても、確認が残る項目があります。
- マクロが画面表示や書式(色、列幅、罫線)に依存していないか。値の比較では書式は見ていません
- マクロがユーザー入力(InputBox)やファイルダイアログに依存していないか
- 実行時に開いた別ブックや、外部データ接続を使っていないか
- 数式セルを含む出力は、保存時に計算結果が入るか。
openpyxlで数式を書いたブックは、Excelで開いて保存するまで計算結果を持ちません。再計算の扱いはClaude xlsxスキルの使い方にあります - xlwingsに足りない機能は、
.apiでExcel本体のオブジェクトに触れますが、その書き方はWindowsとmacOSで違い、コードがOS依存になります
書式まで同じにする必要があるマクロは、値の比較テストと別に、書式を確かめるテストを足します。見た目の確認は人が目で行う範囲として残すのが現実的です。
まとめ
VBAからPythonへの移行は、書き換えの上手さより、何をもって同じと呼ぶかの定義で決まります。マクロの実行結果をexpected.xlsxとして保存し、比較テストを先に書いておけば、Claude Codeへの依頼は「このテストを通す」の一言で足ります。丸めだけは、VBAのRoundとセルのROUNDで結果が違いうるため、人がマクロの該当行を読んで決めてください。
参照した一次ソース
この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。
- code.claude.com/docs/en/headless
- openpyxl.readthedocs.io/en/stable/tutorial.html
- docs.xlwings.org/en/stable/installation.html
- learn.microsoft.com/en-us/office/vba/language/reference/user-interface-help/ro…
- learn.microsoft.com/en-us/office/vba/language/reference/user-interface-help/va…
- code.claude.com/docs/en/permissions