Claude Media
Claude CodeでVBAをPythonに移行する手順 — マクロの出力を正解にして置き換える

Claude CodeでVBAをPythonに移行する手順 — マクロの出力を正解にして置き換える

VBAの集計マクロをopenpyxlやxlwingsのPythonに置き換える手順です。先にマクロの出力を正解として保存し、比較テストが通るまでClaude Codeに直させます。

VBAの集計マクロをPythonに書き換えるとき、危ないのは「コードの見た目が似ているから移行完了」と判断することです。コードは読めても、丸め方や空セルの扱いが違えば、合計が1円ずれます。ずれに気づくのは、新しいPythonに切り替えた翌月の締め処理です。

この記事の進め方は逆順です。先にマクロ自身の出力を正解として保存し、新しいPythonがそれと一致するかを確かめるテストを用意します。そのテストが通るまでClaude Codeに直させる、という流れです。

最初にやること — マクロの出力を正解として保存する

置き換えの出発点は、Pythonのコードではなく、動いているマクロの実行結果です。固定の入力ブックでマクロを実行し、結果をtests/fixtures/に保存します。

手順

正解データを作る3手順

  1. 1

    入力を固定する

    本番データのコピーから個人情報を除いた入力ブックを用意し、input.xlsxとして保存します。件数は数十行で足ります。ただし、空欄の行、同額の行、端数が出る金額を意図的に混ぜます。

  2. 2

    マクロを人が実行する

    Excelでマクロを実行し、出力されたブックをマクロなしの.xlsxでexpected.xlsxに保存します。Claude CodeはExcelを開いてマクロを走らせられないので、この手順は人が行います。

  3. 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のRoundPythonでの書き方DecimalのROUND_HALF_EVEN
WorksheetFunction.Round、セルのROUNDPythonでの書き方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 Sub

RunPythonは、既定では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で結果が違いうるため、人がマクロの該当行を読んで決めてください。

参照した一次ソース

この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。

  1. code.claude.com/docs/en/headless
  2. openpyxl.readthedocs.io/en/stable/tutorial.html
  3. docs.xlwings.org/en/stable/installation.html
  4. learn.microsoft.com/en-us/office/vba/language/reference/user-interface-help/ro…
  5. learn.microsoft.com/en-us/office/vba/language/reference/user-interface-help/va…
  6. code.claude.com/docs/en/permissions
この記事を共有:XはてブLinkedIn