VBAで他のアプリケーションを操作する
RPAツール関係のお話が2017年からぐわっと盛り上がっていますが、実際の所このRPAと呼ばれるツール群やその話題は昔からあったもので、いわばペンキ塗り替えでRPAと称して再登場してるものです。代表的なものはExcelのマクロやVBA、UWSCといった自動化ツール、またウェブ操作系のデバッグツールとしては、Seleniumなどなど。
今までは事務方が現場で自分のために部分最適化の一例として細々と実践してたものなのです。ASPがSaaSと名を変えてリバイバルしたようなものです。ちょっと前まで「Excelマクロで楽するなんてずるい」とか言ってた時代だったのに、変わったものです。さて、そんなRPAですが、Excel VBAでどこまで行けるのか?まとめてみました。
※ここに記載している他にもマウスの座標をコントロールしてボタンをクリックさせる手法や、VBAでは出来ない事をVBSやWSHなどの別のスクリプトにやらせてVBAはそれを呼び出す事に徹する方法、スクレイピングしてデータを取得する方法など様々な遠隔操作手法があります。
目次
今回使用するファイル
※今回のサンプルはSeleniumBasicは参照設定ではなく、実行時バインディングでコードを記述してあります。
新方式が登場しました
IE11の廃止に伴い、SeleniumやNode.jsやらといった手段を使わず、またPuppeteerと同様の手法(CDPを叩く)でVBAとEdge/ChromeのみでOAuth2.0認証する手段が登場しました。スクレイピングも可能になっています。以下のエントリーを参考にしてみてください。この手法は最も制限が無く、もっともすぐれた選択肢になると思います。
Excel VBAで他のアプリを操作する
SendKeysを使った手法
問題点と解消法
最もポピュラーでこれまでもよく利用されていた手法です。キーボードの操作をプログラムのコードで送りつけて、アプリケーションを操作します。ショートカットキーなども利用出来るのですが、いくつかの問題点があって、結構癖があり扱いにくいです。問題点は
- コピペなどの場合、マクロ側でコピーをしたものでないとコピーがうまく動かない
- 一方的にキーを無造作に送りつけるので、起動が遅いとか処理が間に合わなくても送ってしまう
- 実行中に下手に操作すると、アクティブウィンドウが変わって、そちらにキーを送ってしまう
- 印刷ダイアログなどではショートカットキーが効かない
- ESCキーを送りたい場合、マクロが途中停止する事がある。
- NumLockが外れることがある
コピペはCtrl+C, Ctrl+Vだとうまく動かない時には、VBAでクリップボードにコピーする関数を間に挟んで処理をする。日本語の文字化け処理に対してもこの方法は有効です。
1 2 3 4 5 6 7 8 9 10 11 |
Function clipman(chardat As Variant) Dim hensuu1 As String Dim MyClipboard As New DataObject hensuu1 = chardat With MyClipboard .SetText hensuu1 'テキスト文字列をDataObjectにコピー .PutInClipboard 'DataObjectのデータをクリップボードに移動 End With End Function |
処理が間に合わない場合にはスリープを入れてあげる。Win32 APIを使うので冒頭で宣言が必要です。
1 2 3 4 5 6 |
Public Declare Sub Sleep Lib "kernel32" (ByVal dwMilliseconds As Long) Private Sub StopTime(waittimer as integer) 'ミリセカンド単位で一時停止するAPI Call Sleep(waittimer * 100) End Sub |
印刷ダイアログ等では、ショートカットキーではなく、TABキーやスペースキー(" ")、Enterキーなどを組み合わせて送ってあげます。例えば、TABを6回、スペースを1回ならば
1 2 3 4 5 |
'TABキーで6回移動する SendKeys "{TAB 6}", True 'スペースキーを押す(ボタンクリック) SendKeys " ", True |
ESCキーはマクロの停止と被るので、これをまずは受け付けないようにブロックしておく
1 2 3 4 5 6 7 8 9 |
'ESCキーをExcelで受け付けないようにしておく Application.EnableCancelKey = xlDisabled 'キーを送信する SendKeys "{TAB 6}", True SendKeys " ", True 'ESCキーを送って閉じる SendKeys "{ESC}", True |
NumLockがオフになったりする現象がバグとして公式で報告されています。これが鬱陶しいのでこの場合には、NumLockをSendkeysで送ってしまうか?こちらのサイトで紹介されてるような、NumLockの状態を取得して必ずオンになるようにするコードを追加してみる。
基本、SendKeysを使うようなVBAの場合、実行中は操作してはいけません。
サンプルコード
今回は、「irfanviewを起動し、特定フォルダ内の画像を全てBMP形式で特定のフォルダに変換して保存する」といった処理をSendKeysで作ってみました。sleepをさせるコードと特殊フォルダのパスを取得する関数を別に用意してあります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
Sub irfanrun() '変数の宣言 Dim fol As String '開くフォルダ Dim fol2 As String '保存するフォルダ Dim progra As String 'Program Filesのパス 'フォルダパスの格納 fol = SHGetFoldPath(CSIDL_DESKTOP) & "\kinoko\" fol2 = SHGetFoldPath(CSIDL_DESKTOP) & "\save\" progra = SHGetFoldPath(CSIDL_PROGRAM_FILES) 'irfanviewを起動しアクティブにする Shell progra & "\IrfanView\i_view32.exe", 1 AppActivate "IrfanView" 'sendkeysでキー操作を送りつけるルーチン SendKeys "%(FB)", True '形式名前の一括変換 SendKeys fol, True '保存先フォルダを指定 SendKeys "%O", True 'フォルダを開く 'ファイルを全部追加を実行 StopTime 1000 SendKeys "{TAB 6}", True SendKeys " ", True '変換形式と保存先を指定する(BMP形式) SendKeys "{TAB 8}", True SendKeys "{B}", True SendKeys "{TAB 4}", True SendKeys fol2, True SendKeys "%(S)", True End Sub |
※SHGetFoldPathは特殊なフォルダパスを取得する為のルーチンです。
※StopTimeはミリセカンド単位でsleepをさせる為のルーチンです。
※事前にデスクトップにsaveとkinokoというフォルダを作っておき、kinokoフォルダに画像類を入れておく必要があります。
実行してみた
WSHのSendKeysを使った手法
概要
VBAのSendKeysメソッドはバグがあるということで、避けている人が代替としてよく利用しているのがこの手法。同じSendKeysなのですが、Windows Scripting Hostと呼ばれるスクリプト実行環境版です。これをVBAから利用します。参照設定より、「Microsoft Scripting Runtime」を追加すればOKですが、一時的に使用したいような場合には、CreateObjectで呼び出すのも良いでしょう。
※但し、VBAではWindows Scripting HostのSleepは使えません。また、書き方は殆ど同じですが、()で括る点やRunでアプリを起動させる時にProgram Filesのような半角スペースを含むものは、パスの渡し方がちょっと独特です。
サンプルコード
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
Sub irfanrunwsh() '変数の宣言 Dim fol As String '開くフォルダ Dim fol2 As String '保存するフォルダ 'WSH用の変数の宣言 Dim wshshell As Object Set wshshell = CreateObject("WScript.Shell") 'フォルダパスの格納 fol = wshshell.SpecialFolders("Desktop") & "\kinoko\" fol2 = wshshell.SpecialFolders("Desktop") & "\save\" 'irfanviewを起動しアクティブにする wshshell.Run ("""C:\Program Files\IrfanView\i_view32.exe") wshshell.AppActivate "IrfanView" 'すぐコマンドを送ると失敗するので、ウェイト StopTime 1000 'sendkeysでキー操作を送りつけるルーチン wshshell.SendKeys ("%FB") wshshell.SendKeys (fol) wshshell.SendKeys ("%O") 'ファイルを全部追加 StopTime 1000 wshshell.SendKeys ("{TAB 6}") wshshell.SendKeys (" ") '変換形式と保存先を指定する(PNG形式) wshshell.SendKeys ("{TAB 8}") wshshell.SendKeys ("{B}") wshshell.SendKeys ("{TAB 4}") wshshell.SendKeys (fol2) wshshell.SendKeys ("%S") '終了処理(メモリから開放) Set wshshell = Nothing End Sub |
SendInputを使った手法
概要
SendKeysを送る手法のバグ対策の為に、WSHを使った手法を使いました。それ以外にもWindows APIを使ったSendInput関数を使ったキーの送信方法があります。但しこの手法は素のやり方でコードを書くと非常に煩雑になるので、こちらのサイトを参考にして、キー送信部分を関数化しておくと、扱い易くなります。
素の状態でコードを組んだ時、SendInputの場合はキーを押す&キーを離すといった動作まで細かく定義をしなければなりません。簡単にコピペとした場合でも、Ctrlキーを押す⇒Cキーを押す⇒Ctrlキーを離す⇒Cキーを離す⇒(何かの移動処理)⇒Ctrlキーを押す⇒Vキーを押す⇒Ctrlキーを離す⇒Vキーを離すといった処理を事細かに書かなければなりません。
※キーをいちいち作成するわけなのですが、例えばCtrlキーを押してそのままにしておくと、ずっとOSレベルで押しっぱなしの状態になるのでややこしいことになります。キーを作る時には注意が必要です。必ずCtrlキーをアップして開放してあげなければなりません。
※Altキーなどに秀Capsなどのキーボードショートカットコマンドを割り当てていると、反応してうまく動作しない事があります。常駐させている場合には注意が必要です。
サンプルコード
今回のサンプルコードはこちらのサイトの関数化されたプロシージャを利用しています。また、今回使用する分のキーコード定数だけを宣言しているので、他のキーコードを使う場合には追加で宣言に足してあげる必要があります。冒頭の宣言関係やキーコード定数は省略して記載しています。
また、特殊フォルダのパスの取得やirfanviewの起動に関してはWindows Scripting Hostを利用しています。clipman関数はクリップボードにデータを保存するための自作の関数です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 |
'sendinputを使ってキーを送る Public Sub irfansendinput() '変数の宣言 Dim fol As String '開くフォルダ Dim fol2 As String '保存するフォルダ Dim vlRet As Long 'WSH用の変数の宣言 Dim wshshell As Object Set wshshell = CreateObject("WScript.Shell") 'フォルダパスの格納 fol = wshshell.SpecialFolders("Desktop") & "\kinoko\" fol2 = wshshell.SpecialFolders("Desktop") & "\save\" 'irfanviewを起動しアクティブにする wshshell.Run ("""C:\Program Files\IrfanView\i_view32.exe") wshshell.AppActivate "IrfanView" 'すぐコマンドを送ると失敗するので、ウェイト StopTime 1000 'sendkeysでキー操作を送りつけるルーチン 'クリップボードにデータをコピーしておく Call clipman(fol) '一括変換ウィンドウを出す vlRet = SimpleSendKeys(VK_MENU_DOWN, _ VK_FKEY, _ VK_MENU_UP, _ VK_BKEY) 'すぐコマンドを送ると失敗するので、ウェイト StopTime 1000 '指定のフォルダを開く vlRet = SimpleSendKeys(VK_ENTER) 'Enterキー一回でファイルパスの欄に必ず飛ぶ vlRet = SimpleSendKeys(VK_CONTROL_DOWN, _ VK_VKEY, _ VK_CONTROL_UP, _ VK_MENU_DOWN, _ VK_OKEY, _ VK_MENU_UP) 'すぐコマンドを送ると失敗するので、ウェイト StopTime 1000 'フォルダ内のファイルを全て追加する vlRet = SimpleSendKeys(VK_MENU_DOWN, _ VK_DKEY, _ VK_MENU_UP) 'TABで画像形式へ移動する(14回) vlRet = SimpleSendKeys(VK_TAB, VK_TAB, VK_TAB, VK_TAB, VK_TAB, VK_TAB, VK_TAB, VK_TAB, _ VK_TAB, VK_TAB, VK_TAB, VK_TAB, VK_TAB, VK_TAB) '保存先をクリップボードに保存しておく Call clipman(fol2) '画像形式をBMPにして、 vlRet = SimpleSendKeys(VK_BKEY, VK_TAB, VK_TAB, VK_TAB, VK_TAB) '保存先URLを入力して変換実行 vlRet = SimpleSendKeys(VK_CONTROL_DOWN, _ VK_VKEY, _ VK_CONTROL_UP, _ VK_MENU_DOWN, _ VK_SKEY, _ VK_MENU_UP) End Sub |
解説
SendInputは冒頭ジェネラルプロシージャ内で、別途宣言が必要になります。WindowsのAPIであってVBAの機能ではないので、必ずこれは必要になります。以下のような宣言文です。
1 2 3 4 5 |
'キーストローク、マウスの動作、ボタンのクリックをシミュレートする Private Declare Function SendInput Lib "user32.dll" _ (ByVal nInputs As Long, _ pInputs As INPUTS, _ ByVal cbSize As Long) As Long |
今回利用させていただいたSendInputを簡略化して使える関数を利用すると、非常に楽にキーストロークの組み合わせを作って送信する事ができます。ひとつ注意したい点は、ALT(Menu)キーやCtrlキーについては、キーダウンとアップのキーコード定数が異なります。間違えるとOSレベルで押されっぱなしになり、再起動するまで解除されません。VK_CONTROL_DOWNの後には必ず、VK_CONTROL_UPでキーを送りましょう。
また、キーボード以外にもマウス操作も可能ですが今回は使用しませんでした。
プロシージャ内で送り込みたい文字列などはクリップボード関数でまずコピーしておき、Ctrl+Vを送るようなコマンドを送ると良いでしょう。今回も開くフォルダ、保存先フォルダの2つで利用しています。
コマンドラインで操作する手法
概要
これまでの操作は、人間がマウスやキーボードを操作する流れを、忠実にキーコードを送信して実現し、アプリケーションを操作する手法でした。最近はそれほど多くなくなりましたが、かつてのプログラムはGUIと同時にコマンドラインでも操作できるように実装されてるものがありました。irfanviewもそのうちの1つです(今はAPIのほうが主流ですね)。
IrfanViewのコマンドライン・オプション一覧を見て、コマンドプロンプトで入力すればCUIで同じ操作が可能になります。実際に不確実な面がどうしても拭えないキーコード送信よりも、100%確実に結果が期待できるコマンドラインのほうが結果的には面倒が少ないとも言えます。VBAからはWindows Scripting Hostを使ってコマンドプロンプトへコマンドラインを送り込んで実行可能です。これまでと同じ操作のコマンドラインを組んでみました。
1 |
"c:\Program Files\IrfanView\i_view32.exe" "ファイルのあるパス\*.*" /convert="保存先パス\$N.bmp" |
*.*で対象フォルダ内にある全てのファイルが対象になり、$N.jpgで同じファイル名で拡張子がbmpのファイルに、/convertオプションでバッチ変換をする命令文です。
サンプルコード
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
'WSHでコマンドライン経由でirfanviewを実行 Sub irfancomm() '変数の宣言 Dim fol As String '基準フォルダ Dim comstr As String 'コマンドライン用変数 'WSH用の変数の宣言 Dim wshshell As Object Dim wshexec As Object Set wshshell = CreateObject("WScript.Shell") 'フォルダパスとコマンドラインを指定 wshshell.CurrentDirectory = "C:\Program Files\IrfanView" fol = wshshell.SpecialFolders("Desktop") 'コマンドラインを組み立てと実行 comstr = "i_view32.exe " & fol & "\kinoko\*.*" & " /convert=" & fol & "\save\$N.bmp" Set wshexec = wshshell.exec("%ComSpec% /c " & comstr) End Sub |
※wshshell.execでコマンドラインを実行しているのですが、"%ComSpec% /c "はおまじないみたいなものです。comstrで組み立てたコマンドラインで、非常に高速に処理が完了します。
UI Automationを利用して操作する
最近巷で話題になっているPower Automate for Desktopなどでも利用されている、デスクトップアプリケーションをUI Automationを利用して操作する手法をVBAで行うテクニックです。
デスクトップのアプリにもSeleniumで操作する時のように、一個一個のコンポーネントには個別のIDのようなものが振られており、これらを利用してSendKeyを使わずにダイレクトに操作するのがUI Automationです。いずれ、詳しい構築の仕方を別エントリーで紹介したいと思います。
以下は簡単に電卓を起動して計算させる64bit版コードです。別途参照設定にて、UIAutomationClientを追加して利用する事になります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 |
Private Declare PtrSafe Function FindWindowA Lib "user32" _ (ByVal lpClassName As String, ByVal lpWindowName As String) As LongPtr Public Declare PtrSafe Sub Sleep Lib "kernel32" (ByVal dwMilliseconds As LongPtr) '電卓を起動して自動計算させる Public Sub calcautomation() Dim Hwnd As Variant Dim pid As LongPtr '電卓を起動する pid = Shell("calc") Sleep 1000 'ウィンドウハンドを取得する Hwnd = FindWindowA(vbNullString, "電卓") Sleep 1000 '計算させる Call calcman(Hwnd, "1") Call calcman(Hwnd, "+") Call calcman(Hwnd, "1") Call calcman(Hwnd, "=") End Sub '電卓にUI操作コマンドを送る Private Sub calcman(ByVal Hwnd As Long, command As String) 'UI Automation用変数 Dim uauto As CUIAutomation Dim el_calc As IUIAutomationElement Dim uiCnd As IUIAutomationCondition Dim invokePt As IUIAutomationInvokePattern 'UI Automationのインスタンスをセット Set uauto = New CUIAutomation Set el_calc = uauto.ElementFromHandle(ByVal Hwnd) Set uiCnd = uauto.CreatePropertyCondition(UIA_NamePropertyId, "電卓") '送られてくるコマンドにより条件分岐 Select Case StrConv(command, vbNarrow) Case "+" command = "プラス" Set uiCnd = uauto.CreatePropertyCondition(UIA_NamePropertyId, "標準演算子") Case "-" command = "マイナス" Set uiCnd = uauto.CreatePropertyCondition(UIA_NamePropertyId, "標準演算子") Case "/" command = "除算" Set uiCnd = uauto.CreatePropertyCondition(UIA_NamePropertyId, "標準演算子") Case "*" command = "乗算" Set uiCnd = uauto.CreatePropertyCondition(UIA_NamePropertyId, "標準演算子") Case "=" command = "等号" Set uiCnd = uauto.CreatePropertyCondition(UIA_NamePropertyId, "標準演算子") Case Else Set uiCnd = uauto.CreatePropertyCondition(UIA_NamePropertyId, "数字パッド") End Select 'コマンドを電卓に送り込む Set uiCnd = uauto.CreatePropertyCondition(UIA_NamePropertyId, command) Set el_calc = el_calc.FindFirst(TreeScope_Subtree, uiCnd) Set invokePt = el_calc.GetCurrentPattern(UIA_InvokePatternId) invokePt.Invoke End Sub |
- 電卓の起動およびウィンドウハンドルの取得時にスリープを入れていないと、掴む前にcalcmanが実行されてしまうので、必須の処理です
- calcmanでは送られてくるコマンドのタイプ毎に詳細なコマンドを構築し直し、最後に電卓側にUI Automationにて送り込んでいます。
- これらのUI要素の解析には、UIAutomation SpyもしくはInspectといったツールで解析します。
図:2005年から実装された結構歴史のあるAPI
SeleniumBasicでウェブアプリを操作する
Seleniumとは、ウェブアプリケーションのデバッグテストなどで使用してる、自動テストを行わせる為のツールです。ブラウザを操作してくれるので、これそのものでもRPA的な使い方も可能です。しかし、VBAからブラウザ操作はあまりにも面倒な上に、昔のIEの操作に関するものが殆どで、現代的なものではありません(今更IEを使う事もないですし、Edgeを使う気にはなれないですし)。
そこで使用するのが、Selenium Basic(旧Selenium VBA)です。Chromeなどのブラウザも操作可能になりますが、その代理をしてくれるのが、SeleniumBasicです。管理者権限無しでインストールや、WebDriverの自動更新などについては、以下のエントリーを参考にしてみてください。
セットアップ
SeleniumBasicのセットアップは簡単。以下の手順でセットアップをします。
- 配布元に行き、Release Pageへ入る
- 最新版をクリックしてダウンロード
- ダウンロードされたインストーラを起動して適当に進める。
- 途中、Web Driverのインストールに関する項目がでるけれど全てそのままインストール
- 完了したらインストール自体は完了。
ただし、このWeb Driverが最新とは限らないので、以下の手順で最新版にすげ替える。今回はChromeで操作を考える。
- Chrome Driverの配布元に行き、Downloadページにゆく
- 最新版をダウンロード
- 解凍すると、exeが1個入ってる。
- Explorerを起動し、パスの部分に「%LOCALAPPDATA%\SeleniumBasic」を入れて移動する
- 3.で解凍されたexeをそのフォルダに放り込み上書きする
これで完了です。
図:インストール自体はとても簡単
図:ブラウザとドライバのバージョンが違うとエラーが出る
実際に操作してみる
使用するには事前に「Selenium Type Library」を参照設定で入れておく必要性があります。インストールした後であれば参照設定のリストに出てくるはずです。チェックを入れておきましょう。コードの中で事前バインディングではなく、実行時バインディングで使う場合には、以下のようなコードになります。今回のサンプルは、実行時バインディングで記述してあります。
1 2 |
Dim selenium as Object Set selenium = CreateObject("Selenium.WebDriver") |
図:事前バインディングの場合参照設定をする
早速コードを書いてみます。今回は、Googleのページで「Excel 自動化」で検索をした結果を、スクリーンショットで取得して、Excelに貼り付けるといった一連のフローを書いてみます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
Public Sub selenium_test() 'ライブラリを呼び出す Dim selenium As New selenium.ChromeDriver 'Chromeを起動してGoogleへアクセス selenium.Start "chrome", "http://www.google.com/" selenium.Get "/" '検索ワードを入れてボタンをクリック With selenium .FindElementByName("q").SendKeys ("Excel 自動化") .FindElementByName("btnK").Click .Timeouts.ImplicitWait = 10000 End With 'スクリーンショットを取る Dim screenshot As Object Set screenshot = selenium.TakeScreenshot 'スクショを貼り付ける(seleniumを停止させる) screenshot.ToExcel Range("h3") '終了処理 Set screenshot = Nothing selenium.Close End Sub |
古いSelenium VBA時代のコードとかなり変わっているので、入力補完などを使ってメソッドは組み立てる必要があります。しっかりした作りのウェブアプリケーションであれば、ほぼログインからデータの取得まではこなせるだけでなく、ウェイトなどを駆使して、Excelのデータを元に申請などを半自動で送り込めるのではないかと思います。
※もともとはウェブアプリケーションの実行テストの為のものなので、かなり細かく挙動をコントロール出来ます。
図:自動でスクショ取って閉じてくれる
Name属性ボタンをクリックできない時の対処法
Googleのトップページなど、ボタンにはID属性がなくName属性が付けられています。このボタンをクリックさせることで検索がされるわけなのですが、Selenium BasicでClickをさせてもクリックされない事があります。こんな時用にキーコードを送ることでクリックさせたことと同じ処理を行わせる事ができます。
この場合、事前にkeysを設定する必要があります。実行時バインディングであれば
1 2 3 4 5 6 7 8 |
'keysを設定する(キーコードを送る場合に使用する) Dim keys As Object Set keys = CreateObject("Selenium.keys") ↓中略 'クリックさせる時には selenium.FindElementByName("btnK").SendKeys (keys.Enter) |
事前バインディグさせている場合であれば
1 2 3 4 5 6 7 |
'Keyを設定する Dim keys As New Selenium.keys ↓中略 'クリックさせる時は selenium.FindElementByName("btnK").SendKeys (keys.Enter) |
冒頭が異なりますが、Keyを送る時には、Clickではなく、SendKeys(keys.Enter)で、Enterキーを送りつけています。
Headless Chromeで実行
最新のChromeはPhantomJSのようなHeadless Modeという特殊なモードを備えています。これは、Chrome自体を表示させずにバックグラウンドで非表示のブラウザを使って処理を進める為のモードです。Excelでも高速化処理の時に「Application.ScreenUpdating = False」を加えて処理を非表示にする事がありますが、似たようなものです。
Headlessで動かす場合には、Startする前にseleniumオブジェクトにオプションを指定してあげます。
1 2 3 4 |
'Headless Modeで動かす場合 Selenium.AddArgument "headless" Selenium.AddArgument "disable-gpu" Selenium.AddArgument "window-size=1920,1080" |
実行してみると一切ブラウザを表示せずに後ろで作業が進められます。
また、Chromeの場合、Google謹製のChrome自動化を実現するPuppeteerと呼ばれるheadless chromeなNode.jsモジュールがあります。nexeと呼ばれるモジュールでNode.jsまるごと1個のexeに出来ます。これをVBAから呼び出して上げる手法も非常に有効でしょう。
その他の選択肢
SeleniumBasic以外にも同様にVBAから操作することのできるライブラリがいくつか公開されています。非常に小規模ながら必要最低限の操作の機能を装備している「TinySeleniumVBA」であったり、WebDriverのみで操作を可能にした「SeleniumWrapperVBA」などがあります。前者はChromeのDevtoolに対して直接命令をHTTPリクエストで送り込んでいるのに対して、後者はSeleniumBasicを必要とせずWebDriverのみで操作をする仕組みになっています。
こちらのサイトに詳しい解説があるので、Selenium以外の選択肢が必要な場合はチャレンジしてみると良いでしょう。
REST APIを叩く
上記のSeleniumを使った手法は、マウス操作というよりも、それをプログラム化するにあたって、HTMLのElementを操作する方法になります。RPAでも各ElementのIDなどを参照して、座標を使ったものよりも正確にボタンを叩くなどの動作を実現できるものがあります。
しかし、これらは基本ウェブアプリケーションのデザインが僅かに変わるだけでも、動作しなくなる恐れがあり、またウェブアプリケーション側も自動操作の禁止をしているケースなどもあり、あまり好ましい方法とは言えません。一方で、現在のウェブアプリケーションはその殆どが「REST API」を備えているので、VBAのWinHttpなどを使って、GETやPOSTで命令を送り、目的の結果を直接的に受け取ったり送ったりが出来るので、マウス操作を真似るまでもなく高速に処理が可能です。
ウェブアプリケーションによって、与えるパラメータやヘッダー内容が異なるので一概には言えませんが、BoxというストレージサービスのAPIを叩いてファイルをダウンロードしたり、アップロードしたり、情報を取得するものは最もベーシックなものになるので、参考になるでしょう。基本、REST APIはウェブサイトのデザインとは異なり、プログラムからの利用を想定しているので、大規模な変更は生じにくいですので、自動化をするならばこちらの手法を学んだほうが応用が効くのでお得です。
以下はVBAからBox APIを叩いてみるの実践編に於ける、ファイルのダウンロードの命令を送る事例です。実際にはこのコードの他にOAuth2.0認証をするコードや、Access Tokenを取得するコード、Refresh Tokenで新しいAccess Tokenを取得するコード、プロキシーサーバを経由する場合の対応などが必要になってきます。
※自分のケースだとGoogle Apps Script APIを使って、自分でREST APIを作りVBAと連携させたりなどもして活用しています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 |
'APIアクセス用エンドポイントURL Private Const filepoint As String = "https://api.box.com/2.0/files/" 'ファイルダウンロード関数(CurrentProject.pathにダウンロード) Public Function getFileDownload() As Boolean 'DB接続用 Dim SQL As String Dim db As DAO.Database Dim rs As DAO.Recordset Dim ret As Variant 'バイナリファイル生成用 Dim FileData() As Byte Dim FileNum As Long 'ファイルのIDを指定 Dim fileid As Variant fileid = "306826865097" 'Access Tokenの取得と失効チェック Dim tokenstatus As Integer tokenstatus = checkExpireToken() 'JSON受信用 Dim Json As Variant Dim doc, jsn 'HTMLDocumentを取得 Set doc = CreateObject("HtmlFile") 'scriptタグを追加 doc.write "<script>document.JsonParse=function (s) {return eval('(' + s + ')');}</script>" 'Tokenの状況に応じて処理を分岐 Select Case tokenstatus Case 0 '無事にTokenは生きてるので何もしない Case 1 'refresh token使って新しいTokenを取得 ret = getNewToken() '取得結果を判定 If ret = True Then '無事に取得できているのでスルーする Else '取得失敗 MsgBox "Access Tokenの取得に失敗しました。" getFileDownload = False Exit Function End If Case 2 '60日オーバーなので再認証をする Call boxAuthorization '終了処理 MsgBox "認証が期限切れです。再認証を実行してください。" getFileDownload = False Exit Function End Select 'Access Tokenを取得する Set db = CurrentDb() Set rs = db.OpenRecordset("tokeninfo", dbOpenDynaset) Dim access_token As String access_token = rs!access_token 'フォルダ情報を取得する 'POST通信でAPIを叩いてデータを取得 Dim getinfourl As String Dim disposition As String Dim tempheader As Variant getinfourl = filepoint & fileid & "/content" With CreateObject("WinHttp.WinHttpRequest.5.1") .Open "GET", getinfourl, False '.setProxy 2, proxyuri 'プロキシサーバのURLとポート番号 .setRequestHeader "Content-Type", "application/x-www-form-urlencoded" .setRequestHeader "Authorization", "Bearer " & access_token .send '返ってきた値をもとにデータを処理 Select Case .status Case 200 'レスポンスヘッダを取得 Debug.Print .GetAllResponseHeaders() 'バイナリデータを取得 FileData = .responseBody 'ファイル名を取得する tempheader = .GetResponseHeader("Content-Disposition") disposition = DEcodeURLMSHTML(Mid(tempheader, InStr(tempheader, "filename*=UTF-8''") + 17)) 'ファイルパス組み立て FilePath = CurrentProject.Path & "\" & disposition 'バイナリデータを生成する FileNum = FreeFile() Open FilePath For Binary Access Write As #FileNum Put #FileNum, 1, FileData Close #FileNum Case Else getFileDownload = False End Select End With '終了処理 Set db = Nothing Set rs = Nothing getFileDownload = True End Function |
また、この手のコードは最初のOAuth2.0認証で、Internet Explorerを使う事例が多いのですが、64bit OSの場合そのままだと「拡張保護モード」に起因する具合が悪いケースがあるので、あらかじめ設定などを行うコードに於いて、レジストリを操作するモジュールを利用して、VBAからセットしてやる方法がスマートです。以下のコードでこの2つの設定を無効化する事が可能です。
1 2 3 |
'IEの拡張保護モードおよび64bit設定を無効にする Call RegSetValue(HKEY_CURRENT_USER, "Software\Microsoft\Internet Explorer\Main", "Isolation64Bit", REG_DWORD, 0) Call RegSetValue(HKEY_CURRENT_USER, "Software\Microsoft\Internet Explorer\Main", "Isolation", REG_SZ, "PMIL") |
メソッドを使って他のアプリを直接操作
VBAでは遥かに昔から「参照設定」を用いて、他のアプリケーションにAPIを介して直接操作する事は常套手段でした。とりわけWordやExcel, PowerPoint、OutlookそしてInternet Explorerの操作もVBA内から参照設定でオンにする事で、命令を直接送って操作が可能です。また、これ以外であってもSDKがリリースされているソフトウェアの場合、VBAから操作する為のメソッドを利用できるようにしてあったりします(業務用のアプリケーションだとこの手のSDKがリリースされているケースが多いです。工場の大きな機械の稼働状況や命令の送信ですらVBAで可能です)。身近な事例だとテプラをVBAから操作するSDKなどがありますね。
ウェブ上でよく見かける資料でCreateObjectでInternet Explorerなどと指定し、IEを操作してスクレイピングさせる事例などはまさにこの事例になります。直接アプリケーションのAPIを介しての操作での事例として、Outlookでメール送信させる事例をここに記載しておきます。利用する為には参照設定より「Microsoft Outlook 1x.0 Object Library」を追加するか?もしくは、CreateObject("Outlook.Application")にて利用をします。前述のSeleniumでの操作もこのメソッドを使った操作の事例になります(向こうはウェブブラウザの操作でウェブアプリケーションの操作が目的ですが)
ちなみに参照設定をしてから使う方法を「アーリーバインディング」と言い、基本そのライブラリがそのPCに入ってる必要があり、コードを書くときにコード補完が効きます。後者を「レイトバインディング」といい、特定のバージョンでなければ合致するものを呼び出してくれる反面、コード補完は効きません。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 |
'Outlookを起動しメールを作成するルーチン Public Function mailapp() Dim result As Variant '問い合わせ result = MsgBox("取り込んだ予約データをもとにサマリーメールを送りますか?", vbYesNo + vbDefaultButton2 + vbExclamation) If result = vbYes Then '次の処理に進む Else MsgBox "メール送信はキャンセルされました。" Exit Function End If 'メール用パーツデータを取得する Dim subjectman As String Dim body As String Dim footerman As String Dim css As String Dim credit As String subjectman = Worksheets("メール設定").Range("B1").Value body = Worksheets("メール設定").Range("B2").Value footerman = Worksheets("メール設定").Range("B3").Value credit = Worksheets("メール設定").Range("B4").Value css = Worksheets("メール設定").Range("B5").Value '改行コードを<br>にリプレース body = Replace(body, vbLf, "<br>") footerman = Replace(footerman, vbLf, "<br>") credit = Replace(credit, vbLf, "<br>") 'レジストリより送信先を取得する Dim sendto As String Dim sendcc As String sendto = RegGetValue(HKEY_CURRENT_USER, _ "Software\cr" & _ "\Settings", _ "sendto", _ REG_SZ, _ 0) sendcc = RegGetValue(HKEY_CURRENT_USER, _ "Software\cr" & _ "\Settings", _ "sendcc", _ REG_SZ, _ 0) 'メール用データを取得 Dim dataArray Dim lastrow As Long Dim i As Long '予約データの最終行を取得 lastrow = Worksheets("予約データ").UsedRange.Rows.Count 'データを取得する If lastrow = 1 Then MsgBox "送信するべきデータがありませんよ。" Exit Function Else dataArray = Worksheets("予約データ").Range("A2:R" & lastrow) End If '送信するサマリーデータを組み立て 'テーブルヘッダーの構築とCSSねじこみ Dim formbody As String Dim csshead As String Dim cssfoot As String Dim tableth As String csshead = "<head><style>" cssfoot = "</style></head>" tableth = "<body><table class='type08'><thead><tr>" & _ "<th>日付</th>" & _ "<th>時間</th>" & _ "<th>部署名</th>" & _ "<th>来室者</th>" & _ "<th>続柄</th>" & _ "<th>性別</th>" & _ "<th>職籍</th>" & _ "<th>新規継続</th>" 'サマリーデータの生成 Dim cnt As Long Dim kananame As String cnt = 1 For i = 2 To lastrow '入力値が空の場合、スルーする If dataArray(cnt, 2) = "" Or IsNull(dataArray(cnt, 2)) Then 'スルーする Else 'カナの頭だけ取った名前を構成 kananame = Left(dataArray(cnt, 15), 1) & "・" & Left(dataArray(cnt, 16), 1) & "さん" '入力値からテーブルを構成 tableth = tableth & "<tr>" tableth = tableth & "<td>" & dataArray(cnt, 5) & "</td>" & _ "<td>" & Format(CDate(dataArray(cnt, 6)), "HH:MM") & "</td>" & _ "<td>" & dataArray(cnt, 10) & "</td>" & _ "<td>" & kananame & "</td>" & _ "<td>" & dataArray(cnt, 8) & "</td>" & _ "<td>" & dataArray(cnt, 4) & "</td>" & _ "<td>" & dataArray(cnt, 17) & "</td>" & _ "<td>" & dataArray(cnt, 18) & "</td>" tableth = tableth & "</tr>" End If 'カウンタを加算する cnt = cnt + 1 Next i 'メールパーツを組み立てる tableth = tableth & "</tbody></table></body>" formbody = csshead & css & cssfoot & body & "<br><br>" & tableth & "<br><br>" & footerman & "<br><br>" & credit 'Outlookオブジェクトの生成 Dim objOutlook As Outlook.Application Dim objMail As Outlook.MailItem 'MailItemオブジェクトを生成 Set objOutlook = New Outlook.Application Set objMail = objOutlook.CreateItem(olMailItem) 'メールを組み立てる With objMail .To = sendto 'メール宛先(To) .CC = sendfcc 'メール宛先(CC) .Subject = subjectman 'メール件名 .BodyFormat = olFormatHTML 'メールの形式 .HTMLBody = formbody 'メールを送信する .Send End With '終了処理 Set objOutlook = Nothing MsgBox "メールで通知しました。" End Function |
メールを送るだけであれば、上記のコードの一番したにある「With objMail」の部分だけで十分です。ネットには「VBAは他のアプリケーションの操作が出来ない」「RPAは操作可能で範囲が広い」などといったノンプログラマーと思わしき者が書いた資料が出回っていますが、大きな過ちですのでご注意ください。
関連リンク
- Headless Chrome
- SeleniumからHeadless Chromeを使ってみた
- ブラウザーテストが捗る!Node.jsで使えるヘッドレスChromeが便利すぎる
- ヘッドレスChromeでシンプルに自動テストを行う
- Phantom.jsがメンテナンス終了になる件を調べてみた
- PowerShellでSendKeysを使ったWindowsの自動化をしてみた
- Excel2010, SendKeysの文字化け対策
- VBAでファイル選択ダイアログを自動操作する
- 他アプリへの文字入力1
- WSH(Windows Scripting Host)
- SendInputでALT+Nをすると、終了後にALTが押されっぱなしになる。
- SeleniumBasic(ExcelVBA + Selenium)でchromeが起動しなくなったときの対処メモ
- RPA九人衆による「アカネチャンカワイイヤッタ」の自動化
- アプリケーションのUIを分析する(Inspectツール)