Excel VBAからPythonスクリプトを実行する完全ガイド

Excel VBAからPythonを呼び出すと、Excelでの入力・表示と、Pythonによる集計やデータ加工を組み合わせられます。ただし、外部プログラムを起動することと、処理の成功を確認して結果を取り込むことは別です。この記事では、パスの指定、実行環境の固定、終了待ち、終了コード、CSVの文字コードまでを順に整理します。

対象はWindows版のデスクトップExcelと、PCにインストールした外部のCPythonです。以下のWindows Script HostやCOMを使うコードは、Mac版Excel・Excel for the web向けではありません。セルにPythonを書く「Python in Excel」はMicrosoftのクラウドで実行される別機能であり、ローカルのPython環境はその実行環境になりません。詳しくはMicrosoftのPython in Excelの説明を参照してください。

目次

なぜExcel VBAからPythonを使うのか

  • 拡張性:Pythonのライブラリを使ったデータ分析、ファイル変換、機械学習などを、Excelから起動できます。
  • 役割分担:Excelを入力・確認画面として使い、計算や前処理をPython側にまとめることで、処理を再利用しやすくなります。
  • 自動化:データの読み込み、検証、集計、結果の取り込みまでを一連の操作にできます。ただし、外部プログラムの失敗や処理時間も考慮した設計が必要です。

Pythonの環境設定

Pythonと作業フォルダーを準備する

Pythonが未導入の場合は、Python公式サイトとWindows向けの公式導入手順を確認してください。配布方式や既存のインストールによって、利用できるコマンドや保存先は異なります。「特定の固定ディレクトリにある」「PATHの追加が必須」とは限りません。会社のPCでは組織が承認した環境を使います。

以下では、書き込み可能なローカルフォルダーC:\Excel Python Demoを作成したものとします。別の場所を使う場合は、コマンドと後述のVBA定数PROJECT_FOLDERを同じ場所に変更してください。空白を含むフォルダー名でも扱える例です。

まずコマンドプロンプトで、利用するPythonを確認します。次のpythonは、意図したPythonが起動することを確認して使ってください。うまく起動しない場合は、導入済みPythonの絶対パスを指定するか、公式手順に従ってコマンドの解決先を確認します。

python -c "import sys; print(sys.executable); print(sys.version)"

仮想環境を作り、そのpython.exeを指定する

python -m venv "C:\Excel Python Demo\.venv"
"C:\Excel Python Demo\.venv\Scripts\python.exe" -c "import sys; print(sys.executable)"

VBAでは、この仮想環境のpython.exeを絶対パスで呼び出します。実行時のPATHや、別のターミナルで選択したPythonに依存させないためです。venvの公式資料にあるとおり、仮想環境の実行ファイルを直接指定する場合、事前のactivateは不要です。PowerShellの実行ポリシー変更も、この方法のためには必要ありません。仮想環境は別の場所へコピーせず、移動先で作り直します。

必要なライブラリを同じ環境へ導入する

この記事の基本例とCSV出力例はPython標準ライブラリだけで動きます。応用でpandasなどを使う場合は、単独のpipではなく、VBAが呼び出すPythonに対して-m pipを実行します。例えば次は、データ分析・グラフ・xlsx読み込み用の任意の追加パッケージを、その仮想環境へ入れるコマンドです。

"C:\Excel Python Demo\.venv\Scripts\python.exe" -m pip install pandas matplotlib openpyxl

マクロを保存するExcelブックは.xlsm形式などのマクロ対応形式にします。自分で内容を確認したコードだけを、組織のマクロ運用方針に従って実行してください。「すべてのマクロを有効にする」などのセキュリティ緩和は必要ありません。

VBAからPythonを起動する基本例

短いPythonスクリプトを用意する

次をUTF-8のC:\Excel Python Demo\hello.pyとして保存します。

print("Hello from Python!")

VBAより先に、コマンドプロンプトで同じ実行ファイルとスクリプトを指定して動作を確認します。

"C:\Excel Python Demo\.venv\Scripts\python.exe" "C:\Excel Python Demo\hello.py"

パスを個別に引用してShellで起動する

VBEで標準モジュールを作成し、次のコードを入れます。後のVBA例も同じ標準モジュールに追加してください。Option Explicitと定数、共通関数は一度だけ記述します。

Option Explicit

