競馬予想AIで買い目を弾き出しておきながら、当日の買い目、金額はオッズ、人気を見ながら手作業で決めていく…。それじゃぁ、片手落ちですよね。そこでスクレイピング。でも、rvestパッケージではnetkeibaのオッズや人気は、スクレイピングできないんです。ここでは、RSeleniumパッケージを使ってオッズや人気をスクレイピングする方法を紹介しています。
netkeibaからオッズ、人気データをスクレイピングしたい
過去データをもとに勝ち馬を予想するような競馬予想AIを作っても、それだけでは競馬には勝てない。最終的に買う馬券やその金額はレース当日のオッズや人気データも考慮に入れながら決めたいですよね。各レース、何をいくら買うのかを手作業で決めるのは、それはそれで楽しくもありますが、せっかく競馬予想AIを作っているのだからできれば自動で行いたい。というわけで、オッズ、人気データもスクレイピングしたいと思います。
実は、以前、こちらのページでnetkeibaの出馬表をスクレイピングを行なっています。
スクレイピング対象のnetkeibaのページにはオッズや人気のデータは記載されていたのですがxml2::read_html()関数(xml2パッケージはrvestを読み込むと自動で読み込まれます)では、オッズと人気のデータはスクレイピングできませんでした。
netkeibaのページは、「オッズ」と「人気」の列は直接htmlファイルに書き込まれているわけではなく、JavaScriptで動的にデータが生成されていることが原因です(Pythonのrequestsライブラリなどでhtmlを入手しても、やはり同様にオッズや人気データはスクレイピングできません)。オッズと人気データをスクレイピングするためには、seleniumなどブラウザベースのモジュールを使う必要があります。
netkeibaのスクレイピングにRSeleniumを使う
Seleniumとは
Selenium(セレニウム)はWebアプリケーションのブラウザ操作を自動化するソフトです。JavaScriptを用いて動的に生成されるWebページの場合、htmlファイルには欲しい情報(今回の「オッズ」や「人気」など)は記載されていない場合があります。read_html()関数では欲しい情報を入手できないような場合でも、ブラウザ操作用のツールであるSeleniumを用いれば、動的に生成されたデータでも入手することが可能になります。
RSeleniumを使うために
本記事のRコードではRSeleniumを用いてスクレイピングを行っています。RSeleniumを使うためには、Rのコードを記述する以外に、アプリ、ファイルのインストール、ダウンロードが必要です。本記事のRコードを実行するために必要な設定をこちらの記事にまとめています。
スクレイピングを始める前に
スクレイピングは、WebページのHTML/CSS構造を利用して目的のデータを抽出します。Seleniumを使う場合でも、データの抽出の大部分はrvestを使う場合と同じで、
- rvest(xml2)パッケージの使い方
- HTML/CSSの構造
の知識が必要です。もし曖昧なら、まずこちらから。これだけ知っていればスクレイピングは始められます。
ソースコード
R用のSeleniumパッケージであるRSeleniumを用いてnetkeibaの出馬表をスクレイピングできるようにしたソースコードを以下に示します。
使用するライブラリ
library(rvest) library(stringr) library(RSelenium) library(tidyverse)
開催日のrace_idを取得する関数
# レースIDを調べる get_race_ids <- function(race_date){ # netkeiba開催日のページ url <- paste0("https://race.netkeiba.com/top/race_list_sub.html?kaisai_date=", race_date) race_ids <- read_html(url, encoding = "UTF-8")%>% html_nodes("a") %>% html_attr("href") %>% str_subset(".*race_id=") %>% str_replace(".*race_id=", "") %>% str_replace("&rf=.*", "") %>% unique() cat(" Sleep") Sys.sleep(1) cat(" Done") race_ids }
race_idのページのhtmlデータを取得する関数
引数:
race_id:レースID
rsChr : RSeleniumでChromeを起動した時の戻り値(リモートドライバー)
戻り値:race_idのページのhtmlデータ
rsChrにChromeのリモートドライバーが設定されているときは、seleniumを用いたスクレイピングが行われ、rsChrがNAのとき(デフォルト)はrvestを用いたスクレイピングが行われます。seleniumを用いたスクレイピングは速くコマンドを出しすぎるとエラーが生じてしまうため、各ステップ2秒間の待ち時間を設定しています(2秒×3回 = 計6秒の待ち時間)。rvestを用いたスクレイピングは1秒です。状況に応じて使い分けてください。
# race_idのページを読み込む get_scheduled_race_html <- function(race_id, rsChr = NA){ cat(race_id) cat("n") url <- paste0("https://race.netkeiba.com/race/shutuba.html?race_id=", race_id, "&rf=race_list") if(isS4(rsChr)){ rsChr$navigate(url) Sys.sleep(2) ret <- rsChr$findElements(using = "css selector", "div.RaceTableArea") Sys.sleep(2) if(length(ret) !=0){ pgsource <- rsChr$getPageSource() Sys.sleep(2) html <- read_html(pgsource[[1]]) } else { html <- NA } }else{ html <- read_html(url, encoding = "EUC-JP") Sys.sleep(1) } html }
htmlデータから各馬共通の情報を抽出する関数
引数:
html:get_scheduled_race_html()関数で取得したhtmlデータ
race_id:スクレイピング対象のrace_id
戻り値:レース条件を記載したdata.frame
# レースの条件等、各馬にとって共通のデータを読み込む get_scheduled_race_cond <- function(html, race_id){ #レース race_No <- html %>% html_element(".RaceNum") %>% html_text() %>% str_remove("R") %>% as.integer() #レース名 race_name <- html %>% html_element(".RaceName") %>% html_text() %>% str_trim() race_data01 <- html %>% html_element(".RaceData01") %>% html_text() %>% str_remove_all("n") race_data02 <- html %>% html_element(".RaceData02") %>% html_text() %>% str_split("n") %>% unlist() # 開催 kaisai <- paste0(race_data02[2:4], collapse = "") #競馬場 place <- race_data02[3] #クラス class <- paste0(race_data02[5:6], collapse = "") #馬場 turf <- race_data01 %>% str_split("/") %>% .[[1]] %>% .[2] %>% str_trim() %>% str_sub(1,1) # 距離 distance <- race_data01 %>% str_split("/") %>% .[[1]] %>% .[2] %>% str_extract("\d+") %>% as.integer() #左右 rotation <- race_data01 %>% str_split("/") %>% .[[1]] %>% .[2] %>% str_replace("^.*\((.)\)$", "\1") df.cond <- tibble("race_id" = race_id, "開催" = kaisai, "競馬場" = place, "レース" = race_No, "レース名" = race_name, "クラス" = class, "馬場" = turf, "距離" = distance, "左右" = rotation ) df.cond }
htmlデータから各馬固有の情報を抽出する関数
引数:
html:get_scheduled_race_html()関数で取得したhtmlデータ
race_id:スクレイピング対象のrace_id
戻り値:枠、馬番、馬名、オッズ、人気順、etcを記載したdata.frame
# 各馬のデータを読み込む get_scheduled_race_horse <- function(html, race_id){ # 出馬表の読み込み df <- html %>% html_elements("div.RaceTableArea") %>% html_table() %>% .[[1]] # 列名の修正 names(df) <- names(df) %>% str_replace_all("n", "") %>% str_replace("(^.*オッズ.*$)|(^.*更新.*$)", "オッズ") # 1行目は不要なので削除 df <- df[-1, ] # 必要な列のみ抽出 df <- df %>% select(`枠`, `馬番`, `馬名`, `性齢`, `斤量`, `騎手`, `厩舎`, `オッズ`, `人気`) # 型変換 # htmlの中身によって、 データが入っている場合と入っていない場合があるので場合分け # # 枠、馬番 : データの入手日時、時間によって、入っていたり入っていなかったりする # オッズ、人気 : データの入手方法(seleniumを使うかどうか)で # 入っていたり入っていなかったりする if(str_detect(df$枠[1], "\d")){ df[c("枠", "馬番")] <- lapply(df[c("枠", "馬番")], as.integer) %>% as.data.frame() } df["斤量"] <- lapply(df["斤量"], as.double) %>% as.data.frame() if(str_detect(df$オッズ[1], "\d")){ df["オッズ"] <- lapply(df["オッズ"], as.double) %>% as.data.frame() } if(str_detect(df$人気[1], "\d")){ df["人気"] <- lapply(df["人気"], as.integer) %>% as.data.frame() } # race_idをdata.frameに追加 df <- df %>% mutate(race_id = race_id, .before = everything()) df }
スクレイピング本体
引数:
race_ids:スクレイピングしたいレースのrace_id
rsChr : RSeleniumでChromeを起動した時の戻り値(リモートドライバー)
戻り値:race条件、枠、馬番、馬名、オッズ、人気順、etcを記載したdata.frame
# スクレイピング本体 get_scheduled_race_table <- function(race_ids, rsChr = NA){ df.cond <- NULL df.horse <- NULL for(race_id in race_ids){ # htmlデータの読み込み html <- get_scheduled_race_html(race_id, rsChr) # race条件の読み込み df.cond.tmp <- get_scheduled_race_cond(html, race_id) # 出馬表を取得する df.horse.tmp <- get_scheduled_race_horse(html, race_id) # 各data.frameの合体 if(is.null(df.cond)){ df.cond <- df.cond.tmp df.horse <- df.horse.tmp }else{ df.cond <- rbind(df.cond, df.cond.tmp) df.horse <- rbind(df.horse, df.horse.tmp) } } # race条件と各馬データの合体 df.race.table <- left_join(df.cond, df.horse) df.race.table }
RSeleniumの起動
引数:なし
戻り値:RSeleniumでChromeを起動した時の戻り値(リモートドライバー)
# RSelenium の起動 start_selenium <- function(){ # seleniumの起動 system("java -jar selenium-server-standalone-3.141.59.jar &") # Google Chrome 起動 rsChr <- RSelenium::remoteDriver(port = 4444L, browserName = "chrome") # Webブラウザを開く rsChr$open() rsChr }
RSeleniumの終了
引数:
rsChr : RSeleniumでChromeを起動した時の戻り値(リモートドライバー)
戻り値:なし
# RSelenium 終了処理 stop_selenium <- function(rsChr){ # ブラウザを閉じる rsChr$close() # 何かバグとかで終了して、Rのみ再起動してしまって、 # selenium を複数立ち上げてしまうことがある # それを防止するため # selenium を全て終了する # まず、seleniumのpidを確認 system("ps -A | grep 'java'") cat("n") pid <- system("ps -A | grep 'java'", intern = TRUE) # seleniumのpid pid_selenium <- pid %>% str_subset("selenium") %>% str_extract("^\d*") # # 6721 ?? 0:01.37 /usr/bin/java -jar selenium-server-standalone-3.141.59.jar # 6724 ?? 0:00.01 sh -c ps -A | grep 'java' # 6726 ?? 0:00.00 grep java # # pidをkillする(上の場合6721) # system("kill 6721") # seleniumをkillする # selenium を全て終了する for(p in pid_selenium){ paste0("kill ", p) system(paste0("kill ", p)) } #killできていることの確認 cat("n") system("ps -A | grep 'java'") }
使用例
以下の例では、まず、2022年3月27日に開催される全レースに関してオッズなしの出馬表をスクレイピングした後、お目当てのレースのrace_idを抽出して(今回は各競馬場の10〜12R)、オッズありの出馬表データをスクレイピングしています。
#--------------------------------------- # race_dateで指定された開催日のrace_idの抽出 #--------------------------------------- # 開催日 race_date <- "20220327" # race_idの読み込み race_ids <- get_race_ids(race_date) # race_idsに対応する出馬表(オッズなし) df.race.table <- get_scheduled_race_table(race_ids, NA) # お目当てのレースのrace_idを抽出 race_ids2 <- df.race.table %>% filter(レース %in% (10:12)) %>% select(race_id) %>% unique() %>% unlist() #--------------------------------------- # オッズありの出馬表の抽出 #--------------------------------------- # seleniumの起動 rsChr <- start_selenium() # オッズありの出馬表のスクレイピング df.race.table <- get_scheduled_race_table(race_ids2, rsChr) # seleniumの停止 stop_selenium(rsChr) # seleniumの停止確認 system("ps -A | grep 'java'")
まとめ
RSeleniumパッケージを使うことで、JavaScriptで動的にデータが作られているサイトもスクレイピングすることができます。これでnetkeibaの出馬表からオッズ、人気データも取得することができるようになります。