VBAによるWebコンテンツの取得

# はじめに

同僚から、「ExcelからWebサイトをスクレイピングするVBAスクリプトがうまく動かなくなったので直して欲しい」という依頼を受けた際に調べたことをまとめました。

VBAでWebのコンテンツを取得するには、以下の4つの方法が代表的なようです。

-   InternetExplorer（以下、「IE」）をVBAから操作する。
-   HTMLDocumentのcreateDocumentFromUrlメソッドを使う。
-   XMLHTTP60を使い文字列としてコンテンツを取得しHTMLとしてパースする。
-   Selenium Webdriver を使う。

現時点での僕の方針を先に書くと、

1.  自由にソフトウェアがインストールできる環境なら、VBA以外の言語を検討
2.  静的ページが対象で、ページ内のリンクを辿る必要があるならHTMLDocument
3.  静的ページが対象で、ページ内のリンクを辿る必要が無いならXMLHTTP
4.  スクレイピング対象コンテンツがJavascriptで読み込まれるならIE

と、なります。

以下では、それぞれの方法のメリット・デメリットをまとめました。
Seleniumを使う方法以外については、使用例のコードを書きました。

# それぞれの手法のメリット・デメリット

## Internet ExplorerをVBAから操作する

ブラウザをプログラムから操作するので、およそ人間の操作であれば、ほぼそれをトレースして、プログラムに行わせることができます。
対象であるWebサイトがJavascriptでコンテンツを読み込んでいる場合には、この方法がもっとも有力に思えます。
一方プログラムを職場なんかの縛られた環境で利用する場合は、その環境でいつまでIEが利用できるのか注意を払っておく必要があります。

### メリット

-   Javascriptで読み込まれるコンテンツへの操作にも対応できる。
-   プログラムがブラウザの操作と結びついているので、非エンジニアにも何が起こっているのか分かりやすい。

### デメリット