Private Const PROJECT_FOLDER As String = "C:\Excel Python Demo"

' この例の固定されたファイルパス専用。自由入力の引数には使わない。
Private Function QuoteFilePath(ByVal value As String) As String
    Dim i As Long

    If Len(value) = 0 Then
        Err.Raise vbObjectError + 1000, , "ファイルパスが空です。"
    End If
    If InStr(value, Chr$(34)) > 0 Or InStr(value, "%") > 0 _
        Or Right$(value, 1) = "\" Then
        Err.Raise vbObjectError + 1001, , "この例では扱えないパスです: " & value
    End If
    For i = 1 To Len(value)
        If AscW(Mid$(value, i, 1)) >= 0 _
            And AscW(Mid$(value, i, 1)) < 32 Then
            Err.Raise vbObjectError + 1002, , "パスに制御文字があります。"
        End If
    Next i
    QuoteFilePath = Chr$(34) & value & Chr$(34)
End Function

Private Sub RequireFile(ByVal path As String)
    Dim fso As Object
    Set fso = CreateObject("Scripting.FileSystemObject")
    If Not fso.FileExists(path) Then
        Err.Raise vbObjectError + 1003, , "ファイルがありません: " & path
    End If
End Sub

Public Sub RunPythonAsync()
    On Error GoTo Failed
    Dim pythonExe As String, scriptPath As String, command As String
    Dim taskId As Double

    pythonExe = PROJECT_FOLDER & "\.venv\Scripts\python.exe"
    scriptPath = PROJECT_FOLDER & "\hello.py"
    RequireFile pythonExe
    RequireFile scriptPath
    command = QuoteFilePath(pythonExe) & " " & QuoteFilePath(scriptPath)

    taskId = Shell(command, vbNormalFocus)
    MsgBox "起動要求を行いました。タスクID: " & CStr(taskId) & vbCrLf & _
           "Pythonの完了や成功を確認したわけではありません。"
    Exit Sub
Failed:
    MsgBox "起動できませんでした: " & Err.Description, vbExclamation
End Sub

実行ファイルとスクリプトのそれぞれをダブルクォートで囲むのがポイントです。QuoteFilePathは、この例で組み立てた絶対ファイルパスだけを扱う限定的な補助関数です。引用符・制御文字・末尾のバックスラッシュに加え、後述のWScript.Shell.Runが環境変数を展開する%も拒否します。任意の文字列を安全に引数化できる汎用関数ではありません。

VBAのShell関数は通常、非同期で動作します。戻り値は起動したプログラムのタスクIDであり、Pythonの終了コードや標準出力ではありません。この例の表示窓は処理後すぐ閉じることがあります。表示を読むためにPythonを対話入力待ちにせず、前述のコマンドプロンプトで確認してください。

引数を渡すときの注意点

Pythonではsys.argvやargparseで引数を受け取れます。ただし、Excelのセル値をarguments = "..."のようなコマンド文字列へ無加工で連結しないでください。空白・引用符・オプションに見える値などで意味が変わるおそれがあります。

  • 実行するpython.exeと.pyのパスは、確認済みのものに固定します。セル値やWebから得た文字列で実行対象を切り替えません。
  • この例ではコマンドシェルを挟まずPythonを直接起動します。cmd /c、PowerShell、python -cなどへ利用者のデータを埋め込む方式にはしません。
  • 自由入力の文章・大量の値は、UTF-8のCSVやJSONなどに保存し、そのファイルパスを渡します。Python側でも列・型・件数などを検証し、データをevalやexecで実行しません。
  • パスを引用するだけでは、不審なスクリプト自体が安全になるわけではありません。スクリプトと仮想環境は、信頼できる利用者だけが変更できる場所で管理します。

終了を待ち、実行結果をExcelに取り込む

結果をすぐ使う処理では、Shellの直後にCSVを読むと、まだ書き込みが終わっていないことがあります。短時間の処理なら、WScript.ShellのRunメソッドにTrueを渡し、終了コードを確認してから読み込みます。待機中はVBAが止まり、Excelを操作しづらくなります。このメソッドにはタイムアウト引数がないため、対話入力や長時間のネットワーク待ちを行う処理には、そのまま使わないでください。

Python側:CSVとログを出力する

