【Excel】Mac版VBAでHTTP通信を行う

# はじめに
Excelでwebブラウザの操作を自動化する最小機能のみ実装したフレームワークの「[TinySeleniumVBA](https://github.com/uezo/TinySeleniumVBA/blob/main/README.ja.md)」があります。
これは今のところActiveXを使用しているため、Windows専用になっています。ActiveXの部分を取り除き代替部分を[VBA-tools](https://github.com/VBA-tools)ライブラリー群を使えば、Mac上のExcelで動作するかもと取り組みました。

これまで、その準備を着々と進めてきました。

https://qiita.com/yaju/items/6bd6140de528346416e6

https://qiita.com/yaju/items/d090be8ba597a57c8851

TinySeleniumVBAでは、HTTP通信する上でActiveXの`MSXML2.ServerXMLHTTP`を使用しています。
MacではActiveX使用できないため、HTTP通信する方法を模索しました。
2016年でExcel for Mac 2011を使用した時の記事も参考にしました。

https://qiita.com/asakaguchi/items/2c0b73e8bae218a77d21

# 環境
Mac Book Pro(2.3 GHz 8コアIntel Core i9) Monterey 12.2.1
Microsoft Office Home and Student 2019

# VBA-Web
[VBA-tools](https://github.com/VBA-tools)ライブラリー群には、HTTP通信用に[VBA-Web](https://github.com/VBA-tools/VBA-Web)があります。

## HTTP通信部分の書き換え
TinySeleniumVBAのHTTP通信の部分は下記のようになっています。
今回は、これを採用します。

### Windows
```vb:WebDriver.cls
' Send HTTP request
Private Function SendRequest(method As String, url As String, Optional data As Dictionary = Nothing) As Dictionary
    Dim client As Object
    Set client = CreateObject("MSXML2.ServerXMLHTTP")
    
    client.Open method, url
    
    If method = "POST" Or method = "PUT" Then
        client.setRequestHeader "Content-Type", "application/json"
        client.send JsonConverter.ConvertToJson(data)
    Else
        client.send
    End If

    Do While client.readyState < 4
        DoEvents
    Loop

    Dim Json As Object
    Set Json = JsonConverter.ParseJson(client.responseText)
    Set SendRequest = Json
End Function
```

### Mac
```vb:WebDriver.cls
' Send HTTP request
Private Function SendRequest(method As String, url As String, Optional Data As Dictionary = Nothing) As Dictionary
    Dim client As Object
    Dim Response As WebResponse

    Set client = New WebClient
    If method = "POST" Or method = "PUT" Then
        Set Response = client.PostJson(url, Data)
    Else
        Set Response = client.GetJson(url)
    End If

    Dim json As Object
    Set json = JsonConverter.ParseJson(Response.Content)
    Set SendRequest = json
End Function
```

# 正しい値が返らない
TinySeleniumVBAのExapmleを動かしていくと、Googleの検索BOXを取得する処理があるのですが、ここで513エラーになってしまいます。

```vb:Exapmle.bas
' Open browser
Driver.OpenBrowser

' Navigate to Google
Driver.Navigate "https://www.google.co.jp/?q=selenium"

' Get search textbox
Dim searchInput
Set searchInput = Driver.FindElement(By.Name, "q")
```

## 調査
デバッグで追っていくと、Web_Curlの渡すパラメーターに何か問題がありそうです。

```
? Web_Curl
curl -i --connect-timeout 5 --max-time 15 --location 
-H 'User-Agent: VBA-Web v4.1.6 (https://github.com/VBA-tools/VBA-Web)' 
-H 'Accept: application/json' 
-H 'Content-Type: application/json' 
-H 'Content-Length: 94' 
-X POST 
-d '{"using":"css selector","value":"[name=\"q\"]","sessionId":"17b3467186550c38a92aeee9f809712e"}'
'http://localhost:9515/session/17b3467186550c38a92aeee9f809712e/element'
```

### Content-Length
最初に`Content-Length: 94`を外してみました。-dの値の桁数は94文字あるのですが、変換か何かでパラメーターの桁数が一致していないと正しく動作しないし、Content-Lengthがなくても動作するからです。

```vb:WebRequest.cls
Public Sub Prepare()
    ' Add/replace general headers for request
    SetHeader "User-Agent", Me.UserAgent
    SetHeader "Accept", Me.Accept
    If Me.method <> WebMethod.HttpGet Then
        SetHeader "Content-Type", Me.ContentType
        'SetHeader "Content-Length", VBA.CStr(Me.ContentLength)
    End If
End Sub
```

残念ながら、これでも513エラーになってしまいます。

### JSON
次はJSONを疑いました。
エラーになるJSONにはダブルクォテーションの囲み内にダブルクォテーションがあった場合に`\"`としてエスケープしています。これはJSONのエスケープ処理としては正しいです。

```
// エラーにならないJSON
'{"url":"https://www.google.co.jp/?q=selenium","sessionId":"17b3467186550c38a92aeee9f809712e"}'
// エラーになるJSON
'{"using":"css selector","value":"[name=\"q\"]","sessionId":"17b3467186550c38a92aeee9f809712e"}'
```

試しにエスケープされないように、中身をシングルクォテーションにしてみました。

```vb:WebDriver.cls
Private Function ToSelector(by_ As By, ByVal value As String) As Dictionary
        ElseIf by_ = By.Name Then
            #If Mac Then
                value = "[name='" + value + "']"
            #Else
                value = "[name=""" + value + """]"
            #End If
        End If
    End If
```

JSONは下記のように[name=\"q\"]→[name='q']になります。

```
'{"using":"css selector","value":"[name='q']","sessionId":"17b3467186550c38a92aeee9f809712e"}'
```

こうするとエラーにならずに正常にパラメーターが返ってくるようになりました。
それではとContent-Lengthを復活させてみたところ、タイムアウトエラーになってしまいました。
※その後、WireSharkで確認したところ、シングルクォーテーションが除去されて`"value":"[name=q]"`となっていたため、Content-Lengthが一致しない状態だったようです。

## WireSharkで確認
ターミナル画面で、Content-Lengthと`\"`としてエスケープした状態で、curlコマンドにセットすると正しく値が返ってきます。そうすると、VBAからの呼び出し時に何かが起きている。

ネットワーク・アナライザ・ソフトウェアである「WireShark」にMac版があることを知り、ダウンロードしました。
https://www.wireshark.org/download.html

Loopback:Io0でフィルターで`tcp.port==9515`をしてみました。

### 正常
ターミナル画面上でcurlコマンドで実行しました。バックスラッシュ`5c`は正しく表示されています。
![スクリーンショット 2022-05-02 23.20.50.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/14053/38a63025-1c1d-71b7-6463-d3eb2a56e558.png)

### 異常
VBA-WebからCurlコマンドで実行しました。バックスラッシュの部分が`80`に変換されてしまっています。

![スクリーンショット 2022-05-02 23.21.09.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/14053/39810cc4-c002-614e-5f8e-1e23c1614b40.png)

## 原因
VBA-Webでは、Macだとpopen関数を使用してcurlコマンドを呼び出しHTTP通信しています。
そして、popen関数は文字コードにMacJapanese(AppleがShift_JISを独自に拡張した文字コード)が使用されます。

http://fanblogs.jp/macyarounanoka/archive/78/0

[MacJapaneseのWikipedia](https://ja.wikipedia.org/wiki/MacJapanese)には、半角バックスラッシュ「\\」は、0x80に変換されると書かれています。
0x80 …… U+5C reverse solidus (= backslash)、半角バックスラッシュ「\\」（ASCII の0x5Cと同じ）

もう少し調べると下記の記述が見つかりました。

>Mac OS では、バージョン 7.1〜9.x で主流だった日本語文字コード MacJapanese において、0x5C に円記号が、0x80 にバックスラッシュ記号がそれぞれ別々のコードポイントに存在している
[バックスラッシュと円記号 (¥) 0x5C番地の問題について](https://ja.wikipedia.org/wiki/%E3%83%90%E3%83%83%E3%82%AF%E3%82%B9%E3%83%A9%E3%83%83%E3%82%B7%E3%83%A5)

## 対応方法1
VBA-Webを使用する前提で一部のプログラムを書き換えます。
半角バックスラッシュ「\\」と円記号(¥)は違うコードが割り当てられており、円記号(¥)が0x5cとなっている。
ということは、半角バックスラッシュ(\\)を円記号(¥)に置換すればいい。

### 変更前
```vb:WebClient.cls
Public Function PrepareCurlRequest(Request As WebRequest) As String
    ' Add method, data, and url
    web_Curl = web_Curl & " -X " & WebHelpers.MethodToName(Request.method)
    web_Curl = web_Curl & " -d '" & Request.body & "'"
    web_Curl = web_Curl & " '" & Me.GetFullUrl(Request) & "'"
```

### 変更後
日本語版の場合に半角バックスラッシュ(\\)を円記号(¥)に置換する処理に書き換える。

```vb:WebClient.cls
Public Function PrepareCurlRequest(Request As WebRequest) As String
    ' Add method, data, and url
    web_Curl = web_Curl & " -X " & WebHelpers.MethodToName(Request.method)
    Dim body As String: body = Request.body
    If Application.LanguageSettings.LanguageID(msoLanguageIDInstall) = 1041 Then
        body = Replace(body, "\", "¥")
    End If
    web_Curl = web_Curl & " -d '" & body & "'"
    web_Curl = web_Curl & " '" & Me.GetFullUrl(Request) & "'"
```

当初は原因が分からずVBA-Webのissuesに書いたのですが、最終的には自己解決してしまいました。

https://github.com/VBA-tools/VBA-Web/issues/474

## 対応方法2
実は、対応方法1が分かるまで別の方法で動かしていました。
WebDriver(chromedriver or msedgedriver)を起動させるためにAppleScriptでShellを動かすようにしているので、それを再利用します。

https://qiita.com/yaju/items/6bd6140de528346416e6

AppleScriptのShellでCurlコマンドを動かしたところ、半角バックスラッシュ(\\)のままで動作しました。
AppleScriptTask関数を利用する手順は2段階となります。

1. スクリプトファイルを「~/Library/Application Scripts/com.microsoft.Excel/」に保存する。
2. VBA内でAppleScriptTaskからスクリプトファイルを呼び出す。


```bash:shell.scpt
on synchandler(param)
    try
        set ret to do shell script param
        return ret
    on error number n
        return "error " & n
    end try
end synchandler

on ansynchandler(param)
    try
        set ret to do shell script param & " &"
        return ret
    on error number n
        return "error " & n
    end try
end ansynchandler
```

web_Curl変数には、Curlコマンドをセットします。

```vb
Dim result As String
result = AppleScriptTask("shell.scpt", "synchandler", web_Curl)
```

resultには下記の結果が返ってきます。
注意としてMacなので改行コードはLF(Chr$(13)の1文字で区切ります。
```
HTTP/1.1 200 OK
Content-Length:88
Content-Type:application/json; charset=utf-8
cache-control:no-cache

{"value":{"element-6066-11e4-a52e-4f735466cecf":"f78d74e7-ed73-4b67-af7f-e5e77c8b73cc"}}
```

### 文字コード
AppleScriptのShellでCurlコマンドを動かしたところ半角バックスラッシュ(\\)のままで動作したので、文字コードはMacJapaneseではないはず、では何になっているのでしょうか？

popenで文字コードを調べた方法を真似て確認してみました。

```vb
Dim result As String
result = AppleScriptTask("shell.scpt", "synchandler", "echo 'あいうえお'>/tmp/a.txt")
```

odコマンドで出力されたコードを確認します。
```bash
$ od -x /tmp/a.txt
0000000      81e3    e382    8481    81e3    e386    8881    81e3    0a8a
0000020
```
Unicodeの「あ」はU+3042、UTF-8では e3 81 82 の3byteになります。
https://orange-factory.com/sample/utf8/code3/e3.html#Hiragana

自PCはIntelCPUのためリトルエンディアンとなり、実際のコードはe38182＝UTF-8ということです。

# 最後に
ようやく技術的な理解と準備が出来ました。
TinySeleniumVBAのMac対応が最終目的です、名前にTinyが付いている通り最小限のコードにしたい。
VBA-Webで仕組みを理解したので、VBA-Webを使用しないで独自クラスを作成します。その際に将来的なことを考えて、UTF-8が使える対応方法2を使います。