競馬サイトをクローリング&スクレイピングしてみた その1

#はじめに
先にまとめた「Pythonクローリング＆スクレイピング［増補改訂版］―データ収集・解析のための実践開発ガイドー」加藤耕太・著 の第2章までの知識の実践として、Pythonを利用した競馬のレース結果のクローリング・スクレイピングを行う。なおコードに関しては同書P71のサンプルコードを大いに参考にしている。

今回クローリング・スクレイピングの対象とするのは競馬情報サイトnetkeibaの地方競馬カテゴリの上部からリンクされているここ数日の開催の結果表である(数日たったら消えるっぽい？コードを書いている途中で気づいた…)

環境についてはWindows10のAtomで書いたコードを、WSL上のUbuntu18.04からPython3.7.3を用いて実行している

#やること
![2019-10-08 22.28.07 nar.netkeiba.com f34908d34776.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/253705/597c979d-4c16-d6c9-74f4-6ac71c980fd4.png)

- 上の結果表(8回浦和1日目1R3歳三)から各行を取り出してCSV形式で保存する

#実装した機能
- コマンドライン引数で入力したURLのWebページを取得(クローリング)
- 取得結果をパースして欲しい部分のみを切り出す(スクレイピング)
- スクレイピングしたデータをCSVに出力

#実際のコードと実行結果
```keiba_scraping.py

import sys
import requests
import lxml.html
import re
import csv

def main(argv):
    url = argv[1] #コマンドライン引数からURLを取得
    html = fetch(url) #URLのWebページを取得
    result = scrape(html) #取得したWebページから欲しい部分のみを切り出す
    save('result.csv',result) #切り出した結果をCSVに保存する

def fetch(url :str): 
    r = requests.get(url) #urlのWebページを保存する
    r.encoding = r.apparent_encoding #文字化けを防ぐためにencodingの値をappearent_encodingで判定した値に変更する
    return r.text #取得データを文字列で返す

def scrape(html: str):
    html = lxml.html.fromstring(html) #fetch()での取得結果をパース
    result = [] #スクレイピング結果を格納
    for h in html.cssselect('#race_main > div > table > tr'):#スクレイピング箇所をCSSセレクタで指定
        column = ((",".join(h.text_content().split("\n"))).lstrip(",").rstrip(",")).split(",")
        #text_content()はcssselectでマッチした部分のテキストを改行文字で連結して返すので、
        #splitを使って改行文字で分割して、その結果をカンマ区切りでjoinする。
        #前と後ろに余計な空白とカンマが入っている(tdじゃなくてtrまでのセレクタをしていした分の空文字が入っちゃってる?ようわからん)ので、
        #splitで空白を、lstrip,rstripでカンマを削除してさらにそれをカンマで区切ってリストにしている
        column.pop(4) if column[4] == "" else None  #1行目以外、馬名と性齢の間に空文字が入っちゃってるので取り出す
        result.append(column) #リストに行のデータ(リストを追加)

    return result #結果を返す

def save(file_path, result):
     with open(file_path, 'w', newline='') as f: #ファイルに書き込む
         writer = csv.writer(f) #ファイルオブジェクトを引数に指定する
         writer.writerow(result.pop(0)) #一行目のフィールド名を書き込む
         writer.writerows(result) #残りの行を書き込む

if __name__ == '__main__':
    main(sys.argv) 
```

実行してCSVを表示すると…

```
$ python keiba_scraping.py 'https://nar.netkeiba.com/?pid=race&id=p201942100701'
$ cat result.csv
```

<img width="533" alt="captcha.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/253705/8270047c-fecb-8857-72bf-9ce8385f00d2.png">

念の為、Googleスプレッドシートでも確認
![2019-10-08 22.48.25 docs.google.com ca23487e25d0.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/253705/1025071a-7d0c-d5e8-3afb-ee304be0b5a4.png)

行・列ともに同じ内容が表示されています

同じページ構造の他のレースでも試してみると…

```
$ python keiba_scraping.py 'https://nar.netkeiba.com/?pid=race&id=c201930100812'
$ cat result.csv
```

![2019-10-08 22.50.11 nar.netkeiba.com 277de73bedad.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/253705/6ce4b17b-2115-dae0-afbd-d5cadd0c3234.png)
<img width="580" alt="captcha2.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/253705/d4801a3d-6fe4-6f25-8e82-e70a25ce3d40.png">

きちんとレース結果が取得されています~~(1着馬を買うかどうか悩んでいるうちに、投票締め切られていたのが悔やまれる)~~

#苦労した点
##tbody
スクレイピング部分をするためのCSSセレクタは、Chromeの検証ツールを使ってCopy→Copy selecterでお手軽にコピーしたのだが、ブラウザが実際のソースにはない<tbody>というタグを<table>と<tr>の間に補完するという仕様のせいで最初の壁にぶつかった。が、「Pythonクローリング＆スクレイピング ～～」のコラムに書いてあったおかげで早々に気づけました。

↓検証ツールではtbodyというタグが存在するように表示しているが…
![2019-10-08 23.01.44 nar.netkeiba.com 539bd0f4221e.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/253705/2bad4528-1d50-8467-c890-f3c896678492.png)

↓実際のソースコードにはそんなものはありませ～～ん
![2019-10-08 23.02.20  13b7ee81feaa.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/253705/61239a84-8779-c735-2c0f-4ea537940d72.png)

これを知らずに検証ツールでtbodyの中にある要素をcopy selecterしちゃうとセレクタが
`#race_main > div > table > tbody > tr:nth-child(1) > th:nth-child(6)`
みたいになってlxmlが正しく読み取ってくれなくなるので注意。

##encoding
今回対象としたサイトは文字エンコーディングeuc-jpですが、そのまま取得すると文字化けしてしまったので`r.encoding`に`r.apparent_encoding`が判断した文字エンコーディングの値を入れてます

##謎の空文字
謎である。

#writerow,wrierows
書き間違いに注意！書き間違えたおかげで空文字の存在に気づけたというのはあるが。

#追加したい機能
- スクレイピングしたページごとのファイル名をつける
    - 今のコードだとどのページも同じ名前のCSVに保存されるので、結果ページのURLの末尾のレースIDの数値を正規表現で切り出してファイル名につけたいと思う

- 再帰的なクローリング・スクレイピングの実装
    - 結果表から出走馬のページに飛んで父名・母名・馬主名・生産牧場などを取得して列に追加するというのもありかもしれない

#まとめ
ひとまず、完成しましたが追加したい機能もあるのでその辺は今後この記事に追記していきたい