次をC:\Excel Python Demo\export_result.pyとしてUTF-8で保存します。サンプルデータの2列をCSVに出力し、成功時は0、処理中の例外では1を返します。出力先とログのパスはVBAから受け取り、Pythonの作業ディレクトリには依存しません。

import argparse
import csv
import logging
from pathlib import Path


def main():
    parser = argparse.ArgumentParser()
    parser.add_argument("--output", required=True, type=Path)
    parser.add_argument("--log", required=True, type=Path)
    args = parser.parse_args()

    # ログを開く前に検証し、同じファイルの誤上書きを防ぐ。
    if not args.output.is_absolute() or not args.log.is_absolute():
        parser.error("出力とログには絶対パスを指定してください。")
    if args.output.resolve() == args.log.resolve():
        parser.error("出力とログは別のファイルにしてください。")

    # ログの初期化に失敗した場合などは、標準エラーに表示される。
    logging.basicConfig(
        filename=args.log,
        filemode="w",
        encoding="utf-8",
        level=logging.INFO,
        format="%(asctime)s %(levelname)s %(message)s",
    )
    try:
        rows = [("山田", 28), ("佐藤, 花子", 34), ('鈴木 "A"', 29)]
        # 新規ファイルだけを作成し、既存の結果を上書きしない。
        with args.output.open("x", encoding="utf-8-sig", newline="") as f:
            writer = csv.writer(f)
            writer.writerow(["名前", "年齢"])
            writer.writerows(rows)
        logging.info("正常終了: %s件", len(rows))
        return 0
    except Exception:
        logging.exception("処理に失敗しました")
        return 1


if __name__ == "__main__":
    raise SystemExit(main())

CSVはcsvモジュールで引用符やカンマを処理し、newline=""で開きます。文字コードはBOM付きUTF-8のutf-8-sig、ログはUTF-8と明示しています。Pythonの終了コードをVBA側で判定するため、例外を表示するだけで正常終了に見せかけないことが重要です。

VBA側:終了コードを確認して新しいシートへ取り込む

次を、先ほどの共通関数と同じ標準モジュールへ追加します。実行するたびに一時フォルダーを新規作成するため、以前のCSVを今回の成功結果として読み込むことを避けられます。ブックの既存シートには上書きせず、新しいワークシートを追加します。

Public Sub RunPythonAndImport()
    On Error GoTo Failed
    Dim fso As Object, runner As Object
    Dim pythonExe As String, scriptPath As String, command As String
    Dim jobFolder As String, csvPath As String, logPath As String
    Dim exitCode As Long, message As String
    Dim ws As Worksheet, qt As QueryTable

    Set fso = CreateObject("Scripting.FileSystemObject")
    pythonExe = PROJECT_FOLDER & "\.venv\Scripts\python.exe"
    scriptPath = PROJECT_FOLDER & "\export_result.py"
    RequireFile pythonExe
    RequireFile scriptPath

    jobFolder = fso.BuildPath(fso.GetSpecialFolder(2).Path, fso.GetTempName)
    fso.CreateFolder jobFolder
    csvPath = fso.BuildPath(jobFolder, "output.csv")
    logPath = fso.BuildPath(jobFolder, "run.log")
    command = QuoteFilePath(pythonExe) & " " & QuoteFilePath(scriptPath) & _
              " --output " & QuoteFilePath(csvPath) & _
              " --log " & QuoteFilePath(logPath)

    Set runner = CreateObject("WScript.Shell")
    exitCode = runner.Run(command, 1, True)
    If exitCode <> 0 Then
        Err.Raise vbObjectError + 1004, , _
            "Pythonが失敗しました。終了コード: " & CStr(exitCode)
    End If
    RequireFile csvPath

    Set ws = ThisWorkbook.Worksheets.Add( _
        After:=ThisWorkbook.Worksheets(ThisWorkbook.Worksheets.Count))
    Set qt = ws.QueryTables.Add(Connection:="TEXT;" & csvPath, _
                                Destination:=ws.Range("A1"))
    With qt
        .TextFilePlatform = 65001
        .TextFileParseType = xlDelimited
        .TextFileTextQualifier = xlTextQualifierDoubleQuote
        .TextFileCommaDelimiter = True
        .TextFileTabDelimiter = False
        .TextFileSemicolonDelimiter = False
        .TextFileSpaceDelimiter = False
        .TextFileConsecutiveDelimiter = False
        .TextFileColumnDataTypes = Array(xlTextFormat, xlTextFormat)
        If Not .Refresh(BackgroundQuery:=False) Then
            Err.Raise vbObjectError + 1005, , "CSVの取り込みが中止されました。"
        End If
        If .FetchedRowOverflow Then
            Err.Raise vbObjectError + 1006, , "Excelの行数上限を超えています。"
        End If
    End With
    qt.Delete
    Set qt = Nothing

    MsgBox "取り込み完了: " & ws.Name & vbCrLf & _
           "結果とログの保存先: " & jobFolder, vbInformation
    Exit Sub
