Excel for Mac (VBA) から HTTP GET する方法


お仕事の都合で、Excel for Mac 2011（正確には、Visual Basic for Applications）から Web API を呼び出す必要が生じたため、調べたことを備忘録として残しておきます。

## MacScript 関数を利用する方法
一番お手軽なのは、VBA の MacScript 関数を使って、AppleScript 経由で `curl` コマンドを叩く方法です。

```
Function getHttp(url As String, query As String) As String
    Dim command As String
    
    command = "do shell script  ""curl --get -d '" & query & "' " & url & """"
    getHttp = MacScript(command)
End Function
```
上記のコードを Visual Basic Editor[^1] を開いて、標準モージュール[^2]にコピー＆ペーストしてください。

[^1]: Excel の [ツール] メニューから [マクロ] -> [Visual Basic Editor] を選択すると、Visual Basic Editor の画面に切り替わります。
[^2]: 標準モジュールがなければ、[挿入] メニューから [標準モジュール] を選択すると、標準モジュール（Module1）が追加されます。

使用例として、[OpenWeatherMap](http://openweathermap.org) から天気情報を取得する手順を紹介します。[^3]

[^3]: via [Stack Overflow](http://stackoverflow.com/) での質問 [How do I issue an HTTP GET from Excel VBA for Mac 2011](http://stackoverflow.com/questions/15981960/how-do-i-issue-an-http-get-from-excel-vba-for-mac-2011)  の [Franco Rondini さんの回答](http://stackoverflow.com/a/15994195)

1. セル A1 に、天気を調べたい都市名を入力します。例えば、`Fukuoka,jp` と入力します。
2. セル A2 に、Web API の URL `http://api.openweathermap.org/data/2.1/forecast/city` を入力します。
3. セル A3 に、クエリーパラメーターを組み立てる式 `="q=" & A1` を入力します。 
4. セル A4 に、`=getHttp(A2, A3)` と入力します。

しばらくすると、セル A4 に福岡の天気情報が JSON 形式の文字列で出力されます。

お手軽ではあるんですが、少し反応が鈍いというか、なんとなく遅い感じがしました。わたしのお仕事的には、これで十分だったのですが、もっと良い手はないかと調べてみると、Stack Overflow でベストな解決策がありました。

## C ライブラリの popen 関数を利用する方法
[Stack Overflow](http://stackoverflow.com/) での質問 [How do I issue an HTTP GET from Excel VBA for Mac 2011](http://stackoverflow.com/questions/15981960/how-do-i-issue-an-http-get-from-excel-vba-for-mac-2011)  の [Franco Rondini さんの回答](http://stackoverflow.com/a/15994195)がそれです。

使い方は、先ほど紹介したとおりです（先ほどの使用例は、ここで紹介されていた例を使用しました）。ただし、セル A4 に投入する式を `=HTTPGet(A2, A3)` に変更してください。

```
Option Explicit

' execShell() function courtesy of Robert Knight via StackOverflow
' http://stackoverflow.com/questions/6136798/vba-shell-function-in-office-2011-for-mac

Private Declare Function popen Lib "libc.dylib" (ByVal command As String, ByVal mode As String) As Long
Private Declare Function pclose Lib "libc.dylib" (ByVal file As Long) As Long
Private Declare Function fread Lib "libc.dylib" (ByVal outStr As String, ByVal size As Long, ByVal items As Long, ByVal stream As Long) As Long
Private Declare Function feof Lib "libc.dylib" (ByVal file As Long) As Long

Function execShell(command As String, Optional ByRef exitCode As Long) As String
    Dim file As Long
    file = popen(command, "r")

    If file = 0 Then
        Exit Function
    End If

    While feof(file) = 0
        Dim chunk As String
        Dim read As Long
        chunk = Space(50)
        read = fread(chunk, 1, Len(chunk) - 1, file)
        If read > 0 Then
            chunk = Left$(chunk, read)
            execShell = execShell & chunk
        End If
    Wend

    exitCode = pclose(file)
End Function

Function HTTPGet(sUrl As String, sQuery As String) As String

    Dim sCmd As String
    Dim sResult As String
    Dim lExitCode As Long

    sCmd = "curl --get -d """ & sQuery & """" & " " & sUrl
    sResult = execShell(sCmd, lExitCode)

    ' ToDo check lExitCode

    HTTPGet = sResult

End Function
```

## 余談
これで、どれくらい早くなったかというと、気持ち早くなったという感じでしょうか。

あと、HTTPGet という関数名とかコードの記法にいまひとつ馴染めなかったので、以下のように書き換えて試しました。また、HTTP POST の方も postHttp という関数を追加して試したりしました。


```
Function getHttp(url As String, query As String) As String
    Dim command As String
    Dim result As String
    Dim exitCode As Long

    command = "curl --get -d """ & query & """" & " " & url
    result = execShell(command, exitCode)

    ' ToDo check exitCode

    getHttp = result
End Function

Function postHttp(url As String, query As String) As String
    Dim command As String
    Dim result As String
    Dim exitCode As Long

    command = "curl -d """ & query & """" & " " & url
    result = execShell(command, exitCode)

    ' ToDo check exitCode

    postHttp = result
End Function
```

P.S.
個人的には、最初に紹介したお手軽な方で、利用するかなーといった感じです。ベストな解決策がありましたと紹介しておいてアレなんですが . . . (^^;A