はじめに
同僚から、「ExcelからWebサイトをスクレイピングするVBAスクリプトがうまく動かなくなったので直して欲しい」という依頼を受けた際に調べたことをまとめました。
VBAでWebのコンテンツを取得するには、以下の4つの方法が代表的なようです。
- InternetExplorer(以下、「IE」)をVBAから操作する。
- HTMLDocumentのcreateDocumentFromUrlメソッドを使う。
- XMLHTTP60を使い文字列としてコンテンツを取得しHTMLとしてパースする。
- Selenium Webdriver を使う。
現時点での僕の方針を先に書くと、
- 自由にソフトウェアがインストールできる環境なら、VBA以外の言語を検討
- 静的ページが対象で、ページ内のリンクを辿る必要があるならHTMLDocument
- 静的ページが対象で、ページ内のリンクを辿る必要が無いならXMLHTTP
- スクレイピング対象コンテンツがJavascriptで読み込まれるならIE
と、なります。
以下では、それぞれの方法のメリット・デメリットをまとめました。
Seleniumを使う方法以外については、使用例のコードを書きました。
それぞれの手法のメリット・デメリット
Internet ExplorerをVBAから操作する
ブラウザをプログラムから操作するので、およそ人間の操作であれば、ほぼそれをトレースして、プログラムに行わせることができます。
対象であるWebサイトがJavascriptでコンテンツを読み込んでいる場合には、この方法がもっとも有力に思えます。
一方プログラムを職場なんかの縛られた環境で利用する場合は、その環境でいつまでIEが利用できるのか注意を払っておく必要があります。
メリット
- Javascriptで読み込まれるコンテンツへの操作にも対応できる。
- プログラムがブラウザの操作と結びついているので、非エンジニアにも何が起こっているのか分かりやすい。
デメリット
- 取得したいコンテンツとは関係ない画像なども読み込むため、場合によっては遅い。
- 「IEを使い続けることは技術的負債」 のように言われている中で、新規に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 に関する、Web上のいくつかの解説を読んだだけで、実際にはコードを書いていません。
僕の印象では、VBAでスクレイピングをせにゃならん状況と言うのは、やむなくVBAを使ってることが多いです。
Selenium Basicを入れられるくらい自由に環境をいじれるなら、Xlwingsで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の生存期間には注意すること
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も参照先が無くなって以降の操作ができなくなるので、オブジェクトの生存期間には注意すること
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" にチェックしておく
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ページを取得する方法 @ so-zou.jp
InternetExplorerオブジェクトのdocument.readyStateプロパティ @ VBAのIE制御入門
HTMLDocument Class @ Microsoft .NET
VBAのスクレイピングを簡単楽にしてくれるSelenium @ エクセルの神髄
[Excel VBAコーディング ガイドライン案] (https://qiita.com/mima_ita/items/8b0eec3b5a81f168822d) @ Qiita