Rubyで競馬情報をスクレイピングしてみた

## 馬券を当てたかった
毎週500円ほど重賞レースに賭けているのですが、負けが続いたので予想アプリでも作ってみるかとなった。

## まずは情報が必要だ
ということでJRAのサイトに下記のようなレース結果が載っているので、このデータをスクレイピングしてみました。
https://www.jra.go.jp/datafile/seiseki/replay/2021/001.html

2020年から現在までの重賞レース結果をcsvに出力します。
Rubyのバージョンは2.7.1

## ソース
main.rb

```ruby
require 'bundler/setup'
require 'nokogiri'
require 'open-uri'
require 'csv'
require 'pry'
require './create_url.rb'

INDEX_YEAR = [2020, 2021] # データを取得したい年
CSV_HEADER = ['日時', 'レース名', 'グレード', '着順', '馬名', '年齢', '騎手', '調教師']

def setup_doc(url)
  doc = Nokogiri::HTML.parse(URI.open(url, "r:CP932").read)
  doc.search('br').each { |n| n.replace("\n") }
  doc
end

def scrape_data(data)
  horse_data = []
  # 着順
  horse_data << data.children[1].text
  # 馬名
  horse_data << data.children[7].text
  # 年齢
  horse_data << data.children[9].text
  # 騎手
  horse_data << data.children[13].text
  # 調教師
  horse_data << data.children[25].text

  horse_data.map(&:strip)
end

def race_info(doc)
  race_info = []
  # 日時
  race_info << doc.xpath("//div[@class='cell date']").text.split('（').first
  # レース名
  race_info << doc.xpath("//span[@class='race_name']").text
  # グレード
  race_info << doc.xpath("//span[@class='grade_icon lg']").first.children.attribute('alt').value

  race_info.map(&:strip)
end

if __FILE__ == $0
  index_url_list = index_urls(INDEX_YEAR)
  urls = index_url_list.map do |index_url|
    index_doc = setup_doc(index_url)
    create_result_urls(index_doc)
  end.flatten

  CSV.open('../result.csv', 'w') do |csv|
    csv << CSV_HEADER
    urls.each do |url|
      begin
        doc = setup_doc(url)
        race_info_columns = race_info(doc)
        table_data = doc.xpath("id('race_result')/div[@class='race_result_unit']/table[@class='basic narrow-xy striped']/tbody[1]/tr")
        table_data.each do |data|
          csv << race_info_columns + scrape_data(data)
        end
      rescue => e
        p e
      end
    end
  end
end
```

create_url.rb

```ruby
require 'active_support/all'

# 結果ページURLの配列を生成
def create_result_urls(doc)
  table_data = doc.xpath("id('contentsBody')/div[@class='scr-md']/table[@class='basic narrow-xy striped mt20']/tbody[@class='td_left']/tr")
  urls = table_data.map do |data|
    result_path = data&.children[15]&.children[0]&.values&.first
    "https://www.jra.go.jp/#{result_path}" if result_path.present?
  end

  urls.compact
end

# 年から重賞ページURLの配列を生成
def index_urls(years = [])
  urls = years&.map do |year|
    "https://www.jra.go.jp/datafile/seiseki/replay/#{year}/jyusyo.html"
  end

  urls
end
```
Gemfile
pry-byebugはデバッグ用です。
activesupportはcreate_url.rbで`present?`を使いたかったので追加しました。

```ruby
gem 'nokogiri'
gem 'pry-byebug', '~> 3.9'
gem 'activesupport'
```

### main.rb解説
#### 定数
```ruby
INDEX_YEAR = [2020, 2021] # データを取得したい年
CSV_HEADER = ['日時', 'レース名', 'グレード', '着順', '馬名', '年齢', '騎手', '調教師']
```
`INDEX_YEAR`の配列に取得したいデータの年を入れます。
例えば`2019`を追加すれば、3年分のデータが取得できるようになります。

`CSV_HEADER`はまんま、ヘッダーです。とりあえずこの項目を取得することにしました。
馬体重やタイムなど欲しい情報が増えてきたら追加しようかなと思います。

#### パース
```ruby
def setup_doc(url)
  doc = Nokogiri::HTML.parse(URI.open(url, "r:CP932").read)
  doc.search('br').each { |n| n.replace("\n") }
  doc
end
```
HTMLをパースしている箇所です。
`open(url, "r:CP932")`とするとwarningが出るため、`URI.open()`としています。
またそのままだと日本語が文字化けしてしまうのですが、`r:CP932`をセットすることで解決しました。

#### データ取得

```ruby
def scrape_data(data)
  horse_data = []
  # 着順
  horse_data << data.children[1].text
  # 馬名
  horse_data << data.children[7].text
  # 年齢
  horse_data << data.children[9].text
  # 騎手
  horse_data << data.children[13].text
  # 調教師
  horse_data << data.children[25].text

  horse_data.map(&:strip)
end

def race_info(doc)
  race_info = []
  # 日時
  race_info << doc.xpath("//div[@class='cell date']").text.split('（').first
  # レース名
  race_info << doc.xpath("//span[@class='race_name']").text
  # グレード
  race_info << doc.xpath("//span[@class='grade_icon lg']").first.children.attribute('alt').value

  race_info.map(&:strip)
end
```
`scrape_data`ではそのレースの馬の情報を配列にして返します。
`race_info`はそのレースの情報を配列にして返します。
また最後の`map(&:strip)`で不要な改行や空白をまとめて削除しています。

#### 実行箇所

```ruby
if __FILE__ == $0
  index_url_list = index_urls(INDEX_YEAR)
  urls = index_url_list.map do |index_url|
    index_doc = setup_doc(index_url)
    create_result_urls(index_doc)
  end.flatten

  CSV.open('../result.csv', 'w') do |csv|
    csv << CSV_HEADER
    urls.each do |url|
      begin
        doc = setup_doc(url)
        race_info_columns = race_info(doc)
        table_data = doc.xpath("id('race_result')/div[@class='race_result_unit']/table[@class='basic narrow-xy striped']/tbody[1]/tr")
        table_data.each do |data|
          csv << race_info_columns + scrape_data(data)
        end
      rescue => e
        p e
      end
    end
  end
end
```
まず`index_urls`メソッドでデータが欲しい年のレース結果一覧URLの配列を取得します。
続いてそれをmapし、`create_result_urls`メソッドで各レース結果のURLを取得します。
この辺はcreate_url.rbに定義しているものです。

結果ページURLの準備ができたらCSVに書き込みます。
まずは`csv << CSV_HEADER`で項目を入れ、結果ページURLを回してレース情報と馬情報を書き込みます。
途中、変数`table_data`がありますが、中身はパースされたテーブルのhtmlです。
![スクリーンショット 2021-03-03 1.45.06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/358229/b1f544ca-2404-9de3-7aa6-2e2b9e5f2ffe.png)
これをeachで回して各行の情報を取得しています。

## 実行
ターミナルにて

```terminal
$ ruby main.rb
```
ターンッ

## 結果
2020~2021年現在までの重賞レースのデータを抽出することに成功しました🎉
![keibascrape.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/358229/2f679842-0742-18c5-b052-45ed3074576d.gif)

## あとがき
人生初スクレイピングでしたがとても楽しかったです。
思い通りに出力できた時はおーすげーとなりました。
あとはこのデータを駆使するのみ。
勝ちたい。以上です。