初めてのIE操作(7)~InternetExplorerオブジェクトが現在表示中のDocumentを返してくれない問題に対応する

InternetExplorerオブジェクトが現在表示中のDocumentを返してくれない問題への対応

前々回の問題点の項でも触れたとおり、VBAからHTML上の要素に対してclickメソッドを実行してページ移動した場合、親元のInternetExplorerオブジェクトのDocumentプロパティがちゃんと移動後のHTMLDocumentオブジェクトを返してくれる場合と、移動前のHTMLDocumentオブジェクトを返す場合がある。

素人の私にはなぜこんなわけのわからないことになるのかサッパリ不明だが、とにかく処理の結果が気まぐれというのは実に困る。仕事でIEを自動操縦するというのは、シェフがサラダを作るのとは違うのだ。

強引な対応

原因がわからない以上、素人の私としては対症療法的に対応せざるを得ない。踊るダメ人間が生きて行かざるを得ないように。

そこで、登場するのが、

akashi-keirin.hatenablog.com

このときに作成した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")

では、終了条件判定に、

前回

akashi-keirin.hatenablog.com

ご紹介した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回で見切りをつけてやめてしまうようにする。

実行

イミディエイト・ウインドウの様子をどうぞ。

f:id:akashi_keirin:20181229182111g:plain

2回失敗して3回目で無理矢理目的のページを取得したっぽい。

おわりに

決して美しい対応ではないけれど、致し方ないとは思う。

実は、ここまでの過程で当初の目的であった職場に導入されたWebアプリの攻略は終わっているので、このシリーズも今回でおしまい。

再びIE操作を取り上げる日は来るのか!?

参考

akashi-keirin.hatenablog.com

akashi-keirin.hatenablog.com

akashi-keirin.hatenablog.com

akashi-keirin.hatenablog.com

akashi-keirin.hatenablog.com

akashi-keirin.hatenablog.com

初めてのIE操作(6)~InternetExplorerオブジェクトのDocumentプロパティを調べるFunction

InternetExplorerオブジェクトのDocumentプロパティの返り値を確認するFunction

前回

akashi-keirin.hatenablog.com

「問題点」の項にも書いたとおり、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」が書き込まれ、ページが移動する。

十分に待って、完全にページが表示されたとおぼしき

f:id:akashi_keirin:20181229175035j:plain

この状態で満を持してコードの実行を再開。

f:id:akashi_keirin:20181229175053j:plain

ガックリ。

おわりに

実は、毎回このようになるわけではなく、ちゃんと移動後のページが取得できている場合もあるから悩ましい。

たとえば、上掲コードの(*)の前に

Call WindowsAPI.waitFor(20000)

を入れて、Clickメソッド実行後に20秒(!)待機させるようにして5回実験すると、2回成功、3回失敗、という何とも不安定な結果だった。

やっぱりわけがわからないなあ。

参考

akashi-keirin.hatenablog.com

akashi-keirin.hatenablog.com

akashi-keirin.hatenablog.com

akashi-keirin.hatenablog.com

akashi-keirin.hatenablog.com

akashi-keirin.hatenablog.com

初めてのIE操作(5)~リンク(ボタン)をクリックしてページ移動する

リンクを取得してクリックする

前回までの4記事で、

akashi-keirin.hatenablog.com

目的のWebページを開き、

akashi-keirin.hatenablog.com

起動中のIEオブジェクトを取得し、

akashi-keirin.hatenablog.com

表示中のHTMLドキュメントのソースをぶっこ抜いて分析し、

akashi-keirin.hatenablog.com

で「検索」用テキストボックスを取得して、検索ワード「ち~んw」を入力した。

あとは、検索実行ボタンを突き止めてクリックするだけだ。

ソースの分析

実は、前回お見せした画像にすでに答えはある。

f:id:akashi_keirin:20181229075054j:plain

その部分を抜き書きすると、

