初めてのIE操作(7)~InternetExplorerオブジェクトが現在表示中のDocumentを返してくれない問題に対応する
InternetExplorerオブジェクトが現在表示中のDocumentを返してくれない問題への対応
前々回の問題点の項でも触れたとおり、VBAからHTML上の要素に対してclick
メソッドを実行してページ移動した場合、親元のInternetExplorer
オブジェクトのDocument
プロパティがちゃんと移動後のHTMLDocument
オブジェクトを返してくれる場合と、移動前のHTMLDocument
オブジェクトを返す場合がある。
素人の私にはなぜこんなわけのわからないことになるのかサッパリ不明だが、とにかく処理の結果が気まぐれというのは実に困る。仕事でIEを自動操縦するというのは、シェフがサラダを作るのとは違うのだ。
強引な対応
原因がわからない以上、素人の私としては対症療法的に対応せざるを得ない。踊るダメ人間が生きて行かざるを得ないように。
そこで、登場するのが、
このときに作成したgetIEByTitle
メソッド。
InternetExplorer
オブジェクトのDocument
プロパティが移動後のHTMLDocument
オブジェクトを返してくれないときでも、画面上にはちゃんと移動後のページが表示されているのである。
ならば、ウインドウに表示されているIEを再び捕まえ直せばよいわけだ。
そこで、前々回のリスト1に、次のようにコードを追加する。
リスト1 標準モジュール
Public Sub test() Dim targetIE As New InternetExplorer With targetIE .Visible = True Call .Navigate("http://akashi-keirin.hatenablog.com/entry/2018/12/16/001606") Do While .Busy Or _ .ReadyState <> READYSTATE_COMPLETE DoEvents Loop End With Dim targetTextBox As Object Set targetTextBox = getElementByTagAndKeyWord(targetIE, "input", "name=""q""") targetTextBox.Value = "ち~んw" Dim targetButton As Object Set targetButton = getElementByTagAndKeyWord(targetIE, "input", "value=""検索""") targetButton.Click '(1)' Dim n As Long n = 1 Do DoEvents Call WindowsAPI.waitFor(1000) '(2)' If n > 2 Then _ Set targetIE = Nothing: _ Set targetIE = getIEByTitle("ち~んw") '(3)' Debug.Print "Wait " & n & " 回目:" & targetIE.Document.Title n = n + 1 '(4)' If n > 5 Then targetIE.Quit: Exit Sub Loop Until isTargetPage(targetIE.Document, "ち~んw") Debug.Print "終了間際:" & targetIE.Document.Title End Sub
追加したのは(1)からの13行(実質11行)。
カウンタ変数「n
」を準備した後のDo
ループ、
Do DoEvents Call WindowsAPI.waitFor(1000) '(2)' If n > 2 Then _ Set targetIE = Nothing: _ Set targetIE = getIEByTitle("ち~んw") '(3)' Debug.Print "Wait " & n & " 回目:" & targetIE.Document.Title n = n + 1 '(4)' If n > 5 Then targetIE.Quit: Exit Sub Loop Until isTargetPage(targetIE.Document, "ち~んw")
では、終了条件判定に、
前回
ご紹介したisTargetPage
というメソッドを用いている。
InternetExplorer
オブジェクトのDocument
プロパティの返り値の返り値を調べ、今回の場合だとtitle
プロパティの返り値に「ち~んw
」が含まれていたら目当てのページが表示されているとみなす。
Do
ループ内部では、カウンタ変数「n」の値が2
を超えていたら、すなわちループ3周目に入ると、今のインスタンスtargetIE
に見切りをつけて、getIEByTitle
メソッドでタイトルに「ち~んw
」が含まれるウインドウを捕まえてInternetExplorer
オブジェクトをtargetIE
にセットし直す。
(3)の
Debug.Print "Wait " & n & " 回目:" & targetIE.Document.Title
は動作確認用。何回目の試行で正しいページが取得できているのか確かめる用。
(4)の
If n > 5 Then targetIE.Quit: Exit Sub
はページが移動できていない時用。
そもそもページが移動できていなければ無限ループになってしまうので、試行5回で見切りをつけてやめてしまうようにする。
実行
イミディエイト・ウインドウの様子をどうぞ。
2回失敗して3回目で無理矢理目的のページを取得したっぽい。
おわりに
決して美しい対応ではないけれど、致し方ないとは思う。
実は、ここまでの過程で当初の目的であった職場に導入されたWebアプリの攻略は終わっているので、このシリーズも今回でおしまい。
再びIE操作を取り上げる日は来るのか!?
参考
初めてのIE操作(6)~InternetExplorerオブジェクトのDocumentプロパティを調べるFunction
InternetExplorerオブジェクトのDocumentプロパティの返り値を確認するFunction
前回
の「問題点」の項にも書いたとおり、VBAからIEを操ったときに、画面に表示されているHTMLドキュメントと、InternetExplorer
オブジェクトのDocument
プロパティが返すHTMLDocument
オブジェクトの内容が異なる、というわけのわからない現象がたまに起きる。
ということは、InternetExplorer
オブジェクトがどのような状態にあるのかを確認する工程が必要だと言うことになる。
HTMLDocumentオブジェクトのTitleプロパティを調べるFunction
簡単なFunction
を作る。
リスト1 標準モジュール
Public Function isTargetPage(ByVal targetDocument As HTMLDocument, _ ByVal pageTitleKeyWord) As Boolean isTargetPage = True If InStr(1, targetDocument.Title, pageTitleKeyWord) > 0 _ Then Exit Function isTargetPage = False End Function
アホみたいに簡単なコード。
引数としてHTMLDocument
オブジェクト(InternetExplorer
オブジェクトのDocument
プロパティの返り値)と、タイトルのキーワード文字列を受け取って、Instr
関数によってHTMLDocument.Title
プロパティを調べているだけ。
使ってみる
次のようなコードで実験。
リスト2 標準モジュール
Public Sub testIsTargetPage() Dim targetIE As New InternetExplorer With targetIE .Visible = True Call .Navigate("http://akashi-keirin.hatenablog.com/entry/2018/12/16/001606") Do While .Busy Or _ .ReadyState <> READYSTATE_COMPLETE DoEvents Loop End With Dim targetTextBox As Object Set targetTextBox = getElementByTagAndKeyWord(targetIE, "input", "name=""q""") targetTextBox.Value = "ち~んw" Dim targetButton As Object Set targetButton = getElementByTagAndKeyWord(targetIE, "input", "value=""検索""") targetButton.Click Debug.Print isTargetPage(targetIE.Document, "ち~んw") '……(*)' Debug.Print targetIE.Document.Title End Sub
実行時は、(*)の行にブレークポイントを設定して、ページ移動を待つことにする。
IEが起動し、検索ボックスに「ち~んw
」が書き込まれ、ページが移動する。
十分に待って、完全にページが表示されたとおぼしき
この状態で満を持してコードの実行を再開。
ガックリ。
おわりに
実は、毎回このようになるわけではなく、ちゃんと移動後のページが取得できている場合もあるから悩ましい。
たとえば、上掲コードの(*)の前に
Call WindowsAPI.waitFor(20000)
を入れて、Click
メソッド実行後に20秒(!)待機させるようにして5回実験すると、2回成功、3回失敗、という何とも不安定な結果だった。
やっぱりわけがわからないなあ。
参考
初めてのIE操作(5)~リンク(ボタン)をクリックしてページ移動する
リンクを取得してクリックする
前回までの4記事で、
目的のWebページを開き、
起動中のIEオブジェクトを取得し、
表示中のHTMLドキュメントのソースをぶっこ抜いて分析し、
で「検索」用テキストボックスを取得して、検索ワード「ち~んw」を入力した。
あとは、検索実行ボタンを突き止めてクリックするだけだ。
ソースの分析
実は、前回お見せした画像にすでに答えはある。
その部分を抜き書きすると、
<input class="search-result-button" type="submit" value="検索" />
この通り。
つまり、前回のリスト1でご紹介したgetElementByTagAndKeyWord
メソッドを、タグ名に「input
」、キーワードに「value="検索"
」を指定して実行したらよい。
取得した要素をクリックする
要素(今回の場合はinput
要素)を取得したら、その要素をクリックするにはどうするか。
click
メソッドを実行するだけである。
取得した要素オブジェクト式.click
と書くだけである。
アホみたいに簡単である。
やってみる
というわけで、前回のリスト2に、検索ボタンを取得してクリックする処理を付け加える。
リスト1 標準モジュール
Public Sub test() Dim targetIE As New InternetExplorer With targetIE .Visible = True Call .Navigate("http://akashi-keirin.hatenablog.com/entry/2018/12/16/001606") Do While .Busy Or _ .ReadyState <> READYSTATE_COMPLETE DoEvents Loop End With Dim targetTextBox As Object Set targetTextBox = getElementByTagAndKeyWord(targetIE, "input", "name=""q""") targetTextBox.Value = "ち~んw" '(1)' Dim targetButton As Object '(2)' Set targetButton = getElementByTagAndKeyWord(targetIE, "input", "value=""検索""") '(3)' targetButton.Click End Sub
付け加えたのは(1)からの3行
Dim targetButton As Object '(2)' Set targetButton = getElementByTagAndKeyWord(targetIE, "input", "value=""検索""") '(3)' targetButton.Click
(2)の
Set targetButton = getElementByTagAndKeyWord(targetIE, "input", "value=""検索""")
でgetElementByTagAndKeyWord
メソッドにより、クリックしたい要素を取得して変数targetButton
にぶち込み、(3)の
targetButton.Click
で取得したinput
要素オブジェクトのclick
メソッドを実行している。
(2)の「"value=""検索"""
」のダブルクォーテーション祭りの意味がわからない方は
をどうぞ。
実行してみる。
時間はかかるが意図どおり。
問題点
無事にページ移動が行われている以上、何の問題もなさそうだが、実は、リスト2の末尾に、
Debug.Print targetIE.Document.title
を付け足して実行すると、コード実行後、イミディエイト・ウインドウに、
ち~んw の検索結果 - 素人がExcelVBAによる「一人働き方改革」に挑むブログ
と表示されると思いきや、
マーカ部分をRange配列として取得する(Word) - 素人がExcelVBAによる「一人働き方改革」に挑むブログ
と、ページ移動前のページタイトルが表示されるのである……。
ローカル・ウインドウで見ても、
……わけがわからない……。
おわりに
……というわけで、まだまだ続きます。
参考
WindowsAPI関数をクラスモジュールに封印するとメッチャ便利
WindowsAPIの関数をラップするクラスを作ったらメッチャ便利だった
タイトル通り。
そもそもは、
コチラの『VBA Developer's Handbook』で紹介されていたテクニックで、ちょっとやってみたら便利だったというだけの話。
ちなみに、コチラの書籍は、VBA四天王の一人、id:t-hom さんも
紹介しておられる。
WindowsAPIの関数をクラスに封印する
クラスモジュールを挿入して、テキトーな名前をつける。
私は「WindowsAPI
」という何のひねりもないそのままやんけなネーミングにしている。
WindowsAPIの関数をラップしたいだけ、すなわちプロパティやフィールドを持つ必要がないので、Attribute VB_PredeclaredId
の値をTrue
にしておく。
「Attribute VB_PredeclaredId
」については、
コチラをどうぞ。
たとえば
最近、IE操作関係でWindowsAPI関数を使うことが多いので、手始めにGetTickCount
とSleep
をクラスモジュールに封印してみる。
リスト1 クラスモジュール
オブジェクト名は「WindowsAPI
」、「Attribute VB_PredeclaredId
」の値はTrue
。
Option Explicit '(1)WindowsAPI関数の宣言' Private Declare Function GetTickCount Lib "kernel32" () As Long Private Declare Sub Sleep Lib "kernel32" (ByVal dwMilliseconds As Long) '(2)GetTickCount関数呼び出しメソッド' Public Function callGetTickCount() As Long callGetTickCount = GetTickCount End Function '(3)Sleep関数呼び出しメソッド' Public Sub callSleep(ByVal milliSeconds As Long) Call Sleep(milliSeconds) End Sub '(4)GetTickCountとSleepを組み合わせた待機用メソッド' Public Sub waitFor(ByVal milliSeconds As Long) Dim startTime As Long startTime = GetTickCount Dim endTime As Long Do Sleep (1) DoEvents endTime = GetTickCount Loop Until endTime - startTime > milliSeconds End Sub
それぞれコード中の(1)~(4)に記した通り。
(1)の
Private Declare Function GetTickCount Lib "kernel32" () As Long Private Declare Sub Sleep Lib "kernel32" (ByVal dwMilliseconds As Long)
はおなじみのアレ。
ただし、いづれもPrivate
指定にして、直接外から呼び出すことができないようにクラス内に封印している。
そして、(2)、(3)は、GetTickCount
、Sleep
を呼び出すためのメソッド。
あと、(4)は、コメント通りGetTickCount
とSleep
を組み合わせて、引数で渡した時間だけ待機するメソッド。
単純にDo~While
でループさせるだけでなく、ループ中にSleep
をかましているところがミソ。
こんなふうに複数のWindowsAPIを組み合わせた処理を簡単に利用できるようにすることができる。
使ってみる
次のコードで実行してみる。
リスト2 標準モジュール
Public Sub disposableMacro01() Dim n As Long n = 1 Do Call WindowsAPI.waitFor(1000) '……(1)' Debug.Print "待機 " & n & " 回目" Debug.Print "ち~んw" n = n + 1 Loop Until n > 10 Call WindowsAPI.waitFor(2000) Debug.Print "おしまい" End Sub
上にも書いたように、WindowsAPI
クラスは、「Attribute VB_PredeclaredId
」の値をTrue
にしているので、インスタンス化せずに、(1)のように
Call WindowsAPI.waitFor(1000)
と書けばメソッドの利用ができる。
ここでは、GetTickCount
とSleep
を組み合わせた自作のwaitFor
メソッドを使っている。
Do
ループの中でこのメソッドを呼び出すことにより、1000
ミリ秒(メソッド内でSleep
を引数1
で実行しているので、正確には1001
ミリ秒+α)ごとにイミディエイト・ウインドウに、文字列を出力し、ループを抜けたところで再度しばらく時間をおいて文字列を出力することになる。
こんな感じ。
おわりに
めんどくさいWindowsAPI関数の利用が、非常に簡単になると思った。
ちょこっと修正しました
列挙体の要素列挙時に「:」による改行が使えた
列挙体の要素列挙時に「:」による改行が使えた
前回の
に、id:imihito さんからコメントをいただいた。
曰く、
「: hgAho: hgBaka: hgKasu
」 のように先頭に「:」を付ければ大丈夫
と。
ぬわにぃ~~~~!!(昭和風)
ということで、試してみた。
やってみた
次のようにコードを書く。
リスト1 標準モジュール宣言セクション
Public Enum Hoge : hgAho: hgBaka: hgKasu End Enum
そして、イミディエイト・ウインドウで実験。
実験
イミディエイト・ウインドウに、
「?Hoge.
」と打ち込んでみると、
おお! ちゃんと入力候補も出るし、Hoge.hgAho
の値も意図どおり0
になっとる!
おわりに
これで、要素数が多いときに異様にタテ長になっていたEnum
をコンパクトかつ分かりやすく書くことができるぞー!
id:imihito さん、毎度毎度ありがとぅ~~~!
列挙体の要素列挙時に「:」による改行は使えない
列挙体の要素の列挙に「:」による改行は使えない
もう題名以外に書くことがない。
列挙体の要素列挙時の改行
普通は、
こんなふうに、列挙体の要素の列挙をするときは、改行を用いる。
ただ、要素数が多いと異様にタテ長になってしまうのが南天のど飴難点。
VBAでは、改行の代わりに「:
」が使える。
で、やってみた。
たとえば、ふつう
Public Enum Hoge hgAho hgBaka hgKasu End Enum
と書くところを、
Public Enum Hoge hgAho: hgBaka: hgKasu End Enum
こう書いてはどうか、ということ。
こうなる
百聞は一見に如かず。
非常に地味~なのでわかりづらいが、要素の部分を
hgAho: hgBaka: hgKasu
この状態にしてから、別の行にカーソルを移動した瞬間、
hgAho: hgBaka: hgKasu
このようになる。すなわち、インデントが解除されてしまうのである。
この動作は、そう、行ラベルを指定したときと同じである。
つまり、行ラベルとみなされてしまったということか。
実験
列挙体として作用するのかどうか、確かめてみる。
まずは、コード入力時。
「.」を入力しても、入力候補は出ない。
無理矢理
「?Hoge.hgBaka
」と入力して[Enter]を押しても、
あえなくコンパイル・エラー。
おわりに
列挙体の要素列挙時に「:
」を使うのは無理っぽい。
要素数が多いときにコード1行あたり5個づつとかできたら便利だと思ったんだけどなあ。残念。
追記
今回の内容はガセネタです。
コチラをどうぞ。
初めてのIE操作(4)~タグ名とキーワードからHTML要素を取得するFunction
HTML要素をタグ名とキーワードで特定して取得するFunction
前回まで
で、Webページを取得して、仮に「右クリック→ソースの表示」や「[Ctrl]+[U]
」や「[F12]」などの技が封じられていても、そのHTMLソースをぶっこ抜くところまで進んだ。
後は、データを入力して、必要に応じてクリックするだけである。
データ入力のために必要なこと
データ入力をするためには、ページ上でデータを入力するための場所(たいていはテキストボックスみたいな入力窓口だと思う)がHTML上でどのように表現されているのかを突き止めること。
たいていは「<INPUT>
」タグで囲まれたINPUT
要素。
そのうち、目的の要素が取得できれば良いことになる。
幸い、getElementsByTagName
という非常に便利なメソッドがあるので、HTMLドキュメントオブジェクトをすでに捕まえた今、目的の要素の取得など、指呼の間にあると言えよう。
目的の要素を捕まえる
今回は、
の「検索」のところに文字を入力することにする。
まずは、HTMLソースの分析。
前回ファイル出力したHTMLソースをエディタで開いて、それっぽいところを探す。
はてなブログのサイドバーにある検索窓の場合、「記事を検索」という独特の文字列があるから、これが使えそう。
エディタ上で「記事を検索」でテキスト検索すると、あっさり見つかった。
ちょい読みづらいかも知れないが、当該のINPUT
要素のところを抜き書きすると、
<input name="q" class="search-module-input" required="" type="text" placeholder="記事を検索" value="">
となっている。
別にどれを特定用のキーにしても良さそうだけれど、例えば「name="q"
」という文字列はHTMLソース内の他にどこにもなかったので、これを使うことにする。
コーディングの考えかた
上述したとおり、InternetExplorer.Document
InternetExplorer.HTMLDocument
オブジェクトのgetElementsByTagName("TagName")
メソッドを使うと、HTMLドキュメント内の引数で指定したタグを持つ要素のコレクションを取得することができる。
今回の場合だとINPUT
要素を全て取得することになるので、あとは、それらをしらみつぶしにして、要素の文字列内に「name="q"
」という文字列を持つものを探せば良い。
要素オブジェクトからタグ名も含めた文字列を取得するには、outerHTML
というプロパティを参照すれば良い。
つまり、次のような手順になる。
Document
HTMLDocument
オブジェクトのgetElementsByTagName("input")
メソッドを用いて、INPUT
要素のコレクションを取得するINPUT
要素のコレクションをループで巡回して、そのouterHTML
プロパティの値に「name="q"
」が含まれていたら、その要素を取得する- その要素には
Value
プロパティがあるはずなので、Value
プロパティを任意の文字列にする
このようにすれば良い。ちなみに、今回は、「検索」ボックスに「ち~んw
」と入力することにする。
というわけで、コーディング。
まずは、タグ名とキーワードを元に要素を取得するFunctionから。
リスト1 標準モジュール
Public Function getElementByTagAndKeyWord( _ ByVal targetIE As InternetExplorer, _ ByVal targetTagName As String, _ ByVal targetKeyWord As String) As Object Set getElementByTagAndKeyWord = Nothing With targetIE Dim targetElement As Object For Each targetElement In .Document.getElementsByTagName(targetTagName) If InStr(1, targetElement.outerHTML, targetKeyWord) > 0 Then Set getElementByTagAndKeyWord = targetElement Exit Function End If Next End With End Function
上で説明したとおりのコードなので、説明は省略。
お次は、リスト1を利用して、検索ボックスに「ち~んw」と入力するコード。
リスト2 標準モジュール
Public Sub test() Dim targetIE As New InternetExplorer With targetIE .Visible = True Call .Navigate("http://akashi-keirin.hatenablog.com/entry/2018/12/16/001606") Do While .Busy Or _ .ReadyState <> READYSTATE_COMPLETE DoEvents Loop End With Set targetIE = getIEByTitle("素人が") Dim targetTextBox As Object Set targetTextBox = getElementByTagAndKeyWord(targetIE, "input", "name=""q""") targetTextBox.Value = "ち~んw" End Sub
これもまあ、説明は省略。
前回まで同様、テキトーにブレークポイントを設定して、待機時間の調整は手動(w)
実行
「検索」のところに「ち~んw
」と入っているのがおわかりだろうか。
あとは、必要な箇所をクリックするだけ。続きは次回!