スポンサーリンク

ブラウザのビジー状態を判定するための,より良い方法 (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 Sub

IE アプリケーションのイベントを横取りする
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