<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=""検索"""」のダブルクォーテーション祭りの意味がわからない方は

akashi-keirin.hatenablog.com

をどうぞ。

実行してみる。

f:id:akashi_keirin:20181229075108g:plain

時間はかかるが意図どおり。

問題点

無事にページ移動が行われている以上、何の問題もなさそうだが、実は、リスト2の末尾に、

Debug.Print targetIE.Document.title

を付け足して実行すると、コード実行後、イミディエイト・ウインドウに、

ち~んw の検索結果 - 素人がExcelVBAによる「一人働き方改革」に挑むブログ

と表示されると思いきや、

マーカ部分をRange配列として取得する(Word) - 素人がExcelVBAによる「一人働き方改革」に挑むブログ

と、ページ移動前のページタイトルが表示されるのである……。

f:id:akashi_keirin:20181229075215j:plain

ローカル・ウインドウで見ても、

f:id:akashi_keirin:20181229075203j:plain

……わけがわからない……。

おわりに

……というわけで、まだまだ続きます。

参考

akashi-keirin.hatenablog.com

akashi-keirin.hatenablog.com

akashi-keirin.hatenablog.com

akashi-keirin.hatenablog.com

akashi-keirin.hatenablog.com

WindowsAPI関数をクラスモジュールに封印するとメッチャ便利

WindowsAPIの関数をラップするクラスを作ったらメッチャ便利だった

タイトル通り。

そもそもは、

f:id:akashi_keirin:20181228210702j:plain

コチラの『VBA Developer's Handbook』で紹介されていたテクニックで、ちょっとやってみたら便利だったというだけの話。

ちなみに、コチラの書籍は、VBA四天王の一人、id:t-hom さんも

thom.hateblo.jp

紹介しておられる。

WindowsAPIの関数をクラスに封印する

クラスモジュールを挿入して、テキトーな名前をつける。

私は「WindowsAPI」という何のひねりもないそのままやんけなネーミングにしている。

WindowsAPIの関数をラップしたいだけ、すなわちプロパティやフィールドを持つ必要がないので、Attribute VB_PredeclaredIdの値をTrueにしておく。

Attribute VB_PredeclaredId」については、

akashi-keirin.hatenablog.com

コチラをどうぞ。

たとえば

最近、IE操作関係でWindowsAPI関数を使うことが多いので、手始めにGetTickCountSleepをクラスモジュールに封印してみる。

リスト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)は、GetTickCountSleepを呼び出すためのメソッド。

あと、(4)は、コメント通りGetTickCountSleepを組み合わせて、引数で渡した時間だけ待機するメソッド。

単純に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)

と書けばメソッドの利用ができる。

ここでは、GetTickCountSleepを組み合わせた自作のwaitForメソッドを使っている。

Doループの中でこのメソッドを呼び出すことにより、1000ミリ秒(メソッド内でSleepを引数1で実行しているので、正確には1001ミリ秒+α)ごとにイミディエイト・ウインドウに、文字列を出力し、ループを抜けたところで再度しばらく時間をおいて文字列を出力することになる。

f:id:akashi_keirin:20181228210718g:plain

こんな感じ。

おわりに

めんどくさいWindowsAPI関数の利用が、非常に簡単になると思った。

ちょこっと修正しました

akashi-keirin.hatenablog.com

列挙体の要素列挙時に「:」による改行が使えた

列挙体の要素列挙時に「:」による改行が使えた

前回の

akashi-keirin.hatenablog.com

に、id:imihito さんからコメントをいただいた。

曰く、

: hgAho: hgBaka: hgKasu」 のように先頭に「:」を付ければ大丈夫

と。

ぬわにぃ~~~~!!(昭和風)

ということで、試してみた。

やってみた

次のようにコードを書く。

リスト1 標準モジュール宣言セクション
Public Enum Hoge
: hgAho: hgBaka: hgKasu
End Enum

そして、イミディエイト・ウインドウで実験。

実験

イミディエイト・ウインドウに、

?Hoge.」と打ち込んでみると、

f:id:akashi_keirin:20181228162337g:plain

おお! ちゃんと入力候補も出るし、Hoge.hgAhoの値も意図どおり0になっとる!

おわりに

これで、要素数が多いときに異様にタテ長になっていたEnumをコンパクトかつ分かりやすく書くことができるぞー!

id:imihito さん、毎度毎度ありがとぅ~~~!

列挙体の要素列挙時に「:」による改行は使えない

列挙体の要素の列挙に「:」による改行は使えない

もう題名以外に書くことがない。

列挙体の要素列挙時の改行

普通は、

f:id:akashi_keirin:20181224091229j:plain

こんなふうに、列挙体の要素の列挙をするときは、改行を用いる。

ただ、要素数が多いと異様にタテ長になってしまうのが南天のど飴難点。

VBAでは、改行の代わりに「:」が使える。

で、やってみた。

たとえば、ふつう

Public Enum Hoge
  hgAho
  hgBaka
  hgKasu
End Enum

と書くところを、

Public Enum Hoge
  hgAho:    hgBaka:  hgKasu
End Enum

こう書いてはどうか、ということ。

こうなる

百聞は一見に如かず。

f:id:akashi_keirin:20181224091244g:plain

非常に地味~なのでわかりづらいが、要素の部分を

  hgAho:    hgBaka:  hgKasu

この状態にしてから、別の行にカーソルを移動した瞬間、

hgAho:    hgBaka:  hgKasu

このようになる。すなわち、インデントが解除されてしまうのである。

この動作は、そう、行ラベルを指定したときと同じである。

つまり、行ラベルとみなされてしまったということか。

実験

列挙体として作用するのかどうか、確かめてみる。

まずは、コード入力時。

f:id:akashi_keirin:20181224091307g:plain

「.」を入力しても、入力候補は出ない。

無理矢理

f:id:akashi_keirin:20181224091331j:plain

?Hoge.hgBaka」と入力して[Enter]を押しても、

f:id:akashi_keirin:20181224091347j:plain

あえなくコンパイル・エラー。

おわりに

列挙体の要素列挙時に「:」を使うのは無理っぽい。

素数が多いときにコード1行あたり5個づつとかできたら便利だと思ったんだけどなあ。残念。

追記

今回の内容はガセネタです。

akashi-keirin.hatenablog.com

コチラをどうぞ。

初めてのIE操作(4)~タグ名とキーワードからHTML要素を取得するFunction

HTML要素をタグ名とキーワードで特定して取得するFunction

前回まで

akashi-keirin.hatenablog.com

akashi-keirin.hatenablog.com

akashi-keirin.hatenablog.com

で、Webページを取得して、仮に「右クリック→ソースの表示」や「[Ctrl]+[U]」や「[F12]」などの技が封じられていても、そのHTMLソースをぶっこ抜くところまで進んだ。

後は、データを入力して、必要に応じてクリックするだけである。

データ入力のために必要なこと

データ入力をするためには、ページ上でデータを入力するための場所(たいていはテキストボックスみたいな入力窓口だと思う)がHTML上でどのように表現されているのかを突き止めること。

たいていは「<INPUT>」タグで囲まれたINPUT要素。

そのうち、目的の要素が取得できれば良いことになる。

幸い、getElementsByTagNameという非常に便利なメソッドがあるので、HTMLドキュメントオブジェクトをすでに捕まえた今、目的の要素の取得など、指呼の間にあると言えよう。

目的の要素を捕まえる

今回は、

f:id:akashi_keirin:20181216224135j:plain

の「検索」のところに文字を入力することにする。

まずは、HTMLソースの分析。

前回ファイル出力したHTMLソースをエディタで開いて、それっぽいところを探す。

はてなブログのサイドバーにある検索窓の場合、「記事を検索」という独特の文字列があるから、これが使えそう。

エディタ上で「記事を検索」でテキスト検索すると、あっさり見つかった。

f:id:akashi_keirin:20181216224147j:plain

ちょい読みづらいかも知れないが、当該のINPUT要素のところを抜き書きすると、

<input name="q" class="search-module-input" required="" type="text" placeholder="記事を検索" value="">

となっている。

別にどれを特定用のキーにしても良さそうだけれど、例えば「name="q"」という文字列はHTMLソース内の他にどこにもなかったので、これを使うことにする。

コーディングの考えかた

上述したとおり、InternetExplorer.DocumentInternetExplorer.HTMLDocumentオブジェクトのgetElementsByTagName("TagName")メソッドを使うと、HTMLドキュメント内の引数で指定したタグを持つ要素のコレクションを取得することができる。

今回の場合だとINPUT要素を全て取得することになるので、あとは、それらをしらみつぶしにして、要素の文字列内に「name="q"」という文字列を持つものを探せば良い。

要素オブジェクトからタグ名も含めた文字列を取得するには、outerHTMLというプロパティを参照すれば良い。

つまり、次のような手順になる。

  1. DocumentHTMLDocumentオブジェクトのgetElementsByTagName("input")メソッドを用いて、INPUT要素のコレクションを取得する
  2. INPUT要素のコレクションをループで巡回して、そのouterHTMLプロパティの値に「name="q"」が含まれていたら、その要素を取得する
  3. その要素には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)

実行

f:id:akashi_keirin:20181216224155j:plain

「検索」のところに「ち~んw」と入っているのがおわかりだろうか。

あとは、必要な箇所をクリックするだけ。続きは次回!

参考

akashi-keirin.hatenablog.com

akashi-keirin.hatenablog.com

akashi-keirin.hatenablog.com

akashi-keirin.hatenablog.com

akashi-keirin.hatenablog.com