前回はひとつのキーワードでしか検索結果を取得することができませんでした。今回は複数行入力することで複数の検索ワードを調べることができるようにします。また検索結果をテキストファイルに保存します。
やっていることは前回とほとんど変わりません。ただWebBrowserコントロールがページを読み込まないと処理がはじまらない、どうしてもページにアクセスするためのメソッドと読み込みが完了したときに実行されるメソッドが異なるという事情でわかりにくくなるためにクラスとして独立させました。処理が完了したら自分で定義したイベント(GotSearchResult)で結果をForm1に知らせます。
追加した機能としてはヒット件数も取得できるようにしています。
それから実際に動作させてみると時間がかかります。そこでプログレスバーを表示させます。プログレスバーは2種類、ひとつは全検索ワードのうちどれだけ完了したか、もうひとつはひとつの検索ワードの調査のうち、どれだけ完了したかです。
それから前回のアプリでは処理中はアプリケーションがフリーズしたようになってしまう(思った以上に時間がかかるときでも終了させることができない)ので、そうはならないように対策をします。
WebBrowserTempクラスのコンストラクタを示します。引数で表示させるプログレスバーを渡しています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
public class WebBrowserTemp { WebBrowser webBrowser1 = new WebBrowser(); string _searchWords = ""; ProgressBar _progressBar2 = null; public WebBrowserTemp(ProgressBar progressBar2) { webBrowser1.ScriptErrorsSuppressed = true; webBrowser1.DocumentCompleted += WebBrowser_DocumentCompleted; webBrowser1.Visible = false; _progressBar2 = progressBar2; _progressBar2.Value = 0; } } |
検索してページを解析して結果を取得するためのメソッドを示します。前回とほとんど同じです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 |
public class WebBrowserTemp { public void Search(string searchWords) { webBrowser1.Invoke((Action)(() => { _progressBar2.Value = 0; })); _searchWords = searchWords; string url = "https://search.yahoo.co.jp/search?p="; string urlEnc = System.Web.HttpUtility.UrlEncode(searchWords); url += urlEnc; webBrowser1.Invoke((Action)(() => { _progressBar2.Value = 1; })); webBrowser1.Navigate(url); } IHtmlDocument GetIHtmlDocument(string htmlText) { HtmlParser parser = new HtmlParser(); return parser.ParseDocument(htmlText); } // ヒットした件数を取得する string HitCount(string htmlText) { IHtmlDocument htmlDocument = GetIHtmlDocument(htmlText); var element = htmlDocument.GetElementById("inf"); if(element != null) return element.TextContent; else return "件数が取得できない"; } // ランキングを取得する List<SiteInfo> GetTop10(string htmlText) { List<SiteInfo> siteInfos = new List<SiteInfo>(); IHtmlDocument htmlDocument = GetIHtmlDocument(htmlText); // id = "web"の要素を取得する。 string urlElementText = htmlDocument.GetElementById("web").InnerHtml; // aタグの要素を全て取得する。 IHtmlDocument htmlDocument1 = GetIHtmlDocument(urlElementText); var urlElements1 = htmlDocument1.QuerySelectorAll("a"); foreach (var a in urlElements1) { SiteInfo info = new SiteInfo(); info.title = a.TextContent; info.url = a.Attributes["href"].Value; siteInfos.Add(info); } return siteInfos; } // 虫眼鏡ワードを取得する List<string> GetMushimeganeWords(string htmlText) { List<string> vs = new List<string>(); IHtmlDocument htmlDocument = GetIHtmlDocument(htmlText); // id = "rel"の要素を全て取得する。 string urlElementText = htmlDocument.GetElementById("rel").InnerHtml; // aタグの要素を全て取得する。 IHtmlDocument htmlDocument1 = GetIHtmlDocument(urlElementText); var urlElements1 = htmlDocument1.QuerySelectorAll("a"); foreach (var a in urlElements1) { vs.Add(a.TextContent); } return vs; } } |
上記メソッド内でつかわれているSiteInfoクラスは前回のものと同じです。
1 2 3 4 5 |
public class SiteInfo { public string title; public string url; } |
ページの読み込みが完了したときのイベントハンドラを示します。処理が進行するにしたがってプログレスバーのバーを進めています。
非同期処理をしているので「コントロールが作成されたスレッド以外のスレッドからコントロール ‘_progressBar2’ がアクセスされました」という例外が出ないようにInvoke((Action)(() => { }));を使用しています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
public class WebBrowserTemp { private void WebBrowser_DocumentCompleted(object sender, WebBrowserDocumentCompletedEventArgs e) { webBrowser1.Invoke((Action)(() => { _progressBar2.Value = 2; })); string htmlText = webBrowser1.DocumentText; List<SiteInfo> siteInfos = GetTop10(htmlText); webBrowser1.Invoke((Action)(() => { _progressBar2.Value = 3; })); List<string> vs = GetMushimeganeWords(htmlText); string hitcount = HitCount(htmlText); webBrowser1.Invoke((Action)(() => { _progressBar2.Value = 4; })); StringBuilder sb = new StringBuilder(); sb.Append(hitcount + "\n\n"); foreach(SiteInfo info in siteInfos) { string str = String.Format("{0}\n{1}\n\n", info.title, info.url); sb.Append(str); } GotSearchResult?.Invoke(this, _searchWords, sb.ToString(), String.Join("\n", vs.ToArray())); webBrowser1.Invoke((Action)(() => { _progressBar2.Value = 5; })); } public delegate void GotSearchResultHandler(object sender, string searchWords, string searchResult, string mishimegane); public event GotSearchResultHandler GotSearchResult; } |
Form1側での処理を示します。
調査したい検索ワードをRichTextBoxに入力して[開始]のボタンをクリックすると結果を保存するフォルダを選択するダイアログが表示され、OKが選択されると処理がはじまります。
処理は空白行がないかチェックして、あとは一行ずつ検索しています。検索するときはWebBrowserTempオブジェクトを生成して処理がおわったときに発生するGotSearchResultイベントを処理できるようにしています。そしてSearchメソッドで実際に検索の処理をおこなっています。
またサーバーに負荷をかけないようにリクエストは5秒おきにしています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
public partial class Form1 : Form { WebBrowserTemp webBrowserTemp = null; FolderBrowserDialog folderBrowserDialog = new FolderBrowserDialog(); public Form1() { InitializeComponent(); webBrowserTemp = new WebBrowserTemp(progressBar2); webBrowserTemp.GotSearchResult += WebBrowserTemp_GotSearchResult; // 後述 } private void button1_Click(object sender, EventArgs e) { if (folderBrowserDialog.ShowDialog() != DialogResult.OK) return; // 検索結果はテキストファイルとして保存するので richTextBox1 richTextBox2は不要 // 代わりに検索結果を取得したいワードを入力するためのrichTextBox3を設置する List<string> list = richTextBox3.Lines.ToList(); list = list.Where(x => x != "").ToList(); Task.Run(async () => { Invoke((Action)(() => { progressBar1.Maximum = richTextBox3.Lines.Length; progressBar2.Maximum = 5; progressBar1.Value = 0; progressBar2.Value = 0; })); await SaveFilesSearchResult(list); // 後述 Invoke((Action)(() => { progressBar1.Value = progressBar1.Maximum; progressBar2.Value = progressBar2.Maximum; })); MessageBox.Show("完了"); }); } } |
検索結果を取得するSaveFilesSearchResultメソッドを示します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
public partial class Form1 : Form { private async Task SaveFilesSearchResult(List<string> words) { for (int i = 0; i < words.Count; i++) { Invoke((Action)(() => { progressBar1.Value++; })); if (words[i] == "") continue; webBrowserTemp.Search(words[i]); // 最後の処理でない場合は次の処理まで5秒待つ if (i != words.Count - 1) await Task.Delay(5 * 1000); } } } |
GotSearchResultイベントが発生したらテキストファイルに保存する文字列を取得できたということなので、これをファイルに保存します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
public partial class Form1 : Form { private void Temp_GotSearchResult(object sender, string searchWords, string searchResult, string mishimegane) { string str1 = searchWords + " の検索結果"; string str2 = searchResult; string str3 = mishimegane; // 結果を選択したフォルダに保存する。 StreamWriter sw = new StreamWriter(folderBrowserDialog.SelectedPath + "\\" + str1 + ".txt"); string str = String.Format("{0}\n\n{1}\n\n{2}", str1, str2, str3); sw.Write(str); sw.Close(); progressBar2.Value++; if(progressBar2.Value == allCount) { progressBar1.Value = 0; progressBar2.Value = 0; } } } |
はじめまして。夜分遅くに申し訳ありません。
visual studio 2020 community をインストールし、C#のデスクトップアプリを動かしながら勉強したいと持ってます。
そこで、鳩でも分かるC#管理人様のたくさんの記事を参考に、勉強させていただきたいと思っています。
下記のアプリから内容を理解していきたいと思ってます。
・取得した検索結果をテキストファイルに保存する方法
・簡易画像編集ソフト
丸ごとファイルをいただくことは、できないでしょうか。
ソースコードだけだと、VisualStudio側の配置や各ツールのプロパティ設定などが理解できていないため、動かすことができず苦労しています。
ご検討いただけると助かります。
コメントありがとうございます。ソースコードをアップロードしておきます。
いつの間にか仕様が変更され、この記事にかかれている方法では検索結果を取得できなくなっていました。
記事も書き直しました。
https://lets-csharp.com/zip/get-search-result.zip
https://lets-csharp.com/zip/image-editor.zip