タイトルそのままです。PuppeteerSharpをつかって検索結果を取得します。また虫眼鏡ワードも同時に取得します。これによってどのようなキーワードが検索されているかもわかります。
もっともサジェスト汚染をする輩もいるので、これで本当に検索需要があるキーワードが取得できるかどうかはわかりませんが・・・。あとサーバーに過度の負荷をかけないようにほどほどに使うようにしてください。
Contents
なぜPuppeteerSharp?
webスクレーピングではSeleniumを使うことのほうが多いかもしれません。しかしSelenium.Chrome.WebDriverを使う場合、アプリを使用しようとしているパソコンにGoogle Chromeがインストールされていないと使うことができません。
またGoogle Chromeはバージョンアップされることがあります。Selenium.Chrome.WebDriverはインストールされているGoogle Chromeのバージョンがあっていないとうまく動きません。Google Chromeが自動的にバージョンアップされてしまい、これに対応したSelenium.Chrome.WebDriverはまだ存在しないとなれば完全にお手上げです。
そこでPuppeteerSharpを使います。これを使う場合、実行ファイルがあるフォルダにGoogle Chromeがダウンロードされます。そのためGoogle Chromeがインストールされていない環境であっても困ることはありません。WebDriverのバージョン問題も気にしなくていいのですが、実行ファイルがあるフォルダの容量が非常に大きくなります。約400MBくらいになりますが、使わないときはファイルを削除するとか、いまでは大容量ハードディスクも安価になっているので気にする必要はないと思います。
PuppeteerSharpを使ってみる
まずは基本的な操作から。
Yahoo!のトップページにアクセスする
1 2 3 4 5 6 7 |
await new BrowserFetcher().DownloadAsync(BrowserFetcher.DefaultChromiumRevision); LaunchOptions op = new LaunchOptions(); op.Headless = false; // trueにすればブラウザは見えなくなる Browser browser = await Puppeteer.LaunchAsync(op); Page page = await browser.NewPageAsync(); await page.GoToAsync("https://www.yahoo.co.jp/"); |
これでYahoo!のトップページにアクセスすることができます。
キーワードを入力して検索ボタンを押す
次に検索したいキーワードを入力して検索ボタンを押すためにはHTMLのどの部分に検索窓と検索ボタンがあるかを知る必要があります。F12キーを押して要素を解析してみると、検索窓のXPathは/html/body/div/div/header/section[1]/div/form/fieldset/span/inputであり、ボタンは/html/body/div/div/header/section[1]/div/form/fieldset/span/buttonであることがわかります。
以下の方法で検索したい文字列を入力して検索ボタンを押すことができます。
1 2 3 4 5 6 7 8 9 10 |
// XPathで取得した検索窓に文字列を入力する ElementHandle inputs = await page.WaitForXPathAsync("/html/body/div/div/header/section[1]/div/form/fieldset/span/input"); await inputs.TypeAsync("入力したい文字列"); // XPathで取得したボタンをクリックする ElementHandle button = await page.WaitForXPathAsync("/html/body/div/div/header/section[1]/div/form/fieldset/span/button"); await button.ClickAsync(); // 新しいページが読み込まれるまで待つ await page.WaitForNavigationAsync(); |
検索結果を取得する
次に検索結果を表示しているページからページタイトルとurlを取得する方法を考えます。
まずクリックするとそのページへいけるリンクの取得ですが、一発でやろうとしてもうまくいかないので2回にわけます。
1 |
ElementHandle[] searchResultElements = await page.QuerySelectorAllAsync("div.sw-Card.Algo"); |
これで要素を取得して、このなかからページのタイトルとurlを取得します。
上記で得た配列をforeach文で回して、そのなかからa.sw-Card__titleInnerとh3.sw-Card__titleMain.sw-Card__titleMain–clamp.sw-Card__titleMain–cite.util-Clamps–2を取得します。これでurlとページタイトルは取得できます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
foreach (ElementHandle searchResultElement1 in searchResultElements) { // リンクがあるところまでスクロールさせ、処理がおこなわれていることが視覚的にわかるようにする await searchResultElement1.HoverAsync(); try { // オーガニック検索の結果からページタイトルとurlを取得する // 最初に"JSHandle:"が入るので最初の9文字は捨てる ElementHandle searchResultElement2 = await searchResultElement1.QuerySelectorAsync("a.sw-Card__titleInner"); string url = searchResultElement2.GetPropertyAsync("href").Result.ToString().Substring(9); ElementHandle searchResultElement3 = await searchResultElement2.QuerySelectorAsync("h3.sw-Card__titleMain.sw-Card__titleMain--clamp.sw-Card__titleMain--cite.util-Clamps--2"); string title = searchResultElement3.GetPropertyAsync("textContent").Result.ToString().Substring(9); } catch { } } |
次のページへ移動する
10位まででなくそれ以降の結果も取得するのであれば次のページへのリンクを探してクリックしなければなりません。次のページのリンクは以下のコードで取得できますが、ない場合は例外が発生するので例外処理をしておきます。
次のページへのリンクが見つかったらクリックして、新しいページが読み込まれるまで待ちます。
1 2 3 4 5 6 7 8 9 |
try { ElementHandle nextElements = await page.QuerySelectorAsync("div.Pagenation__next a"); await nextElements.ClickAsync(); await page.WaitForNavigationAsync(); } catch { } |
虫眼鏡ワードを取得する
虫眼鏡ワードの取得方法ですが、ul.Unit__list li.Unit__listItem aで取得できます。もし虫眼鏡ワードがなければなにも取得できないのでmushimeganesのなかは空になっているはずです。空でない場合は重複(ページ上と下部の2箇所に表示されるので重複があるかもしれない)を取り除きます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
List<string> mushimeganes = new List<string>(); try { ElementHandle[] mushimeganeElements = await page.QuerySelectorAllAsync("ul.Unit__list li.Unit__listItem a"); foreach (ElementHandle mushimeganeElement1 in mushimeganeElements) { string mushimegane = mushimeganeElement1.GetPropertyAsync("textContent").Result.ToString().Substring(9); mushimeganes.Add(mushimegane); } } catch { } // mushimeganes.Count==0でないなら取得できている。 // 重複を取り除く if (mushimeganes.Count > 0) mushimeganes = mushimeganes.Distinct().ToList(); |
アプリをつくる
それでは実際にアプリをつくってみましょう。同時に複数のキーワードを調べられるように改行してキーワードを入力しておけば一気に調べられるものをつくります。
1 2 3 4 5 6 7 8 9 10 11 12 |
public partial class Form1 : Form { private async void button1_Click(object sender, EventArgs e) { string[] vs = richTextBox1.Lines; List<string> list = vs.ToList(); list = list.Where(x => x != "").ToList(); // 第二引数は取得する件数 今回は30位まで await GetSearchResult(list, 30); } } |
動いていることがわかるようにブラウザは表示させたままにしておきます。非表示にさせたほうが作業の邪魔にならないからよいというのであればHeadless = trueにします。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
public partial class Form1 : Form { async Task GetSearchResult(List<string> vs, int max) { await new BrowserFetcher().DownloadAsync(BrowserFetcher.DefaultChromiumRevision); LaunchOptions op = new LaunchOptions(); op.Headless = false; Browser browser = await Puppeteer.LaunchAsync(op); Page page = await browser.NewPageAsync(); foreach (string keyword in vs) { await GetSearchResult(page, keyword, max); } await page.DisposeAsync(); await browser.DisposeAsync(); } } |
GetSearchResultメソッドが呼び出されたときの処理を示す前に取得したデータを保存するクラスを作成します。
1 2 3 4 5 6 7 8 9 10 11 12 |
public class SearchResult { public string Count = ""; // 検索結果の件数 public List<string> MushimeganeWord = new List<string>(); // 虫眼鏡ワード(あれば) public List<PageInfo> PageInfos = new List<PageInfo>(); // ページタイトルとurlのペア } public class PageInfo { public string Url = ""; public string PageTitle = ""; } |
では、GetSearchResultメソッドが呼び出されたときの処理を示します。https://www.yahoo.co.jp/にアクセスして検索したい文字列を入力して検索ボタンを押します。そして虫眼鏡ワードがあれば取得し、検索結果の総数と今回は30位までのページタイトルとurlを取得します。最後に取得したデータをExcelファイルとして保存します。
Excelファイルはアプリケーションの実行ファイルがあるフォルダの下にoutputというフォルダをつくり、そこに保存します。
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 |
public partial class Form1 : Form { private async Task GetSearchResult(Page page, string keyWord, int max) { SearchResult searchResult = new SearchResult(); await page.GoToAsync("https://www.yahoo.co.jp/"); // 検索したい文字列を入力して検索ボタンを押す await ClickSearchButton(page, keyWord); // 検索結果の総数を取得 await GetHitCount(page, searchResult); // 虫眼鏡ワードがあれば取得する await GetMushimegane(page, searchResult); while (true) { // 検索結果を表示しているページからページタイトルとurlを取得する await GetPageInfoSERP(page, searchResult, max); // 最大取得数を取得しているなら終了 if (searchResult.PageInfos.Count >= max) break; // 次のページへのリンクがあるならクリック。ないなら終了 try { ElementHandle nextElements = await page.QuerySelectorAsync("div.Pagenation__next a"); await nextElements.ClickAsync(); await page.WaitForNavigationAsync(); } catch { break; } } // 取得したデータをExcelファイルとして保存する string folderPath = Application.StartupPath + "\\output"; if (!System.IO.Directory.Exists(folderPath)) System.IO.Directory.CreateDirectory(folderPath); string filePath = folderPath + "\\" + keyWord + ".xlsx"; SaveExcelFile(filePath, searchResult); } } |
検索したい文字列を入力して検索ボタンを押す処理を示します。
1 2 3 4 5 6 7 8 9 10 11 12 |
public partial class Form1 : Form { async Task ClickSearchButton(Page page, string keyWord) { ElementHandle inputs = await page.WaitForXPathAsync("/html/body/div/div/header/section[1]/div/form/fieldset/span/input"); await inputs.TypeAsync(keyWord); ElementHandle button = await page.WaitForXPathAsync("/html/body/div/div/header/section[1]/div/form/fieldset/span/button"); await button.ClickAsync(); await page.WaitForNavigationAsync(); } } |
検索結果の総数を取得してSearchResultオブジェクト内に格納する処理を示します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
public partial class Form1 : Form { async Task GetHitCount(Page page, SearchResult searchResult) { try { ElementHandle hitCountElement = await page.WaitForXPathAsync("/html/body/div/header/div[2]/div/div"); string str = hitCountElement.GetPropertyAsync("textContent").Result.ToString().Substring(9); searchResult.Count = str.Replace("1ページ目", ""); } catch { } } } |
虫眼鏡ワードがあれば取得する処理を示します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
public partial class Form1 : Form { async Task GetMushimegane(Page page, SearchResult searchResult) { List<string> mushimeganes = new List<string>(); try { ElementHandle[] mushimeganeElements = await page.QuerySelectorAllAsync("ul.Unit__list li.Unit__listItem a"); foreach (ElementHandle mushimeganeElement1 in mushimeganeElements) { string mushimegane = mushimeganeElement1.GetPropertyAsync("textContent").Result.ToString().Substring(9); mushimeganes.Add(mushimegane); } } catch { } if (mushimeganes.Count > 0) { mushimeganes = mushimeganes.Distinct().ToList(); searchResult.MushimeganeWord = mushimeganes; } } } |
検索結果を表示しているページからページタイトルとurlを取得する処理を示します。実際に処理が進んでいるのか、フリーズしていないかがわかるようにブラウザをスクロールさせ視覚的に処理がおこなわれていることがわかるようにしています。
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 |
public partial class Form1 : Form { async Task GetPageInfoSERP(Page page, SearchResult searchResult, int max) { // オーガニック検索の結果だけ集める ElementHandle[] searchResultElements = await page.QuerySelectorAllAsync("div.sw-Card.Algo"); foreach (ElementHandle searchResultElement1 in searchResultElements) { // リンクがあるところまでスクロールさせ、処理がおこなわれていることが視覚的にわかるようにする await searchResultElement1.HoverAsync(); try { // オーガニック検索の結果からページタイトルとurlを取得する ElementHandle searchResultElement2 = await searchResultElement1.QuerySelectorAsync("a.sw-Card__titleInner"); string url = searchResultElement2.GetPropertyAsync("href").Result.ToString().Substring(9); ElementHandle searchResultElement3 = await searchResultElement2.QuerySelectorAsync("h3.sw-Card__titleMain.sw-Card__titleMain--clamp.sw-Card__titleMain--cite.util-Clamps--2"); string title = searchResultElement3.GetPropertyAsync("textContent").Result.ToString().Substring(9); PageInfo info = new PageInfo(); info.PageTitle = title; info.Url = url; searchResult.PageInfos.Add(info); // 最大取得数を取得しているのであれば終了 if (searchResult.PageInfos.Count >= max) return; } catch { } } } } |
最後に取得したデータをExcelファイルとして保存する処理を示します。
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 |
public partial class Form1 : Form { void SaveExcelFile(string filePath, SearchResult searchResult) { using (var workbook = new XLWorkbook()) { var worksheet = workbook.Worksheets.Add("シート1"); int row = 1; foreach (PageInfo info in searchResult.PageInfos) { worksheet.Cell(row, 1).Value = info.PageTitle; worksheet.Cell(row, 2).Value = info.Url; row++; } row++; worksheet.Cell(row, 1).Value = searchResult.Count; row += 2; if (searchResult.MushimeganeWord.Count > 0) { worksheet.Cell(row, 1).Value = "虫眼鏡ワード"; row++; foreach (string word in searchResult.MushimeganeWord) { worksheet.Cell(row, 1).Value = word; row++; } } workbook.SaveAs(filePath); } } } |