netkeiba.comをスクレイピングしてみた【Python/スクレイピング/Scrapy】

keiba

競馬の結果を集めて、将来の試合の結果を予測できたらいいと思いませんか。

そのためには、たくさんのデータを集める必要があります。

今回はnetkeiba.comさんのデータベースをスクレイピングして、過去の試合結果を集めてみます。

スクレイピングを始める前に

スクレイピングはデータ収集において、とても強力な手法です。

しかし取得した物の取り扱いを誤ったり、Webサイトの規約を破ってしまうと大変です。

netkeiba.comさんのスクレイピング規約を見ておきましょう

スクレイピングなどの行為により、サービスの運営に支障をきたす場合も含まれるようです。

そのためスクレイピングする際には、間隔を十分空けて節度を持って行わないといけないようです。

スクレイピングを行う際はリスクを承知した上、自己責任でお願いします。

スクレイピングの環境

今回はScrapyを使ってスクレイピングを行います。

ScrapyはPythonで開発されたスクレイピング・クローリングのフレームワークです。

簡単な記述で実行が可能なため、まだ使ったことが無いという方は公式サイトを覗いてみてください。

netkeiba.comの情報(着順・馬名・斤量など)を取得する

netkeiba.comのデータベースから、着順・馬名・斤量などの上を取得するプログラムです。

import scrapy

areaList=['0201','0102','0403','1004','0302','0503','0903','0603','0201','0201','0804']
numberList=['01','02','03','04','05','06','07','08','09','10','11','12']
roundList= ['01','02','03','04','05','06','07','08','09','10','11','12']

class KeibaScrapySpider(scrapy.Spider):
    name = 'keiba_scrapy'
    allowed_domains = ['db.netkeiba.com']
    start_urls = ['https://db.netkeiba.com/race/{}{}{}{}/'.format(str(y),str(j),str(i),str(x)) for y in range(2015,2022) for j in areaList for i in numberList for x in roundList]
    
    def get_title(self,title):
        if title:
            return title.split('目')[1].replace('  ','')
        return title
    
    def get_day(self,day):
        if day:
            return day.split(' ')[0].replace('年','/').replace('月','/').replace('日','')
        return day
    
    def get_distance(self,distance):
        if distance:
            return distance.split(' ')[0].replace('ダ','').replace('芝','').replace('左','').replace('右','').replace('m','').replace('外','')
        return distance
    
    def get_rounds(self,rounds):
        if rounds:
            return rounds.split(' ')[0]
        return rounds
    
    def get_rawMaterials(self,rawMaterials):
        if rawMaterials:
            return rawMaterials.split(' / ')[2].split(' : ')[0]
        return rawMaterials
    
    def get_ridingGround(self,ridingGround):
        if ridingGround:
            return ridingGround.split(' / ')[2].split(' : ')[1]
        return ridingGround
    
    def get_weather(self,weather):
        if weather:
            return weather.split(' / ')[1].split(' : ')[1]
        return weather
  
    def get_horse_weight(self,horse_weight):
        if horse_weight:
            return horse_weight.split('(')[0]
        return horse_weight

    def get_age(self,age):
        if age:
            return age[1]
        return
    
    def get_sex(self,age):
        if age:
            return age[0]
        return
  
    def parse(self, response):
        ancestors = response.xpath('//div[@id = "contents_liquid"]//tr')
        title=response.xpath('//div[@class="data_intro"]/p[@class="smalltxt"]/text()').get()
        rounds=response.xpath('normalize-space(//div[@class="data_intro"]/dl/dt/text())').get()
        ridingGround=response.xpath('normalize-space(//dl//dd/p/diary_snap_cut/span/text())').get()
        rawMaterials=response.xpath('normalize-space(//dl//dd/p/diary_snap_cut/span/text())').get()
        weather=response.xpath('normalize-space(//dl//dd/p/diary_snap_cut/span/text())').get()           
        
        for ancestor in ancestors:
            arrival =ancestor.xpath('.//td[1]/text()').get()
            number =ancestor.xpath('.//td[3]/text()').get()
            name=ancestor.xpath('.//td[4]/a/text()').get()
            age=ancestor.xpath('.//td[5]/text()').get()
            weight=ancestor.xpath('.//td[6]/text()').get()
            rider=ancestor.xpath('.//td[7]/a/text()').get()
            time=ancestor.xpath('.//td[8]/text()').get()
            popularity=ancestor.xpath('.//td[11]/span/text()').get()
            horse_weight=ancestor.xpath('.//td[12]/text()').get()

            yield{
                'レース名':self.get_title(title),
                'レース日':self.get_day(title),
                'ラウンド':self.get_rounds(rounds),
                '距離':self.get_distance(ridingGround),
                '素材':self.get_rawMaterials(rawMaterials),
                '馬場':self.get_ridingGround(ridingGround),
                '天気':self.get_weather(weather),              
                '着順':arrival,
                '馬番':number,
                '馬名':name,
                '馬齢':self.get_age(age),
                '馬性':self.get_sex(age),
                '斤量':weight,
                '騎手':rider,
                'タイム':time,
                '人気':popularity,
                '馬体重':self.get_horse_weight(horse_weight),
            }  

