Excel VBAからcurl.exeを呼び出すと、外部APIのデータを取得してワークシートに取り込めます。ただし、コマンドを起動しただけでは通信の成功も、JSONの正しさも確認できません。本記事では、Windows版デスクトップExcelを対象に、終了待ち、HTTPステータス、UTF-8、JSON検証を分けて扱う方法を説明します。
実践例は、認証情報を使わず、東京の固定座標についてOpen-Meteoの気温データを1回取得するGETです。セルや入力欄から任意のURL・コマンド・JSON本文を受け取る汎用ツールではありません。APIキーを表示する例や、平文HTTPで送る例は使用しません。
VBAでcurl.exeを使用する準備
Windows 10・11にはcurlが同梱される構成があります。「Windowsには必ず別途インストールが必要」とは限りません。まず実際の実行ファイルとバージョンを確認し、必要ならcurl公式のWindows配布ページを利用してください。Windows側の更新と、別途導入したcurlの更新は区別します。
コマンドプロンプトでは次のように確認できます。2行目のパスは配置例なので、手元で確認した実行ファイルに置き換えます。PowerShellの別名との混同を避けるため、単なるcurlではなくcurl.exeを明示します。
where.exe curl.exe
"C:\Tools\curl\bin\curl.exe" -q --version- この例の前提はcurl 8.4.0以上です。
%output{ファイル名}は8.3.0以降、サイズ不明の転送中にも働く--max-filesizeは8.4.0以降です。これは最低機能要件であり、古い8.4.0を新規導入する推奨ではありません。保守・セキュリティ更新された版を使ってください。 --versionのProtocolsにHTTPSがあることを確認します。OS同梱版と別途導入版で機能・TLS実装が異なる場合があります。以下のVBAはバージョンを自動更新しません。- マクロを保存できる
.xlsmの検証用ブックを使います。組織のルールに従い、信頼できないマクロを有効にしたり、セキュリティ設定を一括解除したりしないでください。 - Windows Script Host、FileSystemObject、ADODB.Streamを使います。組織のポリシーで利用できない場合は管理者に相談します。Mac版ExcelやWeb版Excel向けのコードではありません。
機能の追加時期はcurlのmax-filesize解説とwrite-out解説で確認できます。実行ファイルはPATH検索に任せず、確認済みの絶対パスをVBAに設定します。
Shellの戻り値はAPIのレスポンスではない
VBAのShell関数は通常、プログラムを非同期で起動し、起動したタスクのIDを返します。戻り値はHTTP本文でもcurlの終了コードでもありません。Shellの直後にファイルを開く処理や、「2秒待てば終わる」と仮定する処理では、未完成・未作成のファイルや前回のデータを読む恐れがあります。MicrosoftのShell関数の説明も参照してください。
以下ではWScript.Shell.Run(コマンド, 0, True)でcurl.exe自体の終了を待ち、その戻り値を終了コードとして扱います。第3引数のTrueによる同期実行はMicrosoftのWSH使用例でも説明されています。待機中はExcelの操作が止まるため、小さな単発取得用です。接続5秒・転送全体20秒の制限をcurlに指定しますが、ローカルI/OやOS障害まで含めたExcelの厳密な実行時間保証ではありません。
curlコマンドの基礎と、Windowsでの引用符
curlの基本形はcurl.exe [オプション] [URL]です。GETは通常デフォルトなので、単純な取得で-X GETは不要です。よく使う指定は次のとおりです。
--header/-H:HTTPヘッダー。Accept: application/jsonは希望する応答形式、Content-Type: application/jsonは送信本文の形式を示します。--output/-o:応答本文をファイルへ保存。--stderr:診断出力を別ファイルへ保存。本文と診断を混ぜません。--silent --show-error:進捗を抑えつつ、curlのエラーは診断出力へ残します。--silentだけではエラー表示も抑制されます。--connect-timeout:接続段階の上限。--max-time:転送全体の上限。どちらも秒数です。--write-out:HTTPステータスなどの転送情報。応答本文とは別に取得します。--data-binary @ファイル:用意した本文を送る指定。通常はPOSTになります。-Xでメソッド名だけ変えても、そのAPIの正しい要求になるとは限りません。
POSIXシェル用の「シングルクォートでJSONを囲む」例を、そのままWindowsのcmd.exeへ貼り付けないでください。VBAの文字列内では二重引用符を""と書きますが、Windowsのコマンド引数の引用、curl設定ファイル内のエスケープ、JSON文字列のエスケープはそれぞれ別の規則です。どれか1つの置換ですべて安全になるわけではありません。
実践例ではcmd /cや>によるリダイレクトを使わず、実行ファイルと設定ファイルのパスだけをコマンドに渡します。URLや%{http_code}はcurl設定ファイルに入れます。設定ファイルは1行1オプションで、引用した値ではバックスラッシュを2個重ね、二重引用符の前にはバックスラッシュを付けます。仕様はcurl公式の設定ファイル解説を参照してください。
url = "https://api.open-meteo.com/v1/forecast?latitude=35.68&longitude=139.76¤t=temperature_2m"
output = "C:\\Example\\body.json"
write-out = "%output{C:\\Example\\status.txt}%{http_code}"これは設定ファイルの構文例であり、そのままコマンドプロンプトへ貼り付けるコマンドではありません。WSHのRunやcmd.exeの環境変数展開と、curl自身の設定読込・write-out置換は別物です。通常の設定値に%API_KEY%と書くだけで秘密情報が安全に注入される、と考えないでください。curl独自の--variable/--expand-*も別機能で、対応版と仕様の確認が必要です。
実践例:固定HTTPSの天気APIからExcelへ取り込む
実行前の確認
この例はOpen-Meteo Forecast APIのcurrent=temperature_2mを使い、東京の固定座標(35.68, 139.76)を指定します。個人の現在地は取得しません。現在値は気象モデルに基づくデータで、手元の温度計の測定値を保証するものではありません。無料APIは非商用利用向けです。商用利用・上限・ライセンスは公式の利用条件・料金案内を確認し、出典表示を残してください。
- VBA標準には汎用JSONパーサーがないため、VBA-JSONの公式リポジトリの導入手順に従って
JsonConverter.basを検証用ブックへインポートします。WindowsではVBEの「ツール → 参照設定」でMicrosoft Scripting Runtimeを参照します。利用するリリースとライセンスを確認してください。未導入なら以下はコンパイルできません。 - 以下の本体と補助関数を、同じ標準モジュールにすべて貼り付けます。
CURL_EXEを確認済みのcurl.exeの絶対パスへ変更します。--versionで8.4.0以上を確認する手順は省略しないでください。 - 簡潔で予測可能な引用処理にするため、このサンプルのローカルパスはASCII文字のドライブ絶対パスに限定し、
" % { }と制御文字を拒否します。日本語名を含むTEMPパスなどでは停止します。その場合はtempRootの代入先を、事前に用意した自分だけが使う書込み可能なローカルASCIIパスへ変更します。入力制約を外すだけで汎用化しないでください。 - ブック構成を保護していない検証用ブックで、
GetTokyoWeatherWithCurlを1回実行します。成功すると新しいシートに時刻・気温・出典を書きます。既存シートの値は更新しません。通信先・パラメーターはコード内で固定されています。
設計上は、実行ごとに新しい作業フォルダーを作り、本文・HTTP状態・診断を別々の絶対パスへ出力します。-qを最初の引数にして既存のcurl既定設定を読み込ませず、リダイレクト追従・圧縮展開・自動再試行も追加しません。なお、-qだけでプロキシや証明書関連などの環境設定がすべて無効になるわけではありません。組織で許可されたネットワーク設定を確認してください。
VBAの本体
Option Explicit
Public Sub GetTokyoWeatherWithCurl()
' Replace with the absolute path checked with --version (curl 8.4.0+).
Const CURL_EXE As String = "C:\Tools\curl\bin\curl.exe"
Const API_URL As String = "https://api.open-meteo.com/v1/forecast?" & _
"latitude=35.68&longitude=139.76¤t=temperature_2m" & _
"&temperature_unit=celsius&timezone=Asia%2FTokyo&forecast_days=1"
Dim fso As Object, sh As Object, config As Object
Dim root As Object, current As Object, units As Object
Dim tempRoot As String, workDir As String, configPath As String
Dim bodyPath As String, statusPath As String, errorPath As String
Dim httpStatus As String, jsonText As String, stage As String
Dim exitCode As Long, errorNumber As Long
Dim folderCreated As Boolean, cleanupFailed As Boolean
Dim message As String, temperature As Variant, stamp As Variant
Dim values(1 To 2, 1 To 3) As Variant, ws As Worksheet
On Error GoTo Failed
stage = "Preparation"
Set fso = CreateObject("Scripting.FileSystemObject")
Set sh = CreateObject("WScript.Shell")
CheckDemoPath CURL_EXE
If Not fso.FileExists(CURL_EXE) Then Err.Raise vbObjectError + 2401
tempRoot = fso.GetSpecialFolder(2).Path
' If this path is unsupported, use an existing private local ASCII folder.
CheckDemoPath tempRoot
workDir = fso.BuildPath(tempRoot, fso.GetTempName)
fso.CreateFolder workDir
folderCreated = True
configPath = fso.BuildPath(workDir, "request.cfg")
bodyPath = fso.BuildPath(workDir, "body.json")
statusPath = fso.BuildPath(workDir, "status.txt")
errorPath = fso.BuildPath(workDir, "curl-error.txt")
' All config text is ASCII. Do not add credentials or user-supplied options.
Set config = fso.CreateTextFile(configPath, False, False)
config.WriteLine "url = " & ConfigString(API_URL)
config.WriteLine "proto = " & ConfigString("=https")
config.WriteLine "silent"
config.WriteLine "show-error"
config.WriteLine "connect-timeout = 5"
config.WriteLine "max-time = 20"
config.WriteLine "max-filesize = 1048576"
config.WriteLine "header = " & ConfigString("Accept: application/json")
config.WriteLine "output = " & ConfigString(bodyPath)
config.WriteLine "stderr = " & ConfigString(errorPath)
config.WriteLine "write-out = " & _
ConfigString("%output{" & statusPath & "}%{http_code}")
config.Close
Set config = Nothing
stage = "curl execution"
' No cmd.exe, no redirection, and no fixed-duration guess at completion.
' -q is first so an existing default curl config is not loaded.
exitCode = sh.Run(CommandPath(CURL_EXE) & " -q --config " & _
CommandPath(configPath), 0, True)
If exitCode <> 0 Then
message = "curl failed. Exit code: " & CStr(exitCode)
GoTo CleanUp
End If
stage = "HTTP status"
' A write-out file failure may NOT change curl's exit code.
If Not fso.FileExists(statusPath) Then Err.Raise vbObjectError + 2402
If fso.GetFile(statusPath).Size <> 3 Then Err.Raise vbObjectError + 2403
httpStatus = ReadUtf8File(statusPath)
If Not httpStatus Like "[1-5]##" Then Err.Raise vbObjectError + 2404
If httpStatus <> "200" Then
message = "API did not return HTTP 200. HTTP status: " & httpStatus
GoTo CleanUp
End If
stage = "JSON parsing and schema"
If Not fso.FileExists(bodyPath) Then Err.Raise vbObjectError + 2405
If fso.GetFile(bodyPath).Size = 0 Then Err.Raise vbObjectError + 2406
If fso.GetFile(bodyPath).Size > 1048576 Then Err.Raise vbObjectError + 2407
jsonText = ReadUtf8File(bodyPath)
Set root = JsonConverter.ParseJson(jsonText)
If TypeName(root) <> "Dictionary" Then Err.Raise vbObjectError + 2408
Set current = RequiredDictionary(root, "current")
Set units = RequiredDictionary(root, "current_units")
If Not current.Exists("time") Then Err.Raise vbObjectError + 2409
If Not current.Exists("temperature_2m") Then Err.Raise vbObjectError + 2410
If Not units.Exists("temperature_2m") Then Err.Raise vbObjectError + 2411
If VarType(units("temperature_2m")) <> vbString Then Err.Raise vbObjectError + 2412
If units("temperature_2m") <> (ChrW(176) & "C") Then Err.Raise vbObjectError + 2413
stamp = current("time")
If VarType(stamp) <> vbString Then Err.Raise vbObjectError + 2414
If Not stamp Like "####-##-##T##:##" Then Err.Raise vbObjectError + 2415
temperature = current("temperature_2m")
Select Case VarType(temperature)
Case vbByte, vbInteger, vbLong, vbSingle, vbDouble, vbCurrency, vbDecimal
' A JSON number, not null, Boolean, or a numeric-looking string.
Case Else
Err.Raise vbObjectError + 2416
End Select
stage = "Worksheet output"
values(1, 1) = "Time (Asia/Tokyo)"
values(1, 2) = "Temperature (" & ChrW(176) & "C)"
values(1, 3) = "Source"
values(2, 1) = stamp
values(2, 2) = temperature
values(2, 3) = "Open-Meteo.com (CC BY 4.0)"
Set ws = ThisWorkbook.Worksheets.Add(After:= _
ThisWorkbook.Worksheets(ThisWorkbook.Worksheets.Count))
ws.Range("A1:C2").NumberFormat = "@"
ws.Range("B2").NumberFormat = "0.0"
ws.Range("A1:C2").Value2 = values
ws.Range("C3").Value2 = "https://open-meteo.com/"
ws.Range("C4").Value2 = "https://creativecommons.org/licenses/by/4.0/"
ws.Columns("A:C").AutoFit
message = "Completed. A new worksheet contains the validated result."
GoTo CleanUp
Failed:
errorNumber = Err.Number
' Do not display raw response data, full commands, or credentials.
message = "Stopped at: " & stage & ". VBA error: " & CStr(errorNumber)
CleanUp:
On Error Resume Next
If Not config Is Nothing Then config.Close
If folderCreated Then
Err.Clear
fso.DeleteFolder workDir, True
cleanupFailed = (Err.Number <> 0)
End If
On Error GoTo 0
If cleanupFailed Then message = message & vbCrLf & _
"Temporary files remain. Remove this run's folder: " & workDir
MsgBox message, vbInformation
End Sub同じ標準モジュールに追加する補助関数
Private Function RequiredDictionary(ByVal parent As Object, ByVal key As String) As Object
If Not parent.Exists(key) Then Err.Raise vbObjectError + 2420
If Not IsObject(parent(key)) Then Err.Raise vbObjectError + 2421
If TypeName(parent(key)) <> "Dictionary" Then Err.Raise vbObjectError + 2422
Set RequiredDictionary = parent(key)
End Function
Private Sub CheckDemoPath(ByVal path As String)
Dim i As Long, n As Long, c As String
' Deliberately narrow demo: local drive, ASCII, no expansion/delimiter chars.
If Len(path) < 4 Then Err.Raise vbObjectError + 2430
If Not Left$(path, 3) Like "[A-Za-z]:\" Then Err.Raise vbObjectError + 2431
If Right$(path, 1) = "\" Then Err.Raise vbObjectError + 2432
For i = 1 To Len(path)
c = Mid$(path, i, 1)
n = AscW(c)
If n < 32 Or n > 126 Then Err.Raise vbObjectError + 2433
If InStr(1, """%{}", c, vbBinaryCompare) > 0 Then _
Err.Raise vbObjectError + 2434
Next i
End Sub
Private Function CommandPath(ByVal path As String) As String
CheckDemoPath path
CommandPath = Chr$(34) & path & Chr$(34)
End Function
Private Function ConfigString(ByVal value As String) As String
' curl config-file quoting is different from VBA/Windows command quoting.
value = Replace(value, "\", "\\")
value = Replace(value, Chr$(34), "\" & Chr$(34))
ConfigString = Chr$(34) & value & Chr$(34)
End Function
Private Function ReadUtf8File(ByVal path As String) As String
Dim stream As Object, savedNumber As Long
On Error GoTo Failed
Set stream = CreateObject("ADODB.Stream")
stream.Type = 2 ' adTypeText
stream.Charset = "utf-8"
stream.Open
stream.LoadFromFile path
ReadUtf8File = stream.ReadText(-1)
stream.Close
Exit Function
Failed:
savedNumber = Err.Number
On Error Resume Next
If Not stream Is Nothing Then stream.Close
On Error GoTo 0
Err.Raise savedNumber
End Function本文はADODB.StreamでUTF-8として読みます。ANSI前提のテキスト入力では、日本語や記号が文字化けすることがあります。文字コードの指定はMicrosoftのCharsetプロパティ、ファイル読込はStreamオブジェクトの説明を参照してください。
JSONとして読めた後も、currentとcurrent_unitsがオブジェクトか、必要なキーがあるか、気温がnullや文字列ではなく数値か、単位が°Cかを確認します。時刻はこのAPI例の分単位のISO形式を確認して文字列として保存します。この形式チェックは暦としての妥当性やデータの鮮度を完全に検証するものではありません。JSON全体を数式として評価したり、InStr(..., "error")だけで成功・失敗を決めたりしません。
エラーハンドリングは3段階で考える
- 起動・転送:実行ファイルが見つからない、権限がない場合はVBAの例外です。curlが起動できても、DNS・接続・TLS・タイムアウト・ファイル保存などで終了コードが0以外になることがあります。本文が存在しても、その部分データは成功扱いしません。
- HTTP:curlの終了コード0は、HTTP 200の保証ではありません。この例は
--failを使わずHTTP状態を別に検査し、このGETで期待する200だけを受け入れます。3xxも追従せず停止します。 - JSON・業務仕様:HTTP 200でもHTML、壊れたJSON、欠けたキー、null、想定外の単位なら取り込まず停止します。ここでは必要なフィールドだけを検証しています。実務では値の範囲、更新日時、業務上の整合性も検証します。
write-outの出力先ファイルを作れなかった場合、curlの終了コードが変わらないことがあります。そのため、この例は毎回新しいフォルダーを使い、ステータスファイルの存在・3バイト・3桁形式を確認します。前回のファイルが残っているだけで成功と判断する設計にはしません。
代表的な失敗と対処
- 終了コード6・7:名前解決や接続失敗を調べます。URL、DNS、許可されたプロキシ、ネットワークを確認します。
- 終了コード28:設定したタイムアウトです。固定の待機秒数を増やしてファイルを読む方法ではなく、応答遅延・接続の原因を調べます。
- 終了コード60:TLS証明書の検証失敗を調べます。時刻、信頼ストア、正式な証明書設定を確認し、
--insecureで検証を無効にして回避しません。 - 終了コード63:ダウンロードのサイズ制限です。この小さな取得で1 MiBを超える理由を確認します。制限を無条件に外さないでください。
- HTTP 401・403:認証・権限・利用条件などを確認します。認証不要の例で返った場合も、応答を成功データとして処理しません。
- HTTP 429・5xx:レート制限やサービス側の状態を確認します。この例は自動再試行しません。後から再試行機構を付けるなら回数・総時間・待機間隔を制限し、APIのRetry-After等の指示に従います。
--fail-with-bodyはHTTPエラー時にも本文を残しながら非0終了にできる別の選択肢で、curl 7.76.0以降です。しかし、それを付けてもJSONの構造や業務上の成功まで保証されるわけではありません。curlの終了値は公式の終了コード解説で確認してください。
POST・PATCHなど更新処理では、タイムアウトしてもサーバー側で処理が完了している可能性があります。失敗したように見えるからといって同じ注文・登録を無条件に再送しないでください。冪等性キー、状態照会、重複防止のルールをそのAPIの仕様に従って設計します。
POSTとJSON本文を扱う場合の拡張方針
この取得例をそのまま販売登録や注文APIに転用しないでください。送信先、HTTPメソッド、必要なヘッダー、認証、JSONスキーマを先に確定し、検証環境で確認します。ユーザー入力やセルの文字列をコマンドへ連結したり、引用符を手で足してJSONを作ったりしないことが基本です。
JSONパーサー/シリアライザーでオブジェクトから本文を生成し、API仕様に合うUTF-8ファイルとして保存したうえで--data-binary @ファイルを使うと、コマンド行へのJSON直書きを避けられます。ファイル名を渡す構文と、そのJSON自身のエスケープは別に処理します。--json @ファイルはcurl 7.82.0以降の代替ですが、curlがJSONの妥当性を検証してくれる機能ではありません。詳しくはcurl公式のJSON送信解説を参照してください。ここでは誤送信を避けるため、実行可能な更新APIの例は載せていません。
セキュリティ対策:APIキーを「隠したつもり」にしない
認証付きAPIでは、ブックに埋め込んだキー、非表示セル、VBAプロジェクトの閲覧制限だけを秘密保管庫として扱わないでください。環境変数はソースコードから値を分離する方法ですが、暗号化された保管庫ではなく、同じ権限のプロセスや子プロセスなどから参照される場合があります。「環境変数に入れたので必ず保護される」とは言えません。
- APIキーやトークンを
MsgBox、Debug.Print、ワークシート、スクリーンショットに表示しません。コマンド全体やHTTP本文の一括ログにも秘密情報が含まれ得ます。 - コマンド行にヘッダーやURLとして秘密情報を埋めると、プロセスのコマンド行や診断記録に残る可能性があります。HTTPSでもローカルのログ漏えいは防げません。クエリ文字列のキーは特にURL記録に注意が必要です。
- 設定ファイルなら自動的に安全になるわけではありません。平文ファイル、権限、バックアップ、同期先、残存期間を確認します。このサンプルの一時ファイル処理は公開データ専用であり、秘密を安全消去する仕組みではありません。
- 認証が必要な実装は、Windowsの資格情報管理などの承認された保管方法、短命・最小権限のトークン、またはサーバー側の中継処理を検討します。デスクトップに配布した長期の共有キーを完全に秘匿できる、とは考えないでください。
- 漏えいが疑われるキーは失効・再発行し、利用状況を調べます。更新の頻度は提供元の仕様と運用ポリシーに従います。平文HTTPへの送信や証明書検証の無効化は避けます。
任意のURLを入力できる仕組みに拡張すると、意図しないホストへの送信や別プロトコルへのアクセスも問題になります。通信先・スキーム・パラメーターを許可リストで制限し、リダイレクト先にも同じ方針を適用します。本例は固定HTTPS先だけを使い、セルからURL・追加オプション・ファイル名を取り込みません。
応用例:天気と販売データを統合する際の設計
天気と販売データの比較という用途は有用ですが、存在しないGetSalesDataを呼ぶだけでは動くプログラムになりません。販売側のAPI仕様・認証・権限・データ契約を確認し、別の取得・検証処理を実装してから統合します。業務利用では天気APIの利用条件も再確認してください。
- 天気と販売それぞれに取得時刻、対象期間、成功・失敗、データ版を記録します。片方だけ更新できた場合に、前回値を今回の値のように混ぜないでください。
- 店舗や地域と座標の対応、日付境界・タイムゾーン、気温や売上の単位、欠損値・重複をそろえます。「同じ日付の列がある」だけで結合しません。
- 検証済みの中間データを用意し、両方が利用可能になってから分析用シートへ反映します。CSVや文字列をセルへ入れる場合も、意図しない数式として解釈されない扱いが必要です。
- ピボットテーブルやグラフで比較できますが、単なる相関を天気による売上変化の因果関係と断定しないでください。曜日、休日、販促などの要因も検討します。
検証範囲とまとめ
掲載コードはWindows VBA向けの実装例です。本稿の確認では、HTMLとVBAを静的に点検し、curlの設定ファイル・HTTP状態・失敗分岐はローカルの模擬HTTPサーバーで検証しています。Windows版Excel上での実行や、Open-Meteoへの実通信を検証済みとするものではありません。実機では、利用するcurl版、COMオブジェクト、JSONライブラリ、パスと権限を確認し、検証用ブックから試してください。
重要なのは、起動と終了を区別し、curlの終了コード、HTTPステータス、JSONの必要な構造を順番に確認することです。固定秒数の待機、古いファイルの再利用、応答中の文字列だけによるエラー判定を避け、UTF-8と引用符の規則を分けて扱えば、API取得をより予測可能にできます。認証情報を扱う実務への拡張は、公開データの取得例とは別のセキュリティ設計として進めましょう。


コメント