ブラウザのビジー状態を判定するための,より良い方法 (WSHでIEを自動操作する際,COMのアプリケーションイベントを利用する)
ActiveX経由でCreateObject("InternetExplorer.Application") などして,IEを自動操作する際,
「ブラウザのビジー状態の解除の判定」は,正確にやろうとすると結構難しい。
よくあるのは,
(DOM操作)ブラウザ上で,submitボタンクリック
↓
(While文とかで)ie.Busy == true もしくは ie.readyState != 4 の間,待機。
↓
While文を抜けたら,待機終了。次の画面に遷移したとみなす。
↓
DOM操作を再開する。
という方法。
この方法では,IEオブジェクトの持つプロパティを基準にして判定している。
よく使われる方法ではあるが,これだと,思わぬ判定結果が返ったりする。
「まだ画面遷移が終わっていないのに,While文から抜けてしまった。」というケースがよくある。
その場合,次画面で行われるはずのDOM操作を,遷移完了前に早まって行なってしまう事になるので,「DOM要素が存在しない」というエラーを引き起こす。
これは,ブラウザの快適な自動操作を妨げる,大きな要因の一つだ。
そこで,以下では,プロパティ基準ではなく,COM アプリケーション イベントを使ってブラウザのビジー状態を判定してみよう。
この方法なら,非常に安全・正確に,画面遷移の完了タイミングを検出する事ができる。
下記はWSH / JScriptによるサンプルコード。
※VBAでも同様のテクニックを使える。下記URLを参照。
Excel VBAでIEを自動操作する際,COMイベントを利用する方法 (WithEventsでブラウザの挙動を細かく把握する)
http://language-and-engineering.hatenablog.jp/entry/20131202/CatchCOMeventsOf...
前提知識
WScript.CreateObject()の第二引数を使えば,COMオブジェクトのイベントを扱うことが可能になる。
下記URLのページの真ん中へんを参照。
WSH入門 第11回3.COMオブジェクトの操作(1)(VBScript)
http://www.atmarkit.co.jp/fwin2k/tuto...CreateObjectメソッドには、ProgIDを指定する第1引数のほかに、イベントを監視するためのプリフィックス(接頭辞)を指定する、省略可能な第2引数が存在する。
イベントとは、オブジェクトが何らかのアクションを起こした際にホスト(ここではWSH)に送られるメッセージのことである。
IEの場合,
- 画面遷移完了時は,DocumentCompleteイベントが発生。
- リロード完了時は,DownloadCompleteイベントが発生。
という仕様になっている。
なので,これらのイベントをハンドルすれば,画面遷移の完了を安全に判定できる。
この件について,よくまとまっている解説:
WebBrowser Control, MSHTMLのイベント発火とJS側の状態
http://moz-addon.g.hatena.ne.jp/ZIGOR...IE7の場合は最も早くwindowオブジェクトが生成されるのが、
* DownloadComplateイベントの発火時
* ReadyStateが1の時が成り立つ時。
それ以前はdocumentオブジェクト相当がunknownとなりアクセス出来ない。
IE コンポーネントにおけるイベントの発生順序
http://d.hatena.ne.jp/dayflower/20070...
- 本当の DocumentComplete は?と思いますが,TWebBrowser の LocationURL にドキュメント全体の URL が入っている(そしてブレない)ので,それと比較すれば判定できる
- 引数pDisp がTWebBrowserのApplicationプロパティと一致する場合、そのページは最上位のフレーム(Topのフレーム)であると判定できます。
下記は,意味がわかるように,簡単に書いてあるサンプルコード。
フレームが無い場合
フレームのない,単純なページでの画面遷移完了を,COMイベント経由で検出する。
以下をダブルクリックすれば正常に動作する。
// このフラグがfalseの間は,IEがビジー状態であるとする。 var document_completed_flag = true; var download_completed_flag = true; // IEにDocumentCompleteイベントが発生したときに呼ばれる関数 function hoge_DocumentComplete() { document_completed_flag = true; } // IEにDownloadCompleteイベントが発生したときに呼ばれる関数 function hoge_DownloadComplete() { download_completed_flag = true; } // IE起動 var ie = WScript.CreateObject( "InternetExplorer.Application", "hoge_" ); ie.Visible = true; // Googleに移動 ie.Navigate( "http://www.google.co.jp/" ); document_completed_flag = false; while( ! document_completed_flag ) { WScript.Sleep( 10 ); } // 検索キーワードを入力 ie.document.getElementById("q").value = "ホゲラッチョ"; WScript.Sleep( 100 ); // 検索ボタンクリック ie.document.all("btnG").click(); document_completed_flag = false; while( ! document_completed_flag ) { WScript.Sleep( 10 ); } // 検索結果画面をリロード ie.document.location.reload( true ); // リロードのときはDocumentCompleteは発生せず, // かわりにDownloadCompleteを検知する。 download_completed_flag = false; while( ! download_completed_flag ) { WScript.Sleep( 10 ); } // 検索結果画面のタイトルを表示 WScript.Echo( ie.document.title );
CreateObjectの第二引数に「hoge_」と入れたので,
IE側でDocumentCompleteイベントが発生すると,
WSH側でhoge_DocumentCompleteが呼ばれる。
参考:
Do While ie.Busy Or ie.readyState <> READYSTATE_COMPLETE --- NOT WORKING !!!
http://www.mrexcel.com/forum/showthre...use its DocumentComplete event which would be a good solution in most cases.
Private Sub ie_DocumentComplete(ByVal pDisp As Object, URL As Variant) 'pDisp is returned explorer object in this event 'pDisp.Document is HTMLDocument control that you can use If InStr(1, URL, "www.google.com/search?") > 0 Then 'Open the first returned page ie.Navigate pDisp.Document.getelementsbytagname("ol")(0).Children(0).getelementsbytagname("a")(0).href ElseIf InStr(1, URL, "www.google.com") > 0 Then pDisp.Document.getelementsbyname("q")(0).Value = "VB WebBrowser DocumentComplete Event" pDisp.Document.getelementsbyname("btnG")(0).Click End If End Sub
フレームがある場合
フレームがある場合は,読み込み完了イベントが複数回発生する。
どのイベントが「最終的な読み込み完了」を表しているのか,URLを使って判別する必要がある。
以下はURLを見て,最終的な読み込み完了かどうかを判定するサンプル。
// IEにDocumentCompleteイベントが発生したときに呼ばれる関数 function hoge_DocumentComplete( obj ) { WScript.Echo("document complete"); WScript.Echo(" " + obj.LocationURL); WScript.Echo(" " + ( obj == ie ) ); // ページ全体の読み込みが終わったならtrue } // IEにDownloadCompleteイベントが発生したときに呼ばれる関数 function hoge_DownloadComplete() { WScript.Echo("download complete"); } // IE起動 var ie = WScript.CreateObject( "InternetExplorer.Application", "hoge_" ); ie.Visible = true; // frameのあるページに移動 ie.Navigate( "http://www.elated.com/res/File/articles/authoring/html/html-frames/frame_examples/menuleft/index.html" ); WScript.Sleep(10000); // リロード ie.document.location.reload( true ); WScript.Sleep(10000);
出力されるログ:
D:\temp>cscript ie.js Microsoft (R) Windows Script Host Version 5.6 Copyright (C) Microsoft Corporation 1996-2001. All rights reserved. download complete download complete document complete http://www.elated.com/res/File/articles/authoring/html/html-frames/frame_examples/menuleft/menu.html false document complete http://www.elated.com/res/File/articles/authoring/html/html-frames/frame_examples/menuleft/welcome.html false document complete http://www.elated.com/res/File/articles/authoring/html/html-frames/frame_examples/menuleft/index.html true
もし最終的な読み込み完了であれば,
- それはTOPフレームの読み込みが終わったということであり,
- hoge_DocumentCompleteの引数には,最初から扱っているIEオブジェクトそのものが渡ってくる。
参考:
ページが WebBrowser コントロール内の読み込みを実行するときに確認する方法
http://support.microsoft.com/kb/180366意味不明な翻訳:
- トップレベルのフレームは、DocumentComplete、最終的に発生します。 そのかどうかは、ページは行わをチェックするをダウンロードする必要があるかどうかは、IDispatch * パラメーターは、IDispatch WebBrowser コントロールのと同じです。
Private Sub WebBrowser1_DocumentComplete(ByVal pDisp As Object, URL As Variant) If (pDisp Is WebBrowser1.Object) Then Debug.Print "Web document is finished downloading" End If End SubIE アプリケーションのイベントを横取りする
http://www.ken3.org/vba/backno/vba108...
Private Sub object_DocumentComplete( _ ByVal pDisp As Object, _ ByVal URL As Variant)Parameters pDisp Object that specifies the top-level or frame WebBrowser object corresponding to the event.■ Busyの判定がおかしい?
http://homepage1.nifty.com/MADIA/vb/v...
DocumentCompleteイベントが発生した後で、.Documentプロパティを操作するようにしてください。
それ以前のタイミングでは、文書の解析が完了していない可能性があります。(要IE6 SP1)
》 ドキュメントが完全にダウンロードされると、DownloadCompleteイベントが
》 発生します。この場合でも、そのオブジェクト モデルを通じてドキュメントの内容を
》 管理することは必ずしも安全ではありません。代わりに、DocumentComplete
》 イベントが、すべてが完了し、ドキュメントの準備が整ったことを示します
》(DocumentCompleteは、対象URLに初めてアクセスしたときにだけ届きます。
》F5キーを押すか、[最新の情報に更新(Refresh)]ボタンをクリックした
》2回目以降は、DownloadCompleteイベントだけを受け取ります)。
まとめ
- フレームのないページへの画面遷移完了は,DocumentCompleteイベントを拾えば安全に判定できる。
- フレームのないページでのリロード完了は,DownloadCompleteイベントを拾えば安全に判定できる。
- フレームがあるページの読み込み終了は,DocumentCompleteイベントの引数を見れば判定できる。
- *フレームがある場合にリロードされたら,ビジー状態終了の判定ができない。
4つ目は仕方ないが,フレームのある画面ではRefresh禁止,ということで。
(かわりにNavigateとかすればよいだろう。)
補足
おまけ。
記事中では,ActiveXオブジェクトのイベントを拾うために,
function オブジェクト名_イベント名(){ }
という記法を使って,イベントリスナを定義していた。
この記法については,下記のMicrosoftの記事で紹介されている。
イベントのスクリプティング(microsoftのページ)
http://msdn.microsoft.com/ja-jp/libra...
この記事によると,IEでは,イベントリスナを定義するために以下のような記法も可能だ。
function オブジェクト名.イベント名(){ }
のようにドットを使ったり,または
function オブジェクト名::イベント名(){ }
のように,C++っぽい記法すら可能。
この後者のダブルコロンを使ったイベントリスナ定義方法を,Automagicという。
Automagicを使えば,なんと,下記のようなWebページが書ける。
<input type="button" id="hoge" value="クリック"> <script language="JavaScript"> // Automagic記法で,イベントリスナを定義する。 function hoge::onclick(){ alert("fuga"); } </script>
このHTMLを開いてボタンを押すと,ちゃんと「fuga」と表示される。
もちろんJScript限定なので,Firefoxとかでは動かない。
関連する記事:
Excel VBAでIEを自動操作する際,COMイベントを利用する方法 (WithEventsでブラウザの挙動を細かく把握する)
http://language-and-engineering.hatenablog.jp/entry/20131202/CatchCOMeventsOf...
ブラウザの自動操作の最大の問題,「タイムアウト」を克服するには
http://language-and-engineering.hatenablog.jp/entry/20100403/p1
Excel VBAのマクロで,IEを自動操作しよう (DOMセレクタ関数をVBAで自作)
http://language-and-engineering.hatenablog.jp/entry/20090710/p1
JavaScriptの動かないコード (中級編) clickイベントを強制的に発生させたい (fireEvent/createEventの使い方)
http://language-and-engineering.hatenablog.jp/entry/20090907/p1