スクレイピングで競馬の結果を取得するプログラムをつくります。ちなみに当方、競馬にはまったく興味がありません(当たるはずがないので)。
2020年10月4日 中山1R レース情報のhtmlソースをみると以下のような構造になっていることがわかります。
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 |
<table summary="全着順" class="RaceTable01 RaceCommon_Table ResultRefund Table_Show_All" id="All_Result_Table"> <thead> <tr> <th>着順</th> <th>枠</th> <th>馬番</th> <th><div class="Horse_Name">馬名</div></th> <th>性齢</th> <th>斤量</th> <th>騎手</th> <th>タイム</th> <th>着差</th> <th>人気</th> <th>単勝オッズ</th> <th>後3F</th> <th>コーナー通過順</th> <th>厩舎</th> <th>馬体重(増減)</th> </tr> </thead> <tbody> <tr class="FirstDisplay HorseList" class="HorseList"> <td class="Result_Num"><div class="Rank">1</div></td> <td class="Num">3</td> <td class="Num">6</td> <td><span class="Horse_Name">ビーマイベイビー</span></td> <td><div class="Horse_Info_Detail">牝2</div></td> <td><span class="JockeyWeight">54.0</span></td> <td class="Jockey">杉原</td> <td class="Time">1:12.0</td> <td class="Time"></td> <td class="Odds"><span class="OddsPeople">3</span></td> <td class="Odds"><span class="Odds_Ninki">6.4</span></td> <td class="Tim">37.9</td> <td class="PassageRate">2-2</td> <td class="Trainer">美浦 加藤和</td> <td class="Weight">436<small>(0)</small></td> </tr> <tr class="FirstDisplay HorseList" class="HorseList"> 2位以下も同様 </tr> </tbody> </table> |
ここからわかることは
順位は class=”Result_Num” を調べればわかります。class=”Num”はふたつあって最初が枠、次が馬番になっている。 class=”Horse_Name” は馬の名前、 class=”Horse_Info_Detail” は馬の年齢、class=”JockeyWeight”は斤量、class=”Jockey”は騎手の名前、class=”Time”はふたつあって一つ目がタイム、二つ目が着差、class=”Odds”もふたつあって一つ目が何番人気か? 二つ目が単勝オッズ、class=”Tim”は後3F(ゴール前最後の600m(3F)の走破タイム)、class=”PassageRate”はコーナー通過順、class=”Trainer”は厩舎、class=”Weight”は馬体重、そのなかの<small>は増減であることがわかります。
あとは各ページのHTMLソースを取得して解析すれば各データを取得できるということになります。
スクレイピングにはHtmlAgilityPackを使います。HtmlAgilityPackはプロジェクトごとにNuGetから導入します。
ではHtmlAgilityPackを導入したら、さっそくスクレイピングをやってみましょう。
最初にurlからHtmlソースを取得します。これは自作メソッド GetHtmlText(string url)で取得可能です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
string GetHtmlText(string url) { string str = ""; WebRequest rq = WebRequest.Create(url); Encoding encoding = Encoding.GetEncoding("EUC-JP"); using(WebResponse res = rq.GetResponse()) using(Stream stream = res.GetResponseStream()) using(StreamReader sr = new StreamReader(stream, encoding)) str = sr.ReadToEnd(); return str; } |
次にHtmlAgilityPackをつかって必要な情報を抜き出すのですが、classから抽出することが可能です。
1 2 3 4 |
var htmlDoc = new HtmlAgilityPack.HtmlDocument(); htmlDoc.LoadHtml(htmlText); var nodes = htmlDoc.DocumentNode.SelectNodes("//tr[@class=\"HorseList\"]"); |
とやれば<tr class=”FirstDisplay HorseList” class=”HorseList”>~<tr>の間を取得することができます。それをさらに
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
foreach(HtmlNode node in nodes) { var htmlDoc = new HtmlAgilityPack.HtmlDocument(); htmlDoc.LoadHtml(node1.InnerHtml); // 着順を取得 var nodes1 = htmlDoc.DocumentNode.SelectNodes("//div[starts-with(@class, \"Rank\")]"); if(nodes1 != null) sb.Append(nodes1[0].InnerText + ","); else sb.Append("*1,"); // 取得できなかった場合 // 他のデータも抜き出す } |
とやれば必要なデータを抜き出すことができます。
着順,枠,馬番,馬名,性齢,斤量,騎手,タイム,着差,人気,単勝オッズ,後3F,コーナー通過順,厩舎,馬体重(増減)をCSVファイルとして保存するには以下のようにすれば可能です。
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 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 |
public partial class Form1 : Form { private void button1_Click(object sender, EventArgs e) { string url = "https://race.netkeiba.com/race/result.html?race_id=202006040901&rf=race_list"; string htmlText = GetHtmlText(url); var htmlDoc = new HtmlAgilityPack.HtmlDocument(); htmlDoc.LoadHtml(htmlText); var nodes = htmlDoc.DocumentNode.SelectNodes("//tr[@class=\"HorseList\"]"); StringBuilder sb = new StringBuilder(); sb.Append("着順,枠,馬番,馬名,性齢,斤量,騎手,タイム,着差,人気,単勝オッズ,後3F,コーナー通過順,厩舎,馬体重(増減)\n"); foreach(HtmlNode node in nodes) sb.Append(GetTextFromNode(node)); // 得られた文字列をCSVファイルとして保存 SaveCsvFile(@"d:\abc.csv", sb.ToString()); } string GetTextFromNode(HtmlNode node1) { var htmlDoc = new HtmlAgilityPack.HtmlDocument(); htmlDoc.LoadHtml(node1.InnerHtml); StringBuilder sb = new StringBuilder(); // 着順を取得 var nodes1 = htmlDoc.DocumentNode.SelectNodes("//div[starts-with(@class, \"Rank\")]"); if(nodes1 != null) sb.Append(nodes1[0].InnerText + ","); else sb.Append("*1,"); // 枠と馬番を取得 nodes1 = htmlDoc.DocumentNode.SelectNodes("//td[starts-with(@class, \"Num\")]"); if(nodes1 != null && nodes1.Count == 2) { sb.Append(nodes1[0].InnerText + ","); sb.Append(nodes1[1].InnerText + ","); } else sb.Append("*2,*2,"); // 馬名を取得 nodes1 = htmlDoc.DocumentNode.SelectNodes("//span[starts-with(@class, \"Horse_Name\")]"); if(nodes1 != null) sb.Append(nodes1[0].InnerText + ","); else sb.Append("*3,"); // 性齢を取得 nodes1 = htmlDoc.DocumentNode.SelectNodes("//div[starts-with(@class, \"Horse_Info_Detail\")]"); if(nodes1 != null) sb.Append(nodes1[0].InnerText + ","); else sb.Append("*4,"); // 斤量を取得 nodes1 = htmlDoc.DocumentNode.SelectNodes("//span[starts-with(@class, \"JockeyWeight\")]"); if(nodes1 != null) sb.Append(nodes1[0].InnerText + ","); else sb.Append("*5,"); // 騎手を取得 nodes1 = htmlDoc.DocumentNode.SelectNodes("//td[@class=\"Jockey\"]"); if(nodes1 != null) sb.Append(nodes1[0].InnerText + ","); else sb.Append("*6,"); //タイム 着差を取得 nodes1 = htmlDoc.DocumentNode.SelectNodes("//td[@class=\"Time\"]"); if(nodes1 != null && nodes1.Count == 2) { sb.Append(nodes1[0].InnerText + ","); sb.Append(nodes1[1].InnerText + ","); } else sb.Append("*7,*7,"); // 人気と単勝オッズを取得 nodes1 = htmlDoc.DocumentNode.SelectNodes("//td[starts-with(@class, \"Odds\")]"); if(nodes1 != null && nodes1.Count == 2) { sb.Append(nodes1[0].InnerText + ","); sb.Append(nodes1[1].InnerText + ","); } else sb.Append("*8,*8,"); // 後3Fを取得 nodes1 = htmlDoc.DocumentNode.SelectNodes("//td[starts-with(@class, \"Tim\")]"); if(nodes1 != null) sb.Append(nodes1[0].InnerText + ","); else sb.Append("*9,"); // コーナー通過順を取得 nodes1 = htmlDoc.DocumentNode.SelectNodes("//td[starts-with(@class, \"PassageRate\")]"); if(nodes1 != null) { string str1 = nodes1[0].InnerText; str1 = str1.Replace("-", "_"); sb.Append(str1 + ","); } else sb.Append("*10,"); // 厩舎を取得 nodes1 = htmlDoc.DocumentNode.SelectNodes("//td[starts-with(@class, \"Trainer\")]"); if(nodes1 != null) sb.Append(nodes1[0].InnerText + ","); else sb.Append("*11,"); // 馬体重(増減)を取得 nodes1 = htmlDoc.DocumentNode.SelectNodes("//td[starts-with(@class, \"Weight\")]"); if(nodes1 != null) sb.Append(nodes1[0].InnerText + ","); else sb.Append("*12,"); string str = sb.ToString().Replace("\n", ""); return str + "\n"; } void SaveCsvFile(string filePath, string str) { StreamWriter sw = new StreamWriter(filePath, false, Encoding.Default); sw.Write(str); sw.Close(); } } |
それからnetkeiba.comではURLは以下のルールで決められているので、開催年、競馬場コード、開催回数、日数、レース数を指定すれば他のレース結果も収集することができます。
1 |
https://race.netkeiba.com/race/result.html?race_id=開催年+競馬場コード+開催回数+日数+レース数+&rf=race_lis |