Failed:
    message = Err.Description
    On Error Resume Next
    If Not qt Is Nothing Then qt.Delete
    On Error GoTo 0
    MsgBox message & vbCrLf & "ログの保存予定先: " & logPath & vbCrLf & _
           "起動前や構文エラーではログがない場合もあります。", vbExclamation
End Sub

Run(command, 1, True)の第3引数が終了待ちを指定し、戻り値がプロセスの終了コードです。Falseを渡した場合に返る0は、Pythonの成功を意味しません。また、終了コード0でも期待する出力があるとは限らないため、ファイルの存在も確認しています。本番では件数・列数・内容の検証も追加してください。

CSVの読み込みではTextFilePlatformにUTF-8のコードページ65001を指定し、取り込み処理にもBackgroundQuery:=Falseを渡しています。TextFileColumnDataTypesで2列とも文字列にしているため、年齢も文字列として入ります。数値として使う列は、値を検証したうえで意図的に変換してください。列数を増やす場合は、この配列も実際の列構成に合わせます。

外部由来の値が=などで始まる場合や、商品コードの先頭ゼロを保持したい場合に、Excelの自動判定へ任せないことが大切です。ただし、この取り込み設定はCSV自体を書き換えません。同じCSVをダブルクリックなど別の方法で開く場合には、同じ扱いになるとは限りません。処理後はクエリを削除して取り込んだ値を残します。

一時フォルダーとログは、確認のため自動削除しない例です。機密情報を扱う場合は保存先・アクセス権・保管期間を業務の規程に合わせ、不要になったファイルを管理してください。

エラーハンドリングとデバッグのヒント

VBAのOn Errorで捕捉できるのは、VBA側の実行エラーや起動処理の失敗です。別プロセス内のPython例外が、直接VBAのErrになるわけではありません。同期例では終了コードを確認し、必要に応じてVBAのエラーへ変換しています。

  • Pythonが見つからない:実際に使うpython.exeと.pyの絶対パス、引用符、ファイルの存在を確認します。
  • ModuleNotFoundError:コマンドプロンプトで成功するPythonとVBAのPythonが同じか、sys.executableで確認します。追加パッケージはそのPythonの-m pipで管理します。
  • ファイルが見つからない:相対パスは現在の作業ディレクトリ基準です。入力・出力を絶対パスで渡すか、スクリプト自身を基準にPath(__file__).resolve().parentから組み立てます。
  • 失敗したのにログがない:Pythonの起動失敗、構文エラー、引数の誤り、ログの初期化失敗ではログが作られない場合があります。同じコマンドをコマンドプロンプトで実行して標準エラーを確認します。
  • Excelが待ち続ける:入力待ちやネットワーク待ちがないか調べます。Runの同期待ちには組み込みのタイムアウトがないため、長時間処理では別の実行・監視設計が必要です。
  • 文字化けや列ずれ:CSVの書き込みと読み込みで文字コード、区切り文字、引用符、列の型を一致させます。カンマ区切りを単純なSplitだけで解析しません。
  • WScript.Shellや外部実行が拒否される:組織の制限を回避するために設定を緩めず、管理者に許可された実行方法を確認します。

ログにはloggingモジュールで処理段階と例外情報を残すと切り分けやすくなります。APIキー、認証情報、不要な個人情報をログに出さないようにしてください。エラー後に無条件でResume Nextして結果取り込みへ進む実装も避けます。

標準出力や長時間の処理を扱う場合

標準出力を取得したい場合は、WScript.Shell.ExecのStdOut・StdErrなどを使う設計もあります。ただし、RunやVBAのShellが出力文字列を返すわけではありません。標準出力と標準エラーを適切に読み出さないと、出力量によってパイプの待ち合わせが起こることがあります。文字コード、プロセス終了、タイムアウトも別々に扱う必要があります。