-   取得したいコンテンツとは関係ない画像なども読み込むため、場合によっては遅い。
-   [「IEを使い続けることは技術的負債」](https://www.atmarkit.co.jp/ait/articles/1903/05/news011.html) のように言われている中で、新規にIEによるスクレイピングのコードを書くことは不安（職場の端末でIEがサポートされなくなる将来に、一度に改修を行う必要がでるかもしれない)。

## HTMLDocumentのcreateDocumentFromUrlメソッドを使う

1つのライブラリにしか依存しておらず、コードもシンプルに書けます。
静的なウェブサイトのリンクを巡回してコンテンツをダウンロードするような用途であれば、この方法がオススメです。

### メリット

-   IEに依存しない。
-   余計なコンテンツを取得しないので多分速い。

### デメリット

-   Javascriptで読み込まれるコンテンツには対応できない。

## XMLHTTP60で文字列としてコンテンツを取得しHTMLとしてパースする

HTTP GET メソッドを送って、それに対するレスポンス文字列をHTMLとしてパースする方法です。
欲しいコンテンツが相対URLで書かれていない場合や、HTMLとしてパースする必要のない、WebAPIを叩いてXMLやJSONを取得する場合には、この方法がオススメです。

### メリット

-   多分最も高速。
-   一度文字列を介するので、オブジェクトの管理が楽。

### デメリット

-   パースされたHTMLDocumentは元のURLの情報を持たないので、相対URLで書かれたリンクを扱うのに苦労する。
-   Javascriptで読み込まれるコンテンツには対応できない。

## Selenium Webdriver を使う(未検証)
VBAからSelenium Webdriverを扱うライブラリである [SeleniumBasic](https://github.com/florentbr/SeleniumBasic/releases) に関する、Web上のいくつかの解説を読んだだけで、実際にはコードを書いていません。

僕の印象では、VBAでスクレイピングをせにゃならん状況と言うのは、やむなくVBAを使ってることが多いです。
Selenium Basicを入れられるくらい自由に環境をいじれるなら、[Xlwings](https://www.xlwings.org/)でPythonとExcelを連携するとか、メインの処理はもっと書きやすい言語でやって最後にCSVを吐いてExcelから読ませるとかした方が良いようにも思います。

### メリット
-   Javascriptで読み込まれるコンテンツにIE以外のブラウザで対応できる
-   別の言語でSelenium Webdriverに慣れてる人には書きやすい

### デメリット
-   環境を整えるのが大変
-   複数のツールに依存するので問題の切り分けやデバッグが大変そう

# それぞれの方法でHTMLDocument を取得する関数

引数としてURLを与えて、HTMLDocumentオブジェクト（への参照）を返す関数として実装してみました。
返されるHTMLDocumentオブジェクトの生存期間が、他のオブジェクトの影響を受ける場合は、そのオブジェクトをグローバル変数とはせず、関数の引数として一緒に渡すことにしました。
こうしておく方が、変数名の変更なんかが自由にできると思います。

以下全ての例で、HTMLDocumentを使うため、VBEのメニューから
[ツール] → [参照設定]→ "Microsoft HTML Object Library" にチェック
をしておく必要があります。

## IE
-   [ツール] → [参照設定]→ "Microsoft Internet Controls" にチェックしておく
-   呼び出し元のIEオブジェクト（以下では `objIE`）が閉じられると、この関数が返したHTMLDocumentも参照先が無くなって以降の操作ができなくなるので、IEの生存期間には注意すること
   
```VisualBasic
Private Function getHTMLDoc_withIE(ByVal url As String, ByRef objIE As InternetExplorer) As HTMLDocument
  ' IE で対象ページを開く
  Call objIE.Navigate(url)

  ' 完全にページが表示されるまで待機する
  Do While objIE.Busy = True Or objIE.readyState <> READYSTATE_COMPLETE
    DoEvents
  Loop
  Do objIE.Document.readyState <> "complete"
    DoEvents
  Loop

  ' 関数の返り値に設定
  Set getHTMLDoc_withIE = objIE.Document
End Function
```

## HTMLDocumentのcreateDocumentFromUrlメソッド
-   呼び出し元となってるHTMLDocumentオブジェクト（以下では `htmldoc`）が失われると、この関数が返したHTMLDocumentも参照先が無くなって以降の操作ができなくなるので、オブジェクトの生存期間には注意すること

```VisualBasic
Private Function getHTMLDoc_withHTMLDoc(ByVal url As String, ByRef htmldoc As HTMLDocument) As HTMLDocument
  ' HTMLDoc オブジェクトで対象ページを開き関数の返り値に設定
  Set getHTMLDoc_withHTMLDoc = htmldoc.createDocumentFromUrl(url, vbNullString)

  ' 完全にページが表示されるまで待機する
  Do getHTMLDoc_withHTMLDoc.readyState <> "complete"
    DoEvents
  Loop
End Function
```

## XMLHTTP60
-   [ツール] → [参照設定]→ "Microsoft XML v6.0" にチェックしておく

```VisualBasic
Private Function getHTMLDoc(ByVal url As String) As IHTMLDocument
  Dim http As XMLHTTP60
  Set http = New XMLHTTP60

  ' GETメソッドで対象ページを取得
  Call http.Open("GET", url, False)
  Call http.Send()

  ' 待つ
  Do While http.readyState <> READYSTATE_COMPLETE
    DoEvents
  Loop

  ' 取得した文字列をHTMLとしてパースし関数の返り値に設定
  Set getHTMLDoc = New HTMLDocument
  Call getHTMLDoc.Write(http.responseText)
End Function
```
# 参考にしたWebサイト
[VBAでWebページを取得する方法](https://so-zou.jp/software/tech/programming/vba/sample/web.htm) @ so-zou.jp
[InternetExplorerオブジェクトのdocument.readyStateプロパティ](https://www.vba-ie.net/ieobject/documentreadystate.php) @ VBAのIE制御入門
[HTMLDocument Class](https://docs.microsoft.com/ja-jp/dotnet/api/system.windows.forms.htmldocument?view=netframework-4.8) @ Microsoft .NET
[VBAのスクレイピングを簡単楽にしてくれるSelenium](https://excel-ubara.com/excelvba4/EXCEL_VBA_401.html) @ エクセルの神髄
[Excel VBAコーディング ガイドライン案] (https://qiita.com/mima_ita/items/8b0eec3b5a81f168822d) @ Qiita