今回は、競馬データをスクレイピングしてウェブサイトから簡単に情報を取得する方法をご紹介。
GoogleスプレッドシートとGASを活用して、手軽に競馬データの収集する方法を本記事ではまとめています。
競馬ファンの方々やデータ収集・分析に興味がある方々向けにまとめていますのでぜひ最後までご覧になってください。
スクレイピングに関する注意点
本記事で紹介しているスクレイピング方法は、必ず自己責任で行ってください。
スクレイピングによっては、対象サイトの利用規約に抵触したり、サーバーに過剰な負荷をかけて迷惑をかけてしまうリスクがあります。
スクレイピングを行う際のチェックポイント
- 利用規約の確認:対象サイトの利用規約を必ず確認し、スクレイピングが許可されているかを確認しましょう。
- アクセス頻度の調整:短時間に大量のリクエストを送信するとサーバーに負担がかかります。適度なインターバルを設定し、アクセスを分散させるよう工夫してください。
- 事前の許可:商用利用や大量データの取得が必要な場合は、サイト運営者に事前に許可を取ることが望ましいです。
スクレイピングは適切に利用すれば非常に便利な技術ですが、使用方法を誤ると法律的・倫理的な問題につながることがあります。トラブルを避けるためにも、以上のポイントを守って安全に活用しましょう。
スクレイピングとは
スクレイピングとは、ウェブページから情報を抽出する技術のこと。
この技術を使うことで、ウェブサイトに掲載されているデータを自動的に取得ができます。
取得したデータは、自由に加工して分析に利用も可能です。
GASを利用すればスクレイピングも簡単に可能
通常では、高い技術が必要なスクレイピングですがGoogle Apps Script(GAS)を利用すれば比較的簡単に使えます。
そもそもGASとは何かという方のためにGASについておさらいしましょう。
GASって何?
GAS(Google Apps Script)は、Google Workspaceの機能を拡張し、カスタマイズするためのスクリプト言語です。
基本的な使い方や構文を理解することで、スプレッドシートをデータベースのように活用できます。
主な特徴は、以下になります。
作成から実行がすぐにできる
GASは、スクリプト言語と呼ばれるコンパイルをせずに即時実行、展開できる言語。
JAVA言語などのコンパイル言語では、コンパイルと呼ばれるソースコードをアプリに変換する処理が必要です。
GASではそういった変換処理は、一切不要。
作成から修正、実行までのすぐに実施して気軽に改善を行うことができます。
Googleサービスとの連携が簡単
GASは、Googleによって開発されている開発言語になります。
そのため他のGoogleサービス(スプレッドシートやGmail、Googleカレンダーなど)との連携が簡単に行えます。
Googleサービスの機能拡張を図りたい方やGoogleサービスを連携したい方にはおすすめの言語です。
無料で利用可能
GASは、最大の特徴として無料で利用できます。
他のGoogleのサービスであるスプレッドシートやGmail、ドキュメントなどと同様に、GASも無料で利用できます。
追加の費用をかけることなく、さまざまなGoogleサービスとの連携を通じて、自動化やカスタマイズされた機能を作成することが可能。
競馬データをスクレイピングしてデータを分析してみよう!
それでは、GASを利用して競馬データの収集アプリを開発する方法をご紹介していきます。
事前準備、プログラミング、動作確認の流れで説明していきます。
事前準備:競馬データサイトを確認して取得情報を把握
まずは、データ取得元の競馬データサイトを確認します。
今回は、JRAの競馬サイト(https://www.jra.go.jp/datafile/seiseki/)を見てみましょう。
このサイトには、年ごとのレース結果がまとめられているようですね。
年を選択するとその年のレース一覧が表示されレース結果のボタンを押すと個々のレース結果が表示されます。
2024年を選択してみます。
レース結果を選択してみましょう。
レース結果として順位や馬名、騎手名などの情報が一覧で表示されます。
今回は、このレース結果をスクレイピングで一括取得してみようと思います。
ここで重要になるのは、URLです。
URLは、ブラウザの上部に表示されている「http」から始まる文字列です。
インターネット上でのウェブサイトの住所ようなものです。
スクレイピングでは、URLと画面表示に使用されるHTMLファイルというファイルを参考にして何を取得できるか考えます。
今回のJRAのサイトを確認したところ年を選択した際のURLは、以下の構造となっていました。
この構造であれば年を選択してスクレイピングができそうですね。
続いて各コースの結果ページのURL。
こちらは、年に加えて上から順番にページ番号が振られているようです。
なのでページ内のコース数を取得し1件目から最後のコースまでコース数で取得できそうです。
続いて取得する情報を考えておきましょう。
今回取得する情報は、以下を想定しています。
取得したい情報
レース開催月
レース名
競馬場
コース種別
距離
天候
コース状態
順位
枠馬順
馬名
年齢
負担体重
騎手名
タイム
コーナー通過順位
推定上り
馬体重
体重増減
単勝人気
取得したい情報が先程のウェブページのどこで取得できるか確認します。
月、レース名、競馬場名、コース、距離は、年のページにて取得できそうです。
その他は、「レース結果」のページに情報が記載されていますね。
これでどこのページから情報を取得するかの確認は、完了です。
最後に取得する情報がどのように格納されているかを確認します。
確認方法は、スクレイピングしたいサイトにて「F12 」ボタンを押します。
あまり押したことがないボタンかと思いますが一般的にキーボードの右上の方に配置されています。
このボタンを押すと「開発者モード」と呼ばれるウェブサイトの内部構造が見れる状態になります。
利用するブラウザによって画面が異なりますが今回は、GoogleChromeにて実施した際の操作になります。
開発コンソール内の矢印ボタンを押します。
画面の取得したい箇所にカーソルをあわせてクリックします。
普段と異なりカーソルを合わせると周囲が塗りつぶしされたようになります。
開発コンソール上にクリックした箇所を表示するためのソースコードが表示されます。
ここからどのように取得するのかを判断します。
今回の場合、レース名情報は、classという項目が”race”という名前になっていることが確認できました。
このようにソースコード上のどのclass名で表示しているかを確認します。
今回は、事前に確認してプログラムに取り込んでいるので後で確認してみてください。
ここまでできたらスクレイピング対象の把握は完了です。
事前準備:作成するアプリの仕様を決定
JRAの競馬データからどのような情報を取得するか決まったところで作るアプリの仕様を確定しましょう。
今回は、以下の機能を持つアプリを作成していきます。
提供機能
- スクレイピング機能
スプレッドシートにて年を入力し入力された年の競馬データをJRA競馬サイトから取得してスプレッドシートに格納する。
格納する際は、年をシート名に設定したシートを新規作成してそこにデータを格納する。
すでに取得した年のシートが存在する場合は、データを上書きする。 - 検索機能
取得したデータをレース名や馬名などのいくつかの条件で検索できる。
複数条件で検索する場合は、AND条件(条件すべてを満たした結果を表示する)で検索する。
事前準備:スプレッドシート作成
スクレイピング結果を表示するためのスプレッドシートを作成します。
今回作成するスプレッドシートのシート名とそれぞれのシートデザインは、以下。
スプレッドシート名「レース成績データ集計スプレッドシート」
シート1「データ取り込み」
用途:スプレピングを実行するためのシート
シート構成:以下のように作成しておきましょう。
取込開始ボタンは、オブジェクトにて作成します。
取込年度 |
【初心者向け】Google Apps Script (GAS) ボタン作成方法|スプレッドシートで簡単操作【サンプルコードあり】
シート2「検索」
用途:スクレイピングした情報を検索するためのシート
シート構成:以下のように作成しておきましょう。
検索ボタンは、オブジェクトにて作成します。
項目は次の通り。
競馬場 | 天候 | コース | コース状態 | ||||
距離 | 馬名 | 騎手 | 対象シート |
競馬情報スクレイピング検索ツールの作成
ここからは、GASを利用してプログラムを作成していきます。
先程作成したGoogleスプレッドシートにて拡張機能-AppScriptをクリックします。
Google Apps Script(GAS)画面に切り替わります。
まずは、簡単にAppsScript画面についてご説明します。
1.プロジェクト名
クリックすると変更できます。
2.スクリプトファイル
プロジェクトに配置されているスクリプトファイルを確認できます。
各項目横の+ボタンを押すことでファイルを新規で作成できます。
3.ツールバー
スクリプト作成時に必要な操作ボタンがまとめられています。
保存やデバッグ、実行ログ表示、操作を1つ戻すなど
4.サイドバー
GASの設定や統計情報などを確認する画面に切り替えるためのボタンがまとめられています。
5.エディターエリア
スクリプトファイルの編集を行うエリア
手始めにプロジェクト名を変更してみましょう。
今回は、レース取得検索ツールなので「RaceResultDataCollection」に変更しましょう。
プロジェクト名をクリックして名前を変更します。
続いてプログラムを記述していきます。
今回作成するプログラムの完成形は以下になります。
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 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 |
// データ取り込み:シート作成処理 function createResultSheet(){ let ss = SpreadsheetApp.getActiveSpreadsheet(); let sheet = ss.getActiveSheet(); var year = sheet.getRange("B1").getValue(); let sheetYear = ss.getSheetByName(year); if (sheetYear == null) { // 対象年のシートがなかったら作成 let addSheet = ss.insertSheet(); addSheet.setName(year); // シートの位置を移動 ss.setActiveSheet(addSheet); ss.moveActiveSheet(2); sheetYear = addSheet; // 列名を設定 sheetYear.getRange(1,1).setValue("月"); sheetYear.getRange(1,2).setValue("レース名"); sheetYear.getRange(1,3).setValue("競馬場"); sheetYear.getRange(1,4).setValue("コース種別"); sheetYear.getRange(1,5).setValue("距離"); sheetYear.getRange(1,6).setValue("天候"); sheetYear.getRange(1,7).setValue("コース状態"); sheetYear.getRange(1,8).setValue("順位"); sheetYear.getRange(1,9).setValue("枠"); sheetYear.getRange(1,10).setValue("馬順"); sheetYear.getRange(1,11).setValue("馬名"); sheetYear.getRange(1,12).setValue("年齢"); sheetYear.getRange(1,13).setValue("負担体重"); sheetYear.getRange(1,14).setValue("騎手名"); sheetYear.getRange(1,15).setValue("タイム"); sheetYear.getRange(1,16).setValue("コーナー通化順位"); sheetYear.getRange(1,17).setValue("推定上り"); sheetYear.getRange(1,18).setValue("馬体重"); sheetYear.getRange(1,19).setValue("体重増減"); sheetYear.getRange(1,20).setValue("単勝人気"); } getTopPageResult(year,sheetYear); ss.setActiveSheet(sheet); } // データ取り込み:スクレイピング親 function getTopPageResult(year,sheetYear) { let response = UrlFetchApp.fetch(`https://www.jra.go.jp/datafile/seiseki/replay/${year}/jyusyo.html`); let cns = response.getContentText("Shift_JIS"); let output = []; // 重賞一覧を取得Classを取りやすくするためにまずは、Classの1階層上のtbodyをmatclistsに取得 let tbody = cns.match(/<tbody.*>[\s\S]*?<\/tbody>/g)[0]; let matchlist = tbody.match(/<tr.*>[\s\S]*?<\/tr>/g); for (let tr of matchlist) { // 月(Classがdate)月だけ取得するので不要な箇所を分割して月だけ取得 let month = tr.match(/<td class="date">([\s\S]*?)<\/td>/)[1]; month = month.match(/(\d{1,2})月\d{1,2}日<span.+<\/span>/)[1]; // レース名(Classがrace) let raceName = tr.match(/<td class="race">([\s\S]*?)<\/td>/)[1]; raceName = raceName.replace(/<a href=".*">/, ""); raceName = raceName.replace("</a>", ""); raceName = raceName.match(/<span class="grade_icon.*">.+<\/span>(.+)/)[1]; // 競馬場(Classがplace) let place = tr.match(/<td class="place">(.+)<\/td>/)[1]; // コース、距離(Classがcourse) let courseDistance = tr.match(/<td class="course">([\s\S]*?)<\/td>/)[1]; courseDistance = courseDistance.match(/<span class="type">(.+)<\/span>(.+)<span class="unit">.+/); let course = courseDistance[1]; let distance = courseDistance[2]; distance = distance.replace(",", ""); // レース結果のURL(Classがresult) let resultUrl = tr.match(/<td class="result">([\s\S]*?)<\/td>/)[1]; if (resultUrl == "") { break; } // 取得したレース結果のURLの中身をスクレイピング子を呼び出して取得 resultUrl = resultUrl.match(/<a href="(.+)" class/)[1]; resultUrl = `https://www.jra.go.jp${resultUrl}`; let tmp = getRaceResult(resultUrl, month, raceName, place, course, distance); // 取得した結果をputputという変数に格納 output = output.concat(tmp); } // 取得したデータを出力 sheetYear.getRange(2, 1, output.length, output[0].length).setValues(output); } // データ取り込み:スクレイピング子 function getRaceResult(resultUrl, month, raceName, place, course, distance) { let response = UrlFetchApp.fetch(resultUrl); let cns = response.getContentText("Shift_JIS"); // 天気を取得(Classがweather) let matchWeather = cns.match(/<li class="weather">[\s\S]*?<span class="inner">[\s\S]*?<span class="cap">天候<\/span>[\s\S]*?<span class="txt">([\s\S]*?)<\/span>/); let weather = matchWeather[1]; // 馬場状態を取得(Classがturfかdurt)複数のClassを取得したい場合は、"|"を利用して記述する let matchCourseStatus = cns.match(/<li class="(turf|durt)">[\s\S]*?<span class="inner">[\s\S]*?<span class="cap">(芝|ダート)<\/span>[\s\S]*?<span class="txt">([\s\S]*?)<\/span>/); let courseStatus = matchCourseStatus[3]; // 着順一覧を取得Classを取りやすくするためにまずは、Classの1階層上のtbodyをmatclistsに取得 let tbody = cns.match(/<tbody.*>[\s\S]*?<\/tbody>/g)[0]; let matchlist = tbody.match(/<tr>[\s\S]*?<\/tr>/g); let output = []; for (let tr of matchlist) { // 着順(Classがplace) let rank = tr.match(/<td class="place">(.+)<\/td>/)[1]; // 枠(Classがwaku)画像が設定されているので文字ではなく画像(pngファイル)ファイルの名前がそのまま枠順なのでをファイル名を取得 let waku = tr.match(/<td class="waku">([\s\S]*?)<\/td>/)[1]; waku = waku.match(/<img src="\/JRADB\/img\/waku\/(.?).png"/)[1]; // 馬番(Classがnum) let num = tr.match(/<td class="num">(.+)<\/td>/)[1]; // 馬名(Classがhorse)構造が複雑なのでバラして名前だけ取得 let horse = tr.match(/<td class="horse">([\s\S]*?)<\/td>/)[1]; horse = horse.replace(/<span class="horse_icon">.*<\/span>/, ""); horse = horse.replace(/<div class="icon blinker">.*<\/div>/, ""); horse = horse.replace("<div class=\"horse\">", ""); horse = horse.replace("</div>", ""); horse = horse.replace(/\s/g, ""); // 年齢(Classがage) let age = tr.match(/<td class="age">(.+)<\/td>/)[1]; // 重量(Classがweight) let weight = tr.match(/<td class="weight">(.+)<\/td>/)[1]; // 騎手名(Classがjockey) let jockey = tr.match(/<td class="jockey">(.+)<\/td>/)[1]; // タイム(Classがtime) let time = tr.match(/<td class="time">([\s\S]*?)<\/td>/)[1]; // コーナー通過順位(Classがcorner) let matchCorner = tr.match(/<td class="corner">([\s\S]*?)<\/td>/)[1]; let matchLis = matchCorner.match(/<li.+>.+<\/li>/g); let corners = []; // 1,000直はコーナー通過順位がないので別で処理します。 if (matchLis != null) { for (let li of matchLis) { let order = li.match(/<li.+>(.+)<\/li>/)[1]; order = order.replace(" ", ""); if (order == "") { continue; } corners.push(order); } } let corner = corners.join(); // 推定上がり(Classがf_time) let matchLast3fTime = tr.match(/<td class="f_time">(.+)<\/td>/); let last3fTime = ""; if (matchLast3fTime != null) { last3fTime = matchLast3fTime[1]; } // 馬体重(Classがh_weight)他の文字が含まれているので重量だけ取得するように加工 let horseWeight = tr.match(/<td class="h_weight">([\s\S]*?)<\/td>/)[1]; horseWeight = horseWeight.replace(/\s/g, ""); let previousRatio = ""; if (horseWeight != "") { let matchPreviousRatio = horseWeight.match(/<span>\((.+)\)<\/span>/); if (matchPreviousRatio != null) { previousRatio = matchPreviousRatio[1]; } horseWeight = horseWeight.replace(/<span>.*<\/span>/, ""); } // 単勝人気(Classがpop) let matchPop = tr.match(/<td class="pop">(.+)<\/td>/); let pop = ""; if (matchPop != null) { pop = matchPop[1]; } // スクレイピング結果をoutputという変数にpusuを利用してまとめて格納 output.push([ month, raceName, place, course, distance, weather, courseStatus, rank, waku, num, horse, age, weight, jockey, time, corner, last3fTime, horseWeight, previousRatio, pop, ]); } // 結果を親に返す return output; } |
この内容をエディタエリアにコピペして保存すればすぐに動かす事ができます。
時間がない方や仕組みはいいからスプレッドシートのデータベース化だけできればいいという方は、コピペして利用してください。
ここから各処理事に処理内容を説明していきます。
シート追加処理
まずは、スクレイピングをするための前準備箇所を処理を作成します。
今回、JRAのサイト構造に合わせるため、年単位でのデータ取得し対応するシートを追加する処理とします。
データ取得処理が開始されたら最初にシートを作成します。
エディタエリアに以下のコードを記入します。
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 |
// データ取り込み:シート作成処理 function createResultSheet(){ let ss = SpreadsheetApp.getActiveSpreadsheet(); let sheet = ss.getActiveSheet(); var year = sheet.getRange("B1").getValue(); let sheetYear = ss.getSheetByName(year); if (sheetYear == null) { // 対象年のシートがなかったら作成 let addSheet = ss.insertSheet(); addSheet.setName(year); // シートの位置を移動 ss.setActiveSheet(addSheet); ss.moveActiveSheet(2); sheetYear = addSheet; // 列名を設定 sheetYear.getRange(1,1).setValue("月"); sheetYear.getRange(1,2).setValue("レース名"); sheetYear.getRange(1,3).setValue("競馬場"); sheetYear.getRange(1,4).setValue("コース種別"); sheetYear.getRange(1,5).setValue("距離"); sheetYear.getRange(1,6).setValue("天候"); sheetYear.getRange(1,7).setValue("コース状態"); sheetYear.getRange(1,8).setValue("順位"); sheetYear.getRange(1,9).setValue("枠"); sheetYear.getRange(1,10).setValue("馬順"); sheetYear.getRange(1,11).setValue("馬名"); sheetYear.getRange(1,12).setValue("年齢"); sheetYear.getRange(1,13).setValue("負担体重"); sheetYear.getRange(1,14).setValue("騎手名"); sheetYear.getRange(1,15).setValue("タイム"); sheetYear.getRange(1,16).setValue("コーナー通化順位"); sheetYear.getRange(1,17).setValue("推定上り"); sheetYear.getRange(1,18).setValue("馬体重"); sheetYear.getRange(1,19).setValue("体重増減"); sheetYear.getRange(1,20).setValue("単勝人気"); } getTopPageResult(year,sheetYear); ss.setActiveSheet(sheet); } |
ここでは、シートの作成とテンプレートの列名を設定しています。
シートの追加は、insertSheet関数にて追加できます。
追加したシート内にて列名の設定は、getRange.setValue関数を利用して設定していきます。
データ取得処理
シートの追加処理ができたらメイン処理であるJRA競馬サイトからデータを収集する処理を記載していきます。
エディタエリアに以下のコードを記入します。
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 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 |
// データ取り込み:スクレイピング親 function getTopPageResult(year,sheetYear) { let response = UrlFetchApp.fetch(`https://www.jra.go.jp/datafile/seiseki/replay/${year}/jyusyo.html`); let cns = response.getContentText("Shift_JIS"); let output = []; // 重賞一覧を取得Classを取りやすくするためにまずは、Classの1階層上のtbodyをmatclistsに取得 let tbody = cns.match(/<tbody.*>[\s\S]*?<\/tbody>/g)[0]; let matchlist = tbody.match(/<tr.*>[\s\S]*?<\/tr>/g); for (let tr of matchlist) { // 月(Classがdate)月だけ取得するので不要な箇所を分割して月だけ取得 let month = tr.match(/<td class="date">([\s\S]*?)<\/td>/)[1]; month = month.match(/(\d{1,2})月\d{1,2}日<span.+<\/span>/)[1]; // レース名(Classがrace) let raceName = tr.match(/<td class="race">([\s\S]*?)<\/td>/)[1]; raceName = raceName.replace(/<a href=".*">/, ""); raceName = raceName.replace("</a>", ""); raceName = raceName.match(/<span class="grade_icon.*">.+<\/span>(.+)/)[1]; // 競馬場(Classがplace) let place = tr.match(/<td class="place">(.+)<\/td>/)[1]; // コース、距離(Classがcourse) let courseDistance = tr.match(/<td class="course">([\s\S]*?)<\/td>/)[1]; courseDistance = courseDistance.match(/<span class="type">(.+)<\/span>(.+)<span class="unit">.+/); let course = courseDistance[1]; let distance = courseDistance[2]; distance = distance.replace(",", ""); // レース結果のURL(Classがresult) let resultUrl = tr.match(/<td class="result">([\s\S]*?)<\/td>/)[1]; if (resultUrl == "") { break; } // 取得したレース結果のURLの中身をスクレイピング子を呼び出して取得 resultUrl = resultUrl.match(/<a href="(.+)" class/)[1]; resultUrl = `https://www.jra.go.jp${resultUrl}`; let tmp = getRaceResult(resultUrl, month, raceName, place, course, distance); // 取得した結果をputputという変数に格納 output = output.concat(tmp); } // 取得したデータを出力 sheetYear.getRange(2, 1, output.length, output[0].length).setValues(output); } // データ取り込み:スクレイピング子 function getRaceResult(resultUrl, month, raceName, place, course, distance) { let response = UrlFetchApp.fetch(resultUrl); let cns = response.getContentText("Shift_JIS"); // 天気を取得(Classがweather) let matchWeather = cns.match(/<li class="weather">[\s\S]*?<span class="inner">[\s\S]*?<span class="cap">天候<\/span>[\s\S]*?<span class="txt">([\s\S]*?)<\/span>/); let weather = matchWeather[1]; // 馬場状態を取得(Classがturfかdurt)複数のClassを取得したい場合は、"|"を利用して記述する let matchCourseStatus = cns.match(/<li class="(turf|durt)">[\s\S]*?<span class="inner">[\s\S]*?<span class="cap">(芝|ダート)<\/span>[\s\S]*?<span class="txt">([\s\S]*?)<\/span>/); let courseStatus = matchCourseStatus[3]; // 着順一覧を取得Classを取りやすくするためにまずは、Classの1階層上のtbodyをmatclistsに取得 let tbody = cns.match(/<tbody.*>[\s\S]*?<\/tbody>/g)[0]; let matchlist = tbody.match(/<tr>[\s\S]*?<\/tr>/g); let output = []; for (let tr of matchlist) { // 着順(Classがplace) let rank = tr.match(/<td class="place">(.+)<\/td>/)[1]; // 枠(Classがwaku)画像が設定されているので文字ではなく画像(pngファイル)ファイルの名前がそのまま枠順なのでをファイル名を取得 let waku = tr.match(/<td class="waku">([\s\S]*?)<\/td>/)[1]; waku = waku.match(/<img src="\/JRADB\/img\/waku\/(.?).png"/)[1]; // 馬番(Classがnum) let num = tr.match(/<td class="num">(.+)<\/td>/)[1]; // 馬名(Classがhorse)構造が複雑なのでバラして名前だけ取得 let horse = tr.match(/<td class="horse">([\s\S]*?)<\/td>/)[1]; horse = horse.replace(/<span class="horse_icon">.*<\/span>/, ""); horse = horse.replace(/<div class="icon blinker">.*<\/div>/, ""); horse = horse.replace("<div class=\"horse\">", ""); horse = horse.replace("</div>", ""); horse = horse.replace(/\s/g, ""); // 年齢(Classがage) let age = tr.match(/<td class="age">(.+)<\/td>/)[1]; // 重量(Classがweight) let weight = tr.match(/<td class="weight">(.+)<\/td>/)[1]; // 騎手名(Classがjockey) let jockey = tr.match(/<td class="jockey">(.+)<\/td>/)[1]; // タイム(Classがtime) let time = tr.match(/<td class="time">([\s\S]*?)<\/td>/)[1]; // コーナー通過順位(Classがcorner) let matchCorner = tr.match(/<td class="corner">([\s\S]*?)<\/td>/)[1]; let matchLis = matchCorner.match(/<li.+>.+<\/li>/g); let corners = []; // 1,000直はコーナー通過順位がないので別で処理します。 if (matchLis != null) { for (let li of matchLis) { let order = li.match(/<li.+>(.+)<\/li>/)[1]; order = order.replace(" ", ""); if (order == "") { continue; } corners.push(order); } } let corner = corners.join(); // 推定上がり(Classがf_time) let matchLast3fTime = tr.match(/<td class="f_time">(.+)<\/td>/); let last3fTime = ""; if (matchLast3fTime != null) { last3fTime = matchLast3fTime[1]; } // 馬体重(Classがh_weight)他の文字が含まれているので重量だけ取得するように加工 let horseWeight = tr.match(/<td class="h_weight">([\s\S]*?)<\/td>/)[1]; horseWeight = horseWeight.replace(/\s/g, ""); let previousRatio = ""; if (horseWeight != "") { let matchPreviousRatio = horseWeight.match(/<span>\((.+)\)<\/span>/); if (matchPreviousRatio != null) { previousRatio = matchPreviousRatio[1]; } horseWeight = horseWeight.replace(/<span>.*<\/span>/, ""); } // 単勝人気(Classがpop) let matchPop = tr.match(/<td class="pop">(.+)<\/td>/); let pop = ""; if (matchPop != null) { pop = matchPop[1]; } // スクレイピング結果をoutputという変数にpusuを利用してまとめて格納 output.push([ month, raceName, place, course, distance, weather, courseStatus, rank, waku, num, horse, age, weight, jockey, time, corner, last3fTime, horseWeight, previousRatio, pop, ]); } // 結果を親に返す return output; } |
データ取得処理は、確認した結果、取得先のサイト構造は二つのウェブページにまたがっていることが判明。
親、子という形でfunctionを分けています。
getJyushoResult関数では、親のサイトである年を選択した画面にて取得できる情報「月、レース名、競馬場名、コース、距離」を取得します。
残りの情報は、getRaceResult関数にて取得する構造です。
スクレイピングを行う際は、取得先のURLを以下のように設定します。
1 2 3 |
let response = UrlFetchApp.fetch(`https://www.jra.go.jp/datafile/seiseki/replay/${year}/jyusyo.html`); let content = response.getContentText("Shift_JIS"); |
次にサイト上で取得したい情報を以下のように記述します。
こちらは、競馬場名(Classが”place”)を取得する際のコードになります。
1 2 |
let place = tr.match(/<td class="place">(.+)<\\/td>/)[1]; |
match関数は、データ内のどの部分を取得するかを設定できます。
カッコ内に正規表現というデータ抽出ルールに従って取得したい情報を記述します。
今回は、<td class=”place”>と<\/td>に囲まれている文字列を取得したかったので上記のような正規表現を記載しています。
正規表現は、膨大な知識が必要なので今回は、サンプルコードを参考に取得する際は、こんな指定をすればよいということだけ覚えておけば大丈夫です。
ここまでコードがかければデータ取得処理は完了です。
ボタンにスクリプト割当
プログラムが完成したらスプレッドシートのオブジェクトにスクリプトを設定していきましょう。
スプレッドシートのデータ取り込みシートにて作成した「取込開始」ボタンを右クリックします。
表示されるメニューで「スクリプトの割り当て」をクリックします。
割り当てるスクリプト名として「createResultSheet」を設定します。
これで割り当て完了です。
動作確認
実際に作成したツールが正常に動作するか確認してみましょう。
データ取り込みシートにて取込年度を入力して取り込み開始をクリックします。
今回は、2024年を指定してみます。
初回の動作実行時は、権限の確認が表示されます。
指示に従って許可をしておきましょう。
取得結果が「2024」というシートが作成されて競馬結果が格納されていれば成功です。
結構時間がかかるのでしばらく待ちましょう。
応用:検索機能を搭載
今回は、スクレイピングしたデータを検索する機能も応用編として作っていきます。
以下のコードを作成中のソースの最下行に追記し保存してください。
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 |
// 応用編:検索機能 function search(){ let ss = SpreadsheetApp.getActiveSpreadsheet(); let sheet = ss.getActiveSheet(); // 入力値を取得 // 競馬場 let place = sheet.getRange('B1').getValue(); // コース let course = sheet.getRange('F2').getValue(); // 距離 let distance = sheet.getRange('B2').getValue(); // 天候 let weather = sheet.getRange('D1').getValue(); // コース状態 let condition = sheet.getRange('H1').getValue(); // 馬名 let horse = sheet.getRange('D2').getValue(); // 騎手 let jockey = sheet.getRange('F2').getValue(); // 取得年数 let year = sheet.getRange('H2').getValue(); // デフォルㇳ検索値を設定 let whereQuery = '=QUERY('+year+'!A:S,"where A matches \'.*\''; // 検索値に検索内容を追記 if(place != ""){ whereQuery += ' and C =\'' + place + '\''; } if(course != ""){ whereQuery += ' and D =\'' + course + '\''; } if(distance != ""){ whereQuery += ' and E =' + distance; } if(weather != ""){ whereQuery += ' and F =\'' + weather + '\''; } if(condition != ""){ whereQuery += ' and G =\'' + condition + '\''; } if(horse != ""){ whereQuery += ' and K =\'' + horse + '\''; } if(jockey != ""){ whereQuery += ' and N =\'' + jockey + '\''; } whereQuery += '")'; // 作成したQuery関数をA5(検索結果表の先頭)に貼り付け sheet.getRange("A5").setValue(whereQuery); } |
今回の検索処理では、スプレッドシートにある検索用関数の1つであるQUERY関数を利用しています。
QUERY関数は以下のようにシート名と取得条件を設定するだけでシート内からデータを抽出して結果を別のシートや表に表示できます。
クエリの部分は、WHERE検索する列名 = ヒットさせたいデータ値を設定します。
1 2 |
=QUERY(データ範囲,クエリ) |
検索項目をそれぞれのセルから取得してそれをQUERY関数に入力されている検索条件を追加していき検索結果シートに表示するようにしています。
記入ができたら先程と同様の手順で「検索」シートの検索ボタンにスクリプト割当で「search」を設定します。
設定が完了したら検索条件を設定して検索ボタンをクリックしてみましょう。
検索結果が表示されれば成功です。
まとめ
今回は、競馬データをスクレイピングしてみようということでJRAの競馬データから必要な情報をGASを用いて取得する方法をご紹介しました。
本記事の内容をまとめると以下。
スクレイピングは、人力だと非常に手間な作業を自動的に行ってくれる便利な処理です。
今回取得したJRAの競馬データ以外にも各所の競馬データを取得して分析してみればより信頼度の高い結果を得られるかもしれません。
ぜひ、興味が湧いた方は今回の内容を参考に挑戦してみてください。
ただし必ず自己責任で行ってください。
スクレイピングによっては、対象サイトの利用規約に抵触したり、サーバーに過剰な負荷をかけて迷惑をかけてしまうリスクがあります。