本記事では、短い処理を同期実行し、結果とログをファイルで受け渡す方式に絞っています。長時間処理を非同期で動かすなら、ジョブごとの保存先、完了・失敗の通知、タイムアウト、Excel終了時の扱いまで設計します。単に「数秒待つ」だけで完了したと判断しないでください。

実践的な応用例

データ分析と可視化

pandasやNumPyで集計し、Matplotlibなどでグラフを画像保存した後、VBAから取り込めます。以下は任意の応用例です。前述の追加パッケージを導入し、このスクリプトと同じフォルダーにsample_data.xlsxを用意して、まず単独で確認してください。開いているExcelブックの未保存変更は、ディスク上のxlsxファイルを読む処理には反映されません。

from pathlib import Path
import pandas as pd
import matplotlib
matplotlib.use("Agg")  # 画面を開かずファイルへ保存する
import matplotlib.pyplot as plt

base = Path(__file__).resolve().parent
df = pd.read_excel(base / "sample_data.xlsx", engine="openpyxl")
numeric = df.select_dtypes(include="number")
if numeric.empty:
    raise ValueError("集計できる数値データがありません。")

mean_values = numeric.mean()
ax = mean_values.plot(kind="bar", title="Average Values")
output = base / "average_values.png"
try:
    ax.figure.tight_layout()
    # xbは排他的な新規作成。既存の画像がある場合はエラーにする。
    with output.open("xb") as image_file:
        ax.figure.savefig(image_file, format="png")
finally:
    plt.close(ax.figure)

savefigへ保存先のファイル名をそのまま渡すと、既存の画像を上書きすることがあります。この例はxbモードで新規作成し、average_values.pngが既にあれば上書きせずエラーにします。再実行時は、新しい出力名や確認済みの保存先を選んでください。保存中の失敗で不完全な新規ファイルが残った場合も、内容を確認して扱います。

使用APIはpandasのread_excel、select_dtypes、MatplotlibのFigure.savefigで確認できます。この応用例は構文確認のみで、追加パッケージを導入した実行検証は行っていません。使用するバージョンと入力ファイルで確認してください。

平均を計算する列は、業務上の意味に合わせて明示的に選ぶのが望ましい設計です。この例は数値列へ限定しており、文字列列も含めて一括で平均するものではありません。日本語のグラフ表示には利用環境の日本語フォント設定も確認してください。

大量データの処理

Pythonで前処理や集約を行い、Excelには確認に必要な結果だけを戻すと扱いやすくなります。Pythonへ移しただけでメモリ制約やExcelのシート上限がなくなるわけではありません。入力件数を確認し、分割処理・集計・保存先の分離などを検討します。

ウェブスクレイピングとデータ収集

許可されたAPIやWebページから取得したデータをPythonで整形し、CSVとしてExcelへ渡す応用も可能です。実装時は提供元の利用条件やアクセス制限を確認し、通信タイムアウト、HTTPエラー、取得項目の検証を追加します。空のデータや取得失敗を、正常な集計結果として扱わないようにしてください。

まとめ

  • 実行対象は、確認した仮想環境のpython.exeとPythonスクリプトの絶対パスで指定します。空白を含むパスは個別に引用します。
  • Shellは起動のみの非同期用途、Run(..., True)は短い処理の終了待ち用途として区別します。タスクID・終了コード・結果データは別物です。
  • Pythonの成功を終了コードで確認してから、今回の結果ファイルを読み込みます。エラーとログがない場合の切り分けも用意します。
  • CSVの文字コードと列型を明示し、自由入力をコマンド文字列へ埋め込まない設計にします。

まず短いスクリプトで、起動、正常終了、異常終了、日本語データの取り込みをそれぞれ確認してから、業務処理を追加しましょう。ここで示したWindows固有のVBA・COM処理は、利用するExcel環境と組織の実行制限に合わせた動作確認が必要です。

この記事を書いた人

実務の現場で詰まりがちなポイントを地図にするITブログ「IT trip」を運営。Windows/Office(Teams・Excel)からSQL、サーバ運用、ガジェットまで、再現性のある手順と“なぜそうなるか”を丁寧に解説します。読んだらすぐ試せること、そして迷った人の次の一歩が見えることを大切にしています。

コメント

コメントする

目次