【VBA / IE】スクレイピングの基本操作のみ

# はじめに
備忘のため、VBAのスクレイピングについての基本操作を書いておきます。
初心者には難しい用語もあるかもしれませんが、やってみると**以外に簡単**です。

用語については、折々で「[エクセルの神髄](https://excel-ubara.com/)」というサイトのリンクを付けさせていただいています（あいまいな理解を解消してくれる良いサイトです）。
また、私の知識の多くは「[VBAのIE制御入門](https://www.vba-ie.net/)」というサイトで学ばせていただいています。

なお、スクレイピングは、**場合によっては違法**と判断されるため、[Webスクレイピングの注意事項一覧](https://qiita.com/nezuq/items/c5e827e1827e7cb29011)という記事などを参照しておくことをお勧めします。

**＜目次＞**
[1. ブラウザを開くだけのコード](#1-ブラウザを開くだけのコード)
[2. WEBページ中から必要な情報を抽出するコード](#2-webページ中から必要な情報を抽出するコード)
[3. テキストボックスに文字入力して検索を実行するコード](#3-テキストボックスに文字入力して検索を実行するコード)

# 1. ブラウザを開くだけのコード

## 1-1. 参照設定をしない場合

事前の設定（[参照設定](https://excel-ubara.com/excelvba4/EXCEL296.html)）をしなくとも、次のように書くことで、WEBブラウザを表示することができます。

### 1-1-1. ソースコード

```vb
Sub IEGet01()
    Dim objIE As Object 'オブジェクト型の変数を宣言
    Set objIE = CreateObject("InternetExplorer.Application") '外部ライブラリからインスタンスを作成
    objIE.navigate "https://www.amazon.co.jp/" 'navigateメソッドで指定したURLのページを開く
    objIE.Visible = True 'ブラウザを表示させる
    Set objIE = Nothing 'オブジェクトの解放（無くとも問題ないと思う）
End Sub
```

これは、`CreateObject関数`を使用して、コードの実行時に、[外部ライブラリ（ActiveXオブジェクト）](https://excel-ubara.com/excelvba1/EXCELVBA438.html)からインスタンス（objIE）を作成しています。
この方法は「[実行時バインディング](https://excel-ubara.com/excelvba1/EXCELVBA438.html)」と言われています。

なお、バインディングとかインスタンスとか、意味が分からなくても大丈夫です。
このコードであれば、何の前提作業も不要で、とりあえず動作します。

インスタンスとかオブジェクトなどの基礎知識を知りたい方は、[こちら](https://excel-ubara.com/vba_class/VBA_CLASS_01.html)を参照してください。

### 1-1-2. 簡単な説明

**1行目**の`Dim objIE As Object`で、オブジェクト型の変数`objIE`を宣言します。変数なので別名でも構いません。

**2行目**の`Set objIE = CreateObject("InternetExplorer.Application")`で、[外部ライブラリ（ActiveXオブジェクト）](https://excel-ubara.com/excelvba1/EXCELVBA438.html)からインスタンス（`InternetExplorerオブジェクト`）を生成しています。

**3行目**の`objIE.navigate "https://www.amazon.co.jp/"`では、navigateメソッドを使用して指定したURLのページを開いています。

**4行目**の`objIE.Visible = True`は、ブラウザを表示する状態に設定しています。
これがないと、「ブラウザは立ち上がっているものの画面には表示されない」という状態になります。

**5行目**の`Set objIE = Nothing`では、生成したインスタンス（オブジェクト）を解放（破棄）しています。
オブジェクトを解放しないと、プログラム終了後もメモリに残ってしまうためですが、これが無くとも構わないという[見解](https://thom.hateblo.jp/entry/2015/12/20/135035)も有力です（C言語でも同じ論争があります）。

## 1-2. 参照設定をする場合

一般的には、先に[参照設定](https://excel-ubara.com/excelvba4/EXCEL296.html)を行ってコードを記述します。

### 1-2-1. 参照設定の方法

VBAのメニュー「ツール」から「参照設定(R)」を選択します。
![2020-11-03 115347.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/551412/29e53a15-e2a4-6e1c-699c-eddeb7e4a5a2.png)
次に、参照設定の画面から、次の2つの項目を探してチェックを入れます。

`Microsoft HTML Object Library`
`Microsoft Internet Controls`
![2020-11-03 104505.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/551412/e54dc705-abac-425f-54b1-bc663f75979c.png)
チェックを入れたら`OK`ボタンを押して完了です。

### 1-2-2. ソースコード

```vb
Sub IEGet02()
    Dim objIE As InternetExplorer '変数を宣言
    Set objIE = New InternetExplorer 'インスタンスを作成
    objIE.navigate "https://www.amazon.co.jp/"
    objIE.Visible = True
End Sub
```

参照設定を行っているので、**1行目**で直接`InternetExplorerオブジェクト`を変数として宣言することができます。
事前にライブラリを読み込むために、用語としては「[事前バインディング](https://excel-ubara.com/excelvba1/EXCELVBA438.html)」といいます。

これにより、**2行目**のインスタンス生成でも、CreateObject関数を使用する必要はなく、単に`New [クラス名]`という形で記述することができます。

以降は最初の例と同じですが、`Set objIE = Nothing`というオブジェクトの解放の部分は省略しています（[こちらの見解](https://thom.hateblo.jp/entry/2015/12/20/135035)に基づけば、書かなくても問題ないということになります）。

# 2. WEBページ中から必要な情報を抽出するコード

## 2-1. WEBページのHTMLの確認

例として、あまり実用性がないですが、yahooのトップページにある「サービス一覧」の部分のHTMLを取得する方法を考えてみます。
![2020-11-03 130048.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/551412/d2a6abba-8182-e119-f3d4-8ab1348bf9c2.png)
該当部分を右クリックして「検証(I)」を選択します。
![2020-11-03 130541.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/551412/490fe826-6cfe-9b54-ee20-daab890a8877.png)
するとWEBページのHTMLが表示されて、対象部分が青色になっていますので、このあたりが「サービス一覧」のHTML部分と言うことが分かります。
実際には、赤枠の部分が該当するHTMLになります。
![2020-11-03 130953.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/551412/87233f12-7216-58d8-cf85-0c3a768c6db2.png)
このHTMLをVBAで取得してみます。
取得に際しては、1行目の`id="ToolFooter"`の部分を利用することにします。

## 2-2. HTMLを取得するコード

該当部分のHTMLを取得するコードを書くと次のとおりです。

```vb
Sub IEGet03()
    Dim objIE As InternetExplorer '変数を宣言
    Set objIE = New InternetExplorer 'インスタンスを作成
    objIE.navigate "https://www.yahoo.co.jp/"
    objIE.Visible = True
    
    'WEBページの読み込みを待つ
    Do While objIE.Busy = True Or objIE.readyState <> 4
        DoEvents
    Loop
    
    'ID名から要素を特定してHTMLを表示する
    Debug.Print objIE.document.getElementById("ToolFooter").outerHTML
    
End Sub
```

### 2-2-1. WEBページの読み込み待ち

WEBページの読み込みが完了してからでないと、HTMLの読み取り等の操作ができません。
そのため、次のコードで、ブラウザが読み込みを完了するのまで待機します。

```vb
Do While objIE.Busy = True Or objIE.readyState <> 4
    DoEvents
Loop
```

上記は定型で、どのサイトでも大体このような形で書いています。

[Busyプロパティ](https://docs.microsoft.com/en-us/previous-versions/windows/internet-explorer/ie-developer/platform-apis/aa752050(v=vs.85))は、Boolean型であり、WEBページの読み込み中（遷移中、ダウンロード中）の場合に`True`を返します。
[ReadyStateプロパティ](https://docs.microsoft.com/en-us/previous-versions/windows/internet-explorer/ie-developer/platform-apis/aa752066(v=vs.85))は、数値型（Long型）であり、次のように、0から4の[定数](https://docs.microsoft.com/en-us/previous-versions/windows/internet-explorer/ie-developer/platform-apis/bb268228(v=vs.85))が割り当てられています。

|ステータス|数値|説明|
|---|---|---|
|READYSTATE_UNINITIALIZED|0|デフォルトの初期状態|
|READYSTATE_LOADING|1|オブジェクトの属性（プロパティ）読み込み中|
|READYSTATE_LOADED|2|オブジェクトの初期化の完了|
|READYSTATE_INTERACTIVE|3|オブジェクトは使用可能だが、全てのデータは利用はできない|
|READYSTATE_COMPLETE|4|全てのデータ受信完了|

以上は、テキトウ翻訳なので、詳しくは「[VBAのIE制御 / InternetExplorerオブジェクトのReadyStateプロパティ](https://www.vba-ie.net/ieobject/readystate.php)」を参照してください。

結論として、Busyプロパティが`False`になり、かつ、ReadyStateプロパティが`4`になるまでは「ページ読み込み中」と判断してループを繰り返し、両方の条件が成立したら「読み込み完了」と判断して、次の処理に進むということを行っているわけです。

### 2-2-2. IDから要素（ノード）を取得する方法

ページの読み込みが完了したら、必要なHTMLの取得処理に移ります。
IDを元にしてHTMLの要素（ノード）を取得するには、次のように`getElementById`メソッドを使用します。

```vb
objIE.document.getElementById("ToolFooter").outerHTML
```


頭の部分の`objIE.document`で、ページ全体の要素（ノード）を取得しています。
次に、このページ全体のノードから、`getElementById`メソッドを使用して絞り込みをかけてます。括弧内の`"ToolFooter"`はID名です。
最後の`outerHTML`プロパティで、抽出したノードのHTMLを取得しています。

### 2-2-3. 要素（ノード）を取得するメソッドの一覧

要素（ノード）を取得するためのメソッドには次のようなものがあります。

|メソッド|内容|取得形式|
|---|---|---|
|getElementById()|ID名から取得|単独|
|getElementsByClassName()|class名から取得|複数（コレクション）|
|getElementsByName()|name名から取得|複数（コレクション）|
|getElementsByTagName()|タグ名から取得|複数（コレクション）|
|getAttribute()|属性値を取得|単独|

class名、name名、タグ名から要素を取得する場合は、配列形式（コレクション形式）で取得されます。
そのため、1つの要素（ノード）を特定するには、次のように、末尾に要素番号`(0)`を記載する必要があります。

```vb
objIE.document.getElementsByClassName("_3wZKJtfV8AYYaTP-zoyO9O ult__mods")(0).outerHTML
```

属性値については、次のように取得したいノードを特定してから`getAttributeメソッド`を使用します。
これによりクラス名が返されます。

```vb
objIE.document.getElementById("ToolFooter").getAttribute("class")
```

分かる方には分かると思いますが、基本的に`JavaScript`のメソッドと同じです。

# 3. テキストボックスに文字入力して検索を実行するコード

次は、yahooのトップページにある検索フォームに文字を入力して検索を実行するコードです。
検索フォームは、次のような感じです。
![2020-11-03 164525.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/551412/1045ab39-e530-ad21-e1da-bbf75659a9a7.png)

## 3-1. HTMLの確認
検索フォームのHTMLは次のように、formタグで括られた範囲となります。
![2020-11-03 164933.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/551412/4c6115bd-4563-626a-3a23-feaa8591685e.png)
このうち、赤枠部分が`テキストボックス`で、青枠部分が`検索ボタン`となります。

**テキストボックス**は、name属性`name="p"`が一意なので、これで要素を取得できます。

**検索ボタン**は、class名`class="_63Ie6douiF2dG_ihlFTen rapid-noclick-resp"`が一意なので、これを利用して要素を取得します。
なお、このクラスは半角スペースで区切られて2つのclass名を持っているので、ここでは、前半の`_63Ie6douiF2dG_ihlFTen`をclass名として使用します。

## 3-2. ソースコード（上手くいかない例）

一般的には、次のようなコードでテキストボックスに文字を入力して検索を実行します。
ただ、<font color="red">yahooのサイトでは、これではうまくいきません</font>（解決方法は後述します）。

```vb
Sub IEGet04()
    Dim objIE As InternetExplorer '変数を宣言
    Set objIE = New InternetExplorer 'インスタンスを作成
    objIE.navigate "https://www.yahoo.co.jp/"
    objIE.Visible = True
    
    'WEBページの読み込みを待つ
    Do While objIE.Busy = True Or objIE.readyState <> 4
        DoEvents
    Loop
    
    objIE.document.getElementsByName("p")(0).Value = "テスト" '検索文字を入力
    objIE.document.getElementsByClassName("_63Ie6douiF2dG_ihlFTen")(0).Click '検索ボタンをクリック
End Sub
```

### 3-2-1. テキストボックスに文字を入力する

テキストボックスに文字を入力するコードは次の部分です。

```vb
objIE.document.getElementsByName("p")(0).Value = "テスト"
```

取得したテキストボックスのノードについて、`Valueプロパティ`を使用して、`"テスト"`という文字列を代入しています。

### 3-2-2. 検索ボタンをクリックする

検索ボタンをクリックするコードは次の部分です。

```vb
objIE.document.getElementsByClassName("_63Ie6douiF2dG_ihlFTen")(0).Click
```

取得した検索ボタンのノードについて、`Clickメソッド`を使用して、検索ボタンをクリックする処理を行っています。

### 3-2-3. 検索が実行されないことについて

コードを実行してみると、テキスト入力もできて、検索ボタンも押しているのに、検索結果が表示されないという状態になります（私の環境では）。
挙動を確認してみると、`キーボードから入力された文字列`にしか検索が実行されないようです。

## 3-3. ソースコード（上手くいく例）

テキストボックスへの入力部分に難があるようなので、今度は`SendKeysメソッド`でキーボード入力をして検索を実行するようにしました。
次のとおりです。

```vb
'Win32APIからSleep関数を使用するための宣言
#If VBA7 Then
    Private Declare PtrSafe Sub Sleep Lib "kernel32" (ByVal dwMilliseconds As LongPtr)
#Else
    Private Declare Sub Sleep Lib "kernel32" (ByVal dwMilliseconds As Long)
#End If

Sub IEGet05()
    Dim objIE As InternetExplorer '変数を宣言
    Set objIE = New InternetExplorer 'インスタンスを作成
    objIE.navigate "https://www.yahoo.co.jp/"
    objIE.Visible = True
    
    'WEBページの読み込みを待つ
    Do While objIE.Busy = True Or objIE.readyState <> 4
        DoEvents
    Loop
    
    SendKeys "テスト" 'SendKeysメソッドでキーボード入力を行う
    Sleep 1000 '1000ミリ秒待機する
    objIE.document.getElementsByClassName("_63Ie6douiF2dG_ihlFTen")(0).Click '検索ボタンをクリック
End Sub
```

### 3-3-1. テキストボックスにキーボードで文字を入力する

テキストボックスに**キーボードで文字を入力する**コードは次の部分です。

```vb
SendKeys "テスト" 'SendKeysメソッドでキーボード入力を行う
```

yahooのトップページを開くと、デフォルトで、カーソルが検索ボックスに当てられています。
ここで、`SendKeysメソッド`を使用して、`"テスト"`という文字列をキーボード入力しています。

もし、カーソルが目的の箇所とは別のところにある場合は、カーソルを動かす処理が必要になります（ここでは触れません）。

### 3-3-2. 処理を一定時間待機させる

上記のキーボード入力の処理が終わってすぐに検索ボタンを押すと、WEBサイト側で処理が追いつかず正常に検索されないので、`Sleep関数`を使用して1秒間（1000ミリ秒間）待機をさせています。次のコードです。

```vb
Sleep 1000
```

上記の記述で、1秒間（1000ミリ秒間）待機とすることができますが、この`Sleep関数`はVBAの関数ではなくてWindows自体が持っている機能となります。
そのため、Windowsの機能を使用するためのAPI（Win32API）をVBAで使用できるように呼び出す必要があります（次項）。

### 3-3-3. Win32APIの関数の呼び出し

Win32APIの関数を呼び出すための宣言は次の部分です。

```vb
#If VBA7 Then
    Declare PtrSafe Sub Sleep Lib "kernel32" (ByVal dwMilliseconds As LongPtr)
#Else
    Declare Sub Sleep Lib "kernel32" (ByVal dwMilliseconds As Long)
#End If
```

この宣言については、基本はどのサイトも同様の記述をしているので、コピペで十分です。

とはいえ、一応、簡単に構造を説明しておきます。
最初に、VBAのバージョンにより宣言の構造が変わるため、If文で`VBA7`であるか否かを判定しています。
この条件分岐で、2パターンの宣言がありますが、ここでは簡単なElse以下の`Declare Sub Sleep Lib "kernel32" (ByVal dwMilliseconds As Long)`の方を見てみます。

まず冒頭にあるのが、[Declareステートメント](https://docs.microsoft.com/ja-jp/office/vba/language/reference/user-interface-help/declare-statement)です。
これは、「ダイナミックリンクライブラリ(DLL) 内の外部プロシージャへの参照」を宣言するものです。
いい加減に言うと、外部にあるWindowsの機能の使用を宣言するというような意味です。

続く`Sub Sleep`の部分でSleep関数（プロシージャ）を使用することを指定して、更に、この関数は「kernel32」というライブラリにあることを`Lib "kernel32"`で示しています。

最後の`(ByVal dwMilliseconds As Long)`では、引数のデータ型を指定しています。
`ByVal`は引数が「値渡し」であることを示し、`dwMilliseconds`は変数なので何でも良い（`m`とかでも良い）、そして`As Long`で引数のデータ型を指定しています。

もう一つの宣言も基本的に同様の構造ですが、`PtrSafe`や`LongStr`の意味などは、[APIについて（Win32API）](https://excel-ubara.com/excelvba1/EXCELVBA423.html)を参照してみてください。
なお、余談ですが、`Ptr`はポインタの意を表しています。

最後に出てきましたが、「**Sleep関数は、VBAでスクレイピングをする場合には必須**」と言っていい関数です。
毎回使うので、いつでもコピペできるように（？）しておいてください。

# さいごに

昨日、Pythonのスクレイピングについての[記事](https://qiita.com/t-yama-3/items/a21999a7f5b3debf3ec7)を書きましたので、VBAについても残しておきました。

スクレイピングは、基本的な方法だけではなかなか思ったようにいかない場合も多く、試行錯誤の繰り返しが多いです。
そういう中で、解決策を見いだしながらコードを書くことで、結構な勉強にもなります。

後日、余裕があるときに、`iframe内の要素の操作`、`別ウィンドウで開かれたページの制御方法`など個人的に苦労したことの解決方法についても書いていこうと思います。