実行はコマンドプロンプトから行います。

scrapy crawl keiba_scrapy -o data.json

出力された結果は、以下になります。

今回はJsonファイルで出力しました。

もしCSVファイルで出力したい場合は、コマンドプロンプトの末尾をjsonからcsvに変更してみてください。

プログラムの解説

プログラムを簡単に解説します。

ライブラリ設定

#ライブラリ設定
import scrapy

プログラムを実行するために、scrapyをインポートします。

リストを用意する

netkeiba.comのデータベースは、以下の法則に従ったURLが設定されています。

そのため、リストを用意して中身を入れることで欲しい条件のデータを取得することができます。

#リストを作成
areaList=['0201','0102','0403','1004','0302','0503','0903','0603','0201','0201','0804']
numberList=['01','02','03','04','05','06','07','08','09','10','11','12']
roundList= ['01','02','03','04','05','06','07','08','09','10','11','12']

開催場所の対応表は以下のようになっています。

 場所  No 
函館0201
札幌0102
新潟0403
小倉1004
福島0302
東京0503
阪神0903
中山0603
中京0201
京都0804

目的の開催場所がある場合は、リストに目的の場所と合致する番号を入れてください。

このコードでは開催回数とラウンド回数をリストに入れてますが、range関数を使って回しても良いと思います。

スクレイピングするサイトの指定

開始URLを指定して、スクレイピングを行っていきます。

class KeibaScrapySpider(scrapy.Spider):
    name = 'keiba_scrapy'
    allowed_domains = ['db.netkeiba.com']
    start_urls = ['https://db.netkeiba.com/race/{}{}{}{}/'.format(str(y),str(j),str(i),str(x)) for y in range(2015,2022) for j in areaList for i in numberList for x in roundList]

start_urlsの中にリスト内表記を記述しています。

リストやrange関数に記述されたURLを総当たりで確認してくれます。

確認した結果、目的の情報があれば取得してくれるといった仕組みになっています。

関数

取得したデータを成形するために、クラス内に関数を設けます。

Split関数やreplace関数を用いて、不要なデータを削っていきます。

    def get_title(self,title):
        if title:
            return title.split('目')[1].replace('  ','')
        return title
    
    def get_day(self,day):
        if day:
            return day.split(' ')[0].replace('年','/').replace('月','/').replace('日','')
        return day
    
    def get_distance(self,distance):
        if distance:
            return distance.split(' ')[0].replace('ダ','').replace('芝','').replace('左','').replace('右','').replace('m','').replace('外','')
        return distance
    
    def get_rounds(self,rounds):
        if rounds:
            return rounds.split(' ')[0]
        return rounds
    
    def get_rawMaterials(self,rawMaterials):
        if rawMaterials:
            return rawMaterials.split(' / ')[2].split(' : ')[0]
        return rawMaterials
    
    def get_ridingGround(self,ridingGround):
        if ridingGround:
            return ridingGround.split(' / ')[2].split(' : ')[1]
        return ridingGround
    
    def get_weather(self,weather):
        if weather:
            return weather.split(' / ')[1].split(' : ')[1]
        return weather
  
    def get_horse_weight(self,horse_weight):
        if horse_weight:
            return horse_weight.split('(')[0]
        return horse_weight

    def get_age(self,age):
        if age:
            return age[1]
        return
    
    def get_sex(self,age):
        if age:
            return age[0]
        return
  

