none
BackgroundWorkerで複数のWebページをDOMとして扱えるか RRS feed

  • 質問

  • メインスレッドなら、WebBrowserコンポーネントでページの切り替えをし、DOMとして使えるDocumentプロパティを使えます。
    が、サブスレッドからはコンポーネントを操作することができないので、ページの切り替えができません。
    処理自体は、ボタンを押す等の1回の操作の中で、複数の処理をさせたい。
    このため、WebBrowserの中を変えるために、その度に操作するようなことはしたくない。
    XMLの場合は、XmlTextReaderを使う等すると、単にドキュメントをロードし、DOMとして扱うことができます。
    Webページが扱いたいだけで、ブラウザとしての機能はなくても構いません。
    XmlTextReaderのような基本的で非コンポーネントのクラスや、良い方法はないでしょうか。
    HttpWebResponse等を使って、非DOMの単なるテキストデータとして地道に処理するしかないのかなぁ。
    2010年12月24日 12:39

回答

  • Documentは、ロードし終わった現在のページだけを保持していると解釈しています。

    その通りです。

    Clickから直接行った場合、次のページをNavgateするために、ロードも含めて処理完了を待ちます。
    この間、操作はフリーズになります。
    ページが複数あるため、ずっとフリーズすることになります。

    多分、ループとか、Application.DoEvents とかそういったことを書いて処理を待ち、次の処理をする、一つのイベントの中で連続的にコードを書かないと実現できないとお考えなのかもしれませんが、結構よく聞くハマリ方です。
    メンバー変数とか、うまく書けば、そういったループじゃなくても作れます。

    ただ、最初は難しいかもしれませんので、イメージコードを提示しますので参考にしてみてください。
    (C# の書き方で書いてしまっていますし、さらにコンパイルを試していません。適宜 C++/CLI に書き換えていく、修正していくことを試みてください)

    private Stack<string> _urls = new Stack<string>();
    
    private void _button_Click(object sender, EventArgs e)
    {
     // 処理するURLをまとめて登録しておく
     // ここではStackなので後に入れた方が先に処理されることになる
     // 必要に応じてListなど、別のクラスを使っても良い。
     _ulrs.Push("http://a/");
     _ulrs.Push("http://b/");
     _ulrs.Push("http://c/");
     _button.Enabled = false;
     NavigateNextUrl();
    }
    
    private void NavigateNextUrl()
    {
     // Stackなので取り出すと要素が減る
     // 要素がなくなるとCount = 0となるはずなので、それで完了を見分ける
     if (_urls.Count == 0)
     {
     // 処理完了
     _button.Enabled = true;
     return;
     }
     string nextUrl = _urls.Pop();
     _webBrowser.Navigate(nextUrl);
    }
    
    private void _webBrowser_DocumentCompleted(object sender, EventArgs e)
    {
     // 読み込み終了
     // **何らかの解析処理**
     // 解析処理終了
     // 次へ
     NavigateNextUrl();
    }
    
    

    このコードにおいてポイントがあるとすれば、一連の URL 読み込みを一つのイベントの中で連続させず、一度イベントから抜けることです。
    こうすれば、BackgroundWorker.DoWork イベントなどで処理完了待ちループを書かなくて済みますし、BackgroundWorker を使わなくても済みます。
    (一連の処理を連続で書かずに、いかに状態を維持するか、続きを実行するかというところかなぁ)


    質問スレッドで解決した場合は、解決の参考になった投稿に対して「回答としてマーク」のボタンを押すことで、同じ問題に遭遇した別のユーザが役立つ投稿を見つけやすくなります。

     

    • 回答としてマーク K6963 2010年12月29日 20:53
    2010年12月27日 15:40
    モデレータ

すべての返信

  • 環境は、Vista SP2、VSEE2008です。

     

    2010年12月24日 12:59
  • DOMというとHTML DOMとXML DOM がありまして。WebBrowser.DocumentはHtmlDocumentクラス ですが、XmlTextReaderというかそちらはXmlDocumentクラス になります。最大の違いとしてはやはり、HTML DOMの場合、タグの対応が取れていなかったり多少壊れていても大目に見てくれますが、XML DOMの場合、明確にパースエラーになります。あとDTDの扱いが難しいというかなんというか。

    それでもよければ、XmlDocument.Load() を利用できます。

    2010年12月24日 13:39
  • XmlTextReaderで読ませたりすることもしたのですが、案の定、フォーマットエラーになるんですよねぇ。
    機械的に作られたHTMLなので、それ程多くの異常に対応する必要はないんですが、例外が発生した時点でオブジェクトは無効になってしまう。
    ふと思ったのですが、これは私の使い方の場合ですが、サブスレッドからWebBrowserコンポーネントのUrlを変更したりすることはできません。
    しかし、ProgressChangedイベントを発生させて、イベント内で変更する間接的な方法が使えるかもと思いました。
    サブスレッドから、Document等の参照自体はできるので。
    2010年12月24日 16:40
  • うーん、どうもダメみたいです<ProgressChanged経由
    今まで、サブスレッドからメインスレッドのコンポーネントの参照ができていたので、できるものだと思っていたのですが違うのでしょうか?
    サブスレッドのデバッグ中(サブスレッド内のコードのブレークポイントで停止)に、WebBrowserコンポーネントのプロパティを変数一覧で参照すると、赤い「!」マークが付いています。
    説明では値がないということですが、ロードはできてる(DocumentCompletedイベント発生)ので、ないのではなく、ここからは参照できないということだと思いました。
    しかし、例えば、テキストボックスの中身を参照する場合は、同じマークが付いていますが参照できます。
    できてしまうだけで、保証されたものではないのでしょうか?
    2010年12月24日 20:02
  • コントロールを作ったスレッド以外からのアクセスは保障されません。
    従って、WebBrowser コントロールを別スレッドからアクセスすること自体、考え直すべきです。

    また、解析のためにバックグラウンドスレッドを使うとのことですが、この場合、WebBrowser コントロールは使えないと考えておくべきでしょう。


    質問スレッドで解決した場合は、解決の参考になった投稿に対して「回答としてマーク」のボタンを押すことで、同じ問題に遭遇した別のユーザが役立つ投稿を見つけやすくなります。
    2010年12月24日 22:00
    モデレータ
  • mshtml を使うなり、適当な HTML DOM ライブラリ使うなり(CodePlex とかにいくつかあるようです)すれば良いんじゃないですか?

    mshtml なら、COM 参照して HTMLDocument 作って write。

    2010年12月25日 0:15
  • みなさま、レスありがとです。

    やはりそうでしたか(^^;<保証されない
    更に考えてみたのですが、DoWorkイベントはトリガー(ReportProgressメソッド)のみをさせて、そこから発生したProgressChangedイベントでページ取得・処理を行えばいいのではないかと思いました。
    ProgressChangedイベントからDocument等を見た場合は、赤い「!」マークは付いていないので、問題ないのではないかと思いました。
    サブスレッドからコンポーネントを操作してはならないというヘルプをどこかで見て覚えていたのですが、そのヘルプが見つけられません(^^;
    禁止されてるのは、DoWorkイベントからだけだったかどうか。。。???
    2010年12月25日 19:20
  • ではなく、3段階でした<DoWork->ProgressChanged

    ProgressChangedの中ではロード指示(WebBrowserのNavigateメソッド)のみをし、処理は、WebBrowserのDocumentCompleted内。

     

    2010年12月25日 19:27
  • ProgressChangedの中ではロード指示(WebBrowserのNavigateメソッド)のみをし、処理は、WebBrowserのDocumentCompleted内。 

    ちなみに、DoWork の中では何をするのですか?
    ReportProgress しかしないのなら、その BackgroundWorker に意味がないかもしれません。

    ProgressChanged で処理をすることは、結局メインスレッドで処理をしているので BackgroundWorker で期待していた同時に処理することができません。
    順番に 1 ページずつ処理をするだけなら、ボタンかフォーム表示のイベントで WebBrowser.Navigate → DocumentCompleted で処理し、WebBrowser.Navigate → DocumentCompleted で処理という繰り返しで済みますよね。


    質問スレッドで解決した場合は、解決の参考になった投稿に対して「回答としてマーク」のボタンを押すことで、同じ問題に遭遇した別のユーザが役立つ投稿を見つけやすくなります。
    2010年12月25日 23:21
    モデレータ
  • ボタンを押した時のイベント内等で、WebBrowser.Navigate → DocumentCompletedを繰り返す場合、アプリ操作がフリーズしてしまうため、 BackgroundWorkerでしようと考えました。

    (Navigate自体は指示のみですが、複数のページを処理する場合、DocumentCompletedを待ってそのページを処理してから、次のNavigateを行うため。)

    ロード自体に多少時間もかかりますし、ペース数が多い(例えば、数百とか)場合、ボタンを押してから操作復帰まで何もできなくなってしまいます。

    テストコードを書いてみたところ、できそうな感じなので、3段階の方法でやってみようと思います。

     

    2010年12月26日 9:53
  • ボタンを押した時のイベント内等で、WebBrowser.Navigate → DocumentCompletedを繰り返す場合、アプリ操作がフリーズしてしまうため、 BackgroundWorkerでしようと考えました。

    これはご自身で記述されている処理コードの実行時間が長いと言うことでしょうか。

    テストコードを書いてみたところ、できそうな感じなので、3段階の方法でやってみようと思います。 

    そのテストコードでは「フリーズ」を回避できたのでしょうか?
    ProgressChanged イベントで Navigate メソッドを呼んで、DocumentCompleted イベントで HTML 解析のコードを書いているだけならば、同じように「フリーズ」するように思えました。

    繰り返す形になりますが、ProgressChanged イベントで処理することは、結局 Button の Click イベントや WebBrowser の DocumentCompleted イベントで処理することとあまり変わりません。
    何が重くて(長く時間がかかって)、画面が固まる・重くなるのかを理解し、適切に対応することが求められます。

    # ただ、私が懸念を抱いているだけで、お持ちのコードでは解消されているかもしれませんが。


    質問スレッドで解決した場合は、解決の参考になった投稿に対して「回答としてマーク」のボタンを押すことで、同じ問題に遭遇した別のユーザが役立つ投稿を見つけやすくなります。
    2010年12月26日 10:11
    モデレータ
  • 簡易コードで試したため、実コードで全く問題がないかどうかは、微妙なところです。
    考えてみて、おっしゃってることがもっとだと言うことに気づきました。
    スレッドは、メインとサブの2つしかなく、イベントは、自分がメソッドを呼び出すことによって発生したものでなくても、それはスレッドの動作であるわけだから、イベントを呼び出したスレッドは処理に集中することになる。
    その時、見た目にはフリーズになる。
    ただ、ボタンを押したイベントでNavigateする場合と、サブスレッドからProgressChangedを経由してNavigateする場合とで違いもあります。
    1つ目の場合、ページ処理が複数あるということは、1枚づつロード&処理をシリアルに行います。
    Navigateメソッドは、ロード指示だけで、処理はDocumentCompletedが行いますが、その間待っているため、全てが終わるまでフリーズになります。
    2つ目の場合、ボタンイベントはサブスレッドの起動だけなので、メインスレッドはすぐにフリーズが解けます。
    ただし、そのサブスレッドから、中やイベントを介してロード&処理を行います。
    このため、WebBrowserに絡むイベント(DocumentCompleted)はメインスレッドから呼ばれることになるため、このイベントが発生した時は、フリーズするのではないかと思います。
    ただ、2つ目の場合なら、ロード中のフリーズは避けられます。
    私の場合、ロードによるフリーズが一番ネックになりそう(処理はそれ程大したことない)なので、これは効果的です。
    理屈だけなので、実コードに適用して動かしてみたいと思います。
    その結果が出たら報告したいと思うので、それまで、この質問の回答を保留としておきます。
    2010年12月26日 20:31
  • ただ、2つ目の場合なら、ロード中のフリーズは避けられます。

    Button の Click イベントで Navigate メソッドだけを書いた場合も、同じように避けられませんか?
    これが避けられないのなら、ProgressChanged イベントで Navigate メソッドだけを書いた場合も避けられないはずです。
    理由は、両者がほぼ同じことをしているからです。

    <A.メインスレッドだけで作った場合>
    1:Button の Click イベントで Navigate メソッドを呼ぶ。
    2.DocumentCompleted イベントが呼ばれるまでメインスレッドが空く。 ← ここがロード待ちですが、フリーズはしません。
    3.DocumentCompleted イベントで処理するため、少しフリーズ。
    4.Navigate メソッドを呼ぶ。
    5.すべてのページを順次処理し終えるまで、3~4を繰り返す。

    <B.ご掲示の BackgroundWorker の使い方の場合>
    1:Button の Click イベントで RunWorkerAsync メソッドを呼ぶ。
    2.ProgressChanged イベントが呼ばれるまでメインスレッドが空く。
    3.ProgressChanged イベントで Navigate メソッドを呼ぶ。
    4.DocumentCompleted イベントが呼ばれるまでメインスレッドが空く。 ← ここがロード待ちですが、フリーズはしません。
    5.DocumentCompleted イベントで処理するため、少しフリーズ。
    6.処理が完了したことをフラグか何かでセットして、メインスレッドが空く。
    7.サブスレッドはフラグか何かで処理が終わったことを検知し、次のページのために ReportProgress メソッドを呼ぶ。
    8.すべてのページを順次処理し終えるまで、2~7を繰り返す。

    隙間が空く部分がやや増えますが、これは Navigate メソッドを呼ぶ前の DoWork イベントでの処理量がほぼなければ、あまり効果がありません。
    見えている話だと、「ページのロード時間が一番長い(一番ネックになる)」とのことですので、どちらの書き方をしても同じようにフリーズは回避できるはずです。
    逆にメインスレッドだけで書いてだめな場合(A.2でフリーズする場合)、今の BackgroundWorker のやり方でも同じようにだめなはずです。(B.4もメインスレッドであるため)


    質問スレッドで解決した場合は、解決の参考になった投稿に対して「回答としてマーク」のボタンを押すことで、同じ問題に遭遇した別のユーザが役立つ投稿を見つけやすくなります。
    2010年12月26日 22:15
    モデレータ
  • わかりやすい!
    まさしくその通りなのですが、ボタンを押す等の1回の操作で複数のページを処理するために違いがおきます。
    もしかしたら、WebBrowserを詳しく知らない(Documentプロパティだけで実現できると思い、HtmlDocument.Windowプロパティは把握してません。ページ内容も含めて履歴が扱えるんでしょうか???)ためなのかもしれませんが、Documentは、ロードし終わった現在のページだけを保持していると解釈しています。
    このため、次のページをNavgateするためには、現在のページを処理し終わっていること(Navgateを呼んだ時点で、現在のページを保持しているDocumentは破棄されてしまう)が条件になります。
    Navgateを呼んだ後、次のページをNavgateするためには、ページにまつわる処理等の実処理は行いませんが、現行ページの処理を待つ必要があります。
    Clickから直接行った場合、次のページをNavgateするために、ロードも含めて処理完了を待ちます。
    この間、操作はフリーズになります。
    ページが複数あるため、ずっとフリーズすることになります。
    BackgroundWorkerを使った場合、この待ちをサブスレッドがしているため、メインスレッドは、メインスレッドから呼ばれるDocumentCompletedでの処理の時のみのフリーズになるかと思います。
    思ったのですが、直接の場合、ロード・処理完了待ちをClick内でした場合、DocumentCompletedはメインスレッドから呼ばれるものだから、メインスレッドがClickで止まっているため、DocumentCompletedが呼べずにデッドロックになることはないでしょうか?
    2010年12月27日 11:23
  • 試した簡易コードを示します。

    private: System::Void button5_Click(System::Object^ sender, System::EventArgs^ e) {
    	try {
    		this->backgroundWorker1->RunWorkerAsync();
    	}
    	catch ( Exception^ e ){
    		MessageBox::Show( e->Message );
    	}
    }
    private: System::Void backgroundWorker1_DoWork(System::Object^ sender, System::ComponentModel::DoWorkEventArgs^ e) {
    	String^	Tmp;
    
    	try {
    		this->bLoad = false;
    		this->backgroundWorker1->ReportProgress( 0, "http://www1" );
    		while ( !this->bLoad ){
    			Sleep( 500 );
    		}
    		this->bLoad = false;
    		this->backgroundWorker1->ReportProgress( 0, "http://www2" );
    		while ( !this->bLoad ){
    			Sleep( 500 );
    		}
    	}
    	catch ( Exception^ e ){
    		MessageBox::Show( String::Format( "{0}\n\n{1}", e->ToString(), e->Message ) );
    	}
    }
    private: System::Void webBrowser1_DocumentCompleted(System::Object^ sender, System::Windows::Forms::WebBrowserDocumentCompletedEventArgs^ e) {
    	String^	Tmp;
    
    	// 
    	if ( e->Url->AbsoluteUri == this->Arg_URL ){
    		MessageBox::Show( this->webBrowser1->Document->Url->AbsolutePath );
    		this->bLoad = true;
    	}
    }
    private: System::Void backgroundWorker1_ProgressChanged(System::Object^ sender, System::ComponentModel::ProgressChangedEventArgs^ e) {
    	this->Arg_URL = ( String^ )( e->UserState );
    	this->webBrowser1->Navigate( this->Arg_URL );
    }
    
    

    2010年12月27日 11:32
  • Documentは、ロードし終わった現在のページだけを保持していると解釈しています。

    その通りです。

    Clickから直接行った場合、次のページをNavgateするために、ロードも含めて処理完了を待ちます。
    この間、操作はフリーズになります。
    ページが複数あるため、ずっとフリーズすることになります。

    多分、ループとか、Application.DoEvents とかそういったことを書いて処理を待ち、次の処理をする、一つのイベントの中で連続的にコードを書かないと実現できないとお考えなのかもしれませんが、結構よく聞くハマリ方です。
    メンバー変数とか、うまく書けば、そういったループじゃなくても作れます。

    ただ、最初は難しいかもしれませんので、イメージコードを提示しますので参考にしてみてください。
    (C# の書き方で書いてしまっていますし、さらにコンパイルを試していません。適宜 C++/CLI に書き換えていく、修正していくことを試みてください)

    private Stack<string> _urls = new Stack<string>();
    
    private void _button_Click(object sender, EventArgs e)
    {
     // 処理するURLをまとめて登録しておく
     // ここではStackなので後に入れた方が先に処理されることになる
     // 必要に応じてListなど、別のクラスを使っても良い。
     _ulrs.Push("http://a/");
     _ulrs.Push("http://b/");
     _ulrs.Push("http://c/");
     _button.Enabled = false;
     NavigateNextUrl();
    }
    
    private void NavigateNextUrl()
    {
     // Stackなので取り出すと要素が減る
     // 要素がなくなるとCount = 0となるはずなので、それで完了を見分ける
     if (_urls.Count == 0)
     {
     // 処理完了
     _button.Enabled = true;
     return;
     }
     string nextUrl = _urls.Pop();
     _webBrowser.Navigate(nextUrl);
    }
    
    private void _webBrowser_DocumentCompleted(object sender, EventArgs e)
    {
     // 読み込み終了
     // **何らかの解析処理**
     // 解析処理終了
     // 次へ
     NavigateNextUrl();
    }
    
    

    このコードにおいてポイントがあるとすれば、一連の URL 読み込みを一つのイベントの中で連続させず、一度イベントから抜けることです。
    こうすれば、BackgroundWorker.DoWork イベントなどで処理完了待ちループを書かなくて済みますし、BackgroundWorker を使わなくても済みます。
    (一連の処理を連続で書かずに、いかに状態を維持するか、続きを実行するかというところかなぁ)


    質問スレッドで解決した場合は、解決の参考になった投稿に対して「回答としてマーク」のボタンを押すことで、同じ問題に遭遇した別のユーザが役立つ投稿を見つけやすくなります。

     

    • 回答としてマーク K6963 2010年12月29日 20:53
    2010年12月27日 15:40
    モデレータ
  • すごい!
    まず、Stackクラスなんてものを始めて見ました(^^;
    アセンブリを思い出す。
    このままの場合、全てのロードするページが予めわかっている必要がありますね。
    私の場合、ページの処理によって、その後のページが変わってくるので、このままでは使えません。
    しかし、手を加えれば、この流れが使えそうな気がします。
    グローバルのurlsをarray<String^>^だかにし、更にグローバルとしてそのインデックスを設けて、インデックスに沿ってNavigate。
    DocumentCompletedの中では、処理結果にある次ページ情報をurlsに反映。
    どうしてこういう流れを思いつかなかったんだろう。。。(^^;
    試してみます(^^)

    2010年12月27日 18:07
  • 実コードで、ほぼうまくいきました。
    グローバルを使うことによって、NavigateNextUrlで1クッションおかずに書くことができ、かなり単純なコードになりました。
    ありがとうございました。

    2010年12月29日 20:53
  • グローバルを使うことによって、NavigateNextUrlで1クッションおかずに書くことができ、かなり単純なコードになりました。

    グローバル変数、メンバー変数など、用語は適切に使いましょう。

    私が示したコード例はクラスの中に配置することを想定しており、グローバル変数ではありません。


    質問スレッドで解決した場合は、解決の参考になった投稿に対して「回答としてマーク」のボタンを押すことで、同じ問題に遭遇した別のユーザが役立つ投稿を見つけやすくなります。
    2010年12月30日 1:21
    モデレータ