住所データから最寄り駅と徒歩時間を調べたい。実はここを使えば簡単に調べることができます。
しかし住所データが大量にあったらどうでしょうか? ひとつひとつ調べていては時間がかかります。そこでHTMLをしらべて一気に取得してしまいましょう。
このサイトもHTMLに直接検索結果が書かれているわけではありません。そこでSelenium.Chromeを使います。
F12キーを押して、ブラウザの開発ツール インスペクター/Elementsで調べてみると検索ボックスの周辺は以下のようになっています。
1 2 3 4 |
<input class="SearchBoxKeyword__searchInputBox" type="search" placeholder="キーワードを入力" autocapitalize="none" autocomplete="off" autocorrect="off" value="" data-rapid_p="1"> |
また検索をするために押すボタンは以下のようになっています。
1 2 3 4 5 |
<button class="SearchBoxKeyword__searchButton" type="button" aria-label="検索ボタン" data-ylk="rsec:srchbox;pos:1;" data-rapid_p="2"> 検索 </button> |
最寄り駅が表示されている部分はこんな感じです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
<span class="Keyvalue__listItemRow"> 押上駅 <span class="util-text--weak"> (京成押上線 <span class="util-delimiter">/</span> 都営浅草線 <span class="util-delimiter">/</span> 東京メトロ半蔵門線 <span class="util-delimiter">/</span> 東武伊勢崎線) </span> <span class="util-delimiter">-</span> <button type="button" class="KeyvalueListItem__button" data-ylk="rsec:poi;slk:nearsta;pos:0;" data-rapid_p="4"> 3分 </button> </span> |
結果が表示された場合は「閉じる」ボタンが表示されます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
<button type="button" class="Button Button--level5" target="_blank" data-rapid_p="14"> <i class="Icon Icon--cross Icon--xsmall Icon--primary" aria-hidden="true"> <svg width="48" height="48" viewBox="0 0 48 48"> <defs><path d="M28.059 一部省略" id="cross_svg__path-1"></path></defs> <g id="cross_svg__Symbols/Icons---Reference" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> <g id="cross_svg__cross"><mask id="cross_svg__mask-2" fill="#fff"> <use xlink:href="#cross_svg__path-1"></use> </mask> <use id="cross_svg__Fill-1" fill="#444" xlink:href="#cross_svg__path-1"></use> </g> </g> </svg> </i> <span class="Button__text">閉じる</span></button> |
ただ「あああ」のような住所ではないキーワードで検索した場合は最寄り駅は表示されないし、「閉じる」ボタンも表示されません。
住所で検索して最寄り駅が表示されるときのurlは
https://map.yahoo.co.jp/place?XXXXXXX
です。
これらがわかればなんとかできそうです。さっそく作成してみましょう。
まず住所データが大量に入力されたExcelファイルを作成します。入力されているのはA列だけです。また途中で空白行があったらデータはそこで終わりであるものとして扱います。
まずはExcelファイルから住所のリストを取得するメソッドをつくります。ClosedXMLを使うのでNugetでインストールしておきましょう。
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 |
using ClosedXML.Excel; public partial class Form1 : Form { List<string> GetListFromExcelFile(string filePath) { List<string> vs = new List<string>(); using (var workbook = new XLWorkbook(filePath)) { var worksheet = workbook.Worksheet(1); int i = 1; while (true) { var cell = worksheet.Cell(i, "A"); i++; string str = cell.GetValue<string>(); if (str == null || str == "") break; vs.Add(str); } } return vs; } } |
取得したデータを格納するクラスをつくります。
1 2 3 4 5 6 |
public class Data { public string StationName = ""; // 駅名 public string StationInfo = ""; // 何線? public string WalkTime = ""; // 徒歩で何分? } |
1 2 3 4 5 |
public class Result { public string Place = ""; // 住所 public List<Data> Datas = new List<Data>(); // 最寄り駅の情報 通常3件 } |
次にNuGetでSelenium.WebDriverとSelenium.Support、そして操作したいブラウザのDriverをインストールします。今回はChromeを操作するのでSelenium.Chrome.WebDriverをインストールします。
ChromeDriverを生成したら、住所の文字列から最寄り駅の情報をリストで取得するGetDataSearchResultメソッドをつくります。この自作メソッドのなかで検索ワードを入力して検索ボタンをおすSearch(string str)メソッド(後述)、最寄り駅のリストを取得する GetStationInfos()メソッド(後述)を呼び出しています。
またページが読み込まれていないとボタンは押せないので実際にできるようになるまで1秒待機と実行を繰り返しています。
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 |
using OpenQA.Selenium; using OpenQA.Selenium.Chrome; public partial class Form1 : Form { ChromeDriver driver = null; void Init() { ChromeDriverService driverService = ChromeDriverService.CreateDefaultService(); driverService.HideCommandPromptWindow = true; ChromeOptions options = new ChromeOptions(); options.AddArgument("--headless"); driver = new ChromeDriver(driverService, options); driver.Url = "https://map.yahoo.co.jp/maps"; } Result GetDataSearchResult(string place) { List<Data> datas = null; // 検索ワードを入力して検索ボタンを押すことができるまで繰り返す while (!Search(place)) { System.Threading.Thread.Sleep(1000); } // ページ遷移。移動前と後で実際に変化しているか調べている while (true) { System.Threading.Thread.Sleep(1000); if (CurUrl != driver.Url) { CurUrl = driver.Url; break; } } // 検索した文字列が住所ではなかった場合。検索ページに戻る if (CurUrl.IndexOf("https://map.yahoo.co.jp/place?") != 0) { Result result = new Result() { Place = place, Datas = null, }; CurUrl = driver.Url; driver.Url = "https://map.yahoo.co.jp/maps"; while (true) { System.Threading.Thread.Sleep(1000); if (CurUrl != driver.Url) { CurUrl = driver.Url; break; } } return result; } // 最寄り駅の情報を取得できるまで繰り返す while (datas == null) { System.Threading.Thread.Sleep(1000); datas = GetStationInfos(); } // 最寄り駅の情報を取得したら「閉じる」ボタンを押して次の検索ができるようにする driver.FindElementByClassName("Button--level5").Click(); { Result result = new Result() { Place = place, Datas = datas, }; return result; } } } |
検索ワードを入力して検索ボタンを押すSearch(string str)メソッドを示します。ページが読み込まれていない場合は例外が発生するので1秒待ってもう一度やり直します(前述)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
public partial class Form1 : Form { bool Search(string str) { try { IWebElement searchInputBox = driver.FindElementByClassName("SearchBoxKeyword__searchInputBox"); searchInputBox.SendKeys(str); CurUrl = driver.Url; driver.FindElementByClassName("SearchBoxKeyword__searchButton").Click(); return true; } catch { return false; } } } |
最寄り駅情報を取得するGetStationInfos()メソッドを示します。これもページが読み込まれていない場合は例外が発生するので1秒待ってもう一度やり直します(前述)。
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 |
public partial class Form1 : Form { List<Data> GetStationInfos() { List<Data> datas = new List<Data>(); try { var listItems = driver.FindElementsByClassName("Keyvalue__listItemRow"); foreach (IWebElement item in listItems) { var utilTextWeakElm = item.FindElement(By.ClassName("util-text--weak")); string utilTextWeak = utilTextWeakElm.Text; var utilTimeElm = item.FindElement(By.ClassName("KeyvalueListItem__button")); string utilTime = utilTimeElm.Text; int last = item.Text.LastIndexOf(utilTextWeak); string stationName = item.Text.Substring(0, last); string stationInfo = utilTextWeak; Data data = new Data() { StationName = stationName, StationInfo = stationInfo, WalkTime = "徒歩 " + utilTime, }; datas.Add(data); } return datas; } catch { return null; } } } |
SaveExcelメソッドは取得されたResultのリストをファイルに保存するためのものです。新しくシートを作成してそこにデータを書き込みます。最寄り駅情報が取得できなかった場合は「データなし」と記録します。
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 |
public partial class Form1 : Form { void SaveExcel(List<Result> results, string filePath) { using (var workbook = new XLWorkbook(filePath)) { string newSheetName = "結果"; int no = 1; IXLWorksheet worksheet = null; while (true) { var sheet = workbook.Worksheets.FirstOrDefault(x => x.Name == newSheetName + no.ToString()); if (sheet == null) { worksheet = workbook.Worksheets.Add(newSheetName + no.ToString()); break; } no++; } int row = 0; foreach (Result result in results) { if (result.Datas == null) { row++; worksheet.Cell(row, "A").Value = result.Place; worksheet.Cell(row, "B").Value = "データなし"; continue; } int firstRow = row + 1; foreach (Data data in result.Datas) { row++; worksheet.Cell(row, "A").Value = result.Place; worksheet.Cell(row, "B").Value = data.StationName; worksheet.Cell(row, "C").Value = data.StationInfo; worksheet.Cell(row, "D").Value = data.WalkTime; } int lastRow = row; if (firstRow != lastRow) { string range = String.Format("A{0}:A{1}", firstRow, lastRow); worksheet.Range(range).Merge(); } } workbook.Save(); } } } |
実際に処理が進行しているのかどうかがわかるようにプログレスバーを表示させます。これまでに示したコードを少し変更します。
まず開始ボタンが押されたらファイル選択、データの取得などを行います。isDoingというフラグを使っていますが、これは終了するときに処理が完了したのかしていないのかが判断できるようにするためのものです(ChromeDriverをDisposeするときに必要)。
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 |
public partial class Form1 : Form { bool isDoing = false; private void button1_Click(object sender, EventArgs e) { string filePath = ""; OpenFileDialog dialog = new OpenFileDialog(); dialog.Filter = "Excelファイル(*.xlsx)|*.xlsx"; DialogResult dr = dialog.ShowDialog(); if (dr == DialogResult.OK) filePath = dialog.FileName; dialog.Dispose(); if (filePath == "" || !System.IO.File.Exists(filePath)) return; isDoing = true; button1.Enabled = false; List<string> places = GetListFromExcelFile(filePath); // 上のプログレスバーは全件のうち何件取得できたかを示す // 下のプログレスバーはひとつひとつの取得がどの段階まで完了したかを示す progressBar1.Value = 0; progressBar1.Maximum = places.Count; progressBar2.Value = 0; Task.Run(() => { ChromeDriverService driverService = ChromeDriverService.CreateDefaultService(); driverService.HideCommandPromptWindow = true; ChromeOptions options = new ChromeOptions(); options.AddArgument("--headless"); driver = new ChromeDriver(driverService, options); driver.Url = "https://map.yahoo.co.jp/maps"; List<Result> results = GetDataSearchResults(places); SaveExcel(results, filePath); if (isDoing) { driver.Quit(); driver.Dispose(); } isDoing = false; Invoke((Action)(() => { progressBar1.Value = progressBar1.Maximum; progressBar2.Value = progressBar2.Maximum; })); MessageBox.Show("処理が完了しました!"); button1.Enabled = true; }); } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
public partial class Form1 : Form { private void Form1_FormClosed(object sender, FormClosedEventArgs e) { // 処理が終わる前に終了するときはdriverをDisposeしなければならない if (isDoing) { this.Visible = false; driver.Quit(); driver.Dispose(); isDoing = false; } } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
public partial class Form1 : Form { List<Result> GetDataSearchResults(List<string> places) { Invoke((Action)(() => { progressBar1.Value = 0; progressBar1.Maximum = places.Count; progressBar1.Value = 0; })); List<Result> results = new List<Result>(); foreach (string place in places) results.Add(GetDataSearchResult(place)); return results; } } |
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 |
public partial class Form1 : Form { Result GetDataSearchResult(string place) { List<Data> datas = null; Invoke((Action)(() => { progressBar2.Maximum = 3; progressBar2.Value = 0; })); while (!Search(place)) { System.Threading.Thread.Sleep(1000); } Invoke((Action)(() => { progressBar2.Value = 1; })); while (true) { System.Threading.Thread.Sleep(1000); if (CurUrl != driver.Url) { CurUrl = driver.Url; break; } } Invoke((Action)(() => { progressBar2.Value = 2; })); if (CurUrl.IndexOf("https://map.yahoo.co.jp/place?") != 0) { Result result = new Result() { Place = place, Datas = null, }; CurUrl = driver.Url; driver.Url = "https://map.yahoo.co.jp/maps"; while (true) { System.Threading.Thread.Sleep(1000); if (CurUrl != driver.Url) { CurUrl = driver.Url; break; } } Invoke((Action)(() => { progressBar1.Value++; })); return result; } while (datas == null) { System.Threading.Thread.Sleep(1000); datas = GetStationInfos(); } Invoke((Action)(() => { progressBar2.Value = 3; })); Invoke((Action)(() => { progressBar1.Value++; })); driver.FindElementByClassName("Button--level5").Click(); { Result result = new Result() { Place = place, Datas = datas, }; return result; } } } |