それでは、関数について説明をしていきます。

    def get_title(self,title):
        if title:
            return title.split('目')[1].replace('  ','')
        return title

レースのタイトルは、split関数を用いて「目」の文字の後ろ側にあるもの取得します。そして、replace関数を用いて全角の空白を削除します。

def get_day(self,day):
    if day:
        return day.split(' ')[0].replace('年','/').replace('月','/').replace('日','')
    return day

続いて年月日の変換についてです。

抽出した要素は、年月日以外の情報が含まれています。

そこでsplit関数を用いて、半角の空白の1つ目で分割します。

日付は年月日で記載されています。そのため、replace関数で「年」「月」「日」の文字列を変換しています。

このようは、split関数とreplace関数を用いてデータを取得しています。

Xpathを用いてデータを取得

続いて、各要素の取得方法について見ていきましょう。

スクレイピングは取得したい要素のXpathやCSSを記述することで可能となります。

今回のコードはXpathを用いて、要素を指定しています。

    def parse(self, response):
        ancestors = response.xpath('//div[@id = "contents_liquid"]//tr')
        title=response.xpath('//div[@class="data_intro"]/p[@class="smalltxt"]/text()').get()
        rounds=response.xpath('normalize-space(//div[@class="data_intro"]/dl/dt/text())').get()
        ridingGround=response.xpath('normalize-space(//dl//dd/p/diary_snap_cut/span/text())').get()
        rawMaterials=response.xpath('normalize-space(//dl//dd/p/diary_snap_cut/span/text())').get()
        weather=response.xpath('normalize-space(//dl//dd/p/diary_snap_cut/span/text())').get()           
        
        for ancestor in ancestors:
            arrival =ancestor.xpath('.//td[1]/text()').get()
            number =ancestor.xpath('.//td[3]/text()').get()
            name=ancestor.xpath('.//td[4]/a/text()').get()
            age=ancestor.xpath('.//td[5]/text()').get()
            weight=ancestor.xpath('.//td[6]/text()').get()
            rider=ancestor.xpath('.//td[7]/a/text()').get()
            time=ancestor.xpath('.//td[8]/text()').get()
            popularity=ancestor.xpath('.//td[11]/span/text()').get()
            horse_weight=ancestor.xpath('.//td[12]/text()').get()

            yield{
                'レース名':self.get_title(title),
                'レース日':self.get_day(title),
                'ラウンド':self.get_rounds(rounds),
                '距離':self.get_distance(ridingGround),
                '素材':self.get_rawMaterials(rawMaterials),
                '馬場':self.get_ridingGround(ridingGround),
                '天気':self.get_weather(weather),              
                '着順':arrival,
                '馬番':number,
                '馬名':name,
                '馬齢':self.get_age(age),
                '馬性':self.get_sex(age),
                '斤量':weight,
                '騎手':rider,
                'タイム':time,
                '人気':popularity,
                '馬体重':self.get_horse_weight(horse_weight),
            }  

設定について

Scrapyを使ってスクレイピングする際は、設定を別のファイルで行う必要があります。

この設定を誤ってしまうと、スクレイピング先のサイトに負荷をかけてしまいます。

そのため、設定は以下のように行っています。

DEFAULT_REQUEST_HEADERS = {'Accept-Language': 'ja',}
FEED_EXPORT_ENCODING='utf-8'
ROBOTSTXT_OBEY = True
DOWNLOAD_DELAY = 3

Scrapyを使用する際は、DEFAULT_REQUEST_HEADERSをJaに変更しましょう。

DEFAULT_REQUEST_HEADERS = {'Accept-Language': 'ja',}

変更することで、取得する情報が日本語になるように設定することができます。

出力するファイル形式をutf-8に設定しましょう。

FEED_EXPORT_ENCODING='utf-8'

デフォルト値はNoneになっています。

サイトのrobots.txtを尊重するために、Trueを指定しましょう。

ROBOTSTXT_OBEY = True

デフォルト値はFalseになっています。

スクレイピング間隔を空けるために、DOWNLOAD_DELAYを指定しましょう。

DOWNLOAD_DELAY = 3

スクレピング先に負荷をかけないように、1秒以上で設定しましょう。

上記のコードでは、約3秒でスクレイピングするように設定しています。

コメントを残す

メールアドレスが公開されることはありません。

CAPTCHA