LoginSignup
22
33

More than 3 years have passed since last update.

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

Last updated at Posted at 2019-10-08

はじめに

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

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

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

やること

2019-10-08 22.28.07 nar.netkeiba.com f34908d34776.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

captcha.png

念の為、Googleスプレッドシートでも確認
2019-10-08 22.48.25 docs.google.com ca23487e25d0.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
captcha2.png

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

苦労した点

tbody

スクレイピング部分をするためのCSSセレクタは、Chromeの検証ツールを使ってCopy→Copy selecterでお手軽にコピーしたのだが、ブラウザが実際のソースにはない

というタグをとの間に補完するという仕様のせいで最初の壁にぶつかった。が、「Pythonクローリング&スクレイピング ~~」のコラムに書いてあったおかげで早々に気づけました。

↓検証ツールではtbodyというタグが存在するように表示しているが…
2019-10-08 23.01.44 nar.netkeiba.com 539bd0f4221e.png

↓実際のソースコードにはそんなものはありませ~~ん
2019-10-08 23.02.20  13b7ee81feaa.png

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

encoding

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

謎の空文字

謎である。

writerow,wrierows

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

追加したい機能

  • スクレイピングしたページごとのファイル名をつける

    • 今のコードだとどのページも同じ名前のCSVに保存されるので、結果ページのURLの末尾のレースIDの数値を正規表現で切り出してファイル名につけたいと思う
  • 再帰的なクローリング・スクレイピングの実装

    • 結果表から出走馬のページに飛んで父名・母名・馬主名・生産牧場などを取得して列に追加するというのもありかもしれない

まとめ

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

22
33
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
22
33