前々からscrapingとpythonに興味を持っていました。
両者への入門として、pythonのscrapyを利用して、
プロ野球の試合情報サイトから打撃成績の抜き出しをしてみたいと思います。
はじめに
scrapingの実施には著作権が関わってきます。
調べてみたところ、情報の収集・解析自体は問題無いみたいです。
(会員制以外のwebサイトの場合)
以下を参考にしています。
参考
以下の書籍をベースにしているので、詳しくはそちらをお願い致します
成果目標
- 選手毎にシーズン全ての打撃成績を記録できる形式でデータ抽出すること
情報取得サイトの選定
scrapingする対象に、以下の特徴のを持つサイトを選びました。
- 会員制でない
- 打撃成績が右安,三ゴロ...の様に、飛んだ守備位置まで分かる事
- 打撃成績が試合中に更新される事
- scrapingで加工しやすいhtml構造である事
ちなみに、選んだサイト構造は以下の様になっています。
(○月の試合一覧ページ)
├─→ ○月1日の試合 G vs De ├─→ スコア(打撃成績ページ)
├─→ ○月1日の試合 Ys vs C ├─→ スコア(打撃成績ページ)
├─→ ○月2日の試合 G vs De ├─→ スコア(打撃成績ページ)
└─→...
ただし、scraping用のソースコードも一部載せる予定であるので、
情報取得元に迷惑がかからない様に、
サイト名やurlは記載致しません。
環境設定
- Pythonの環境構築 on Mac ( pyenv, virtualenv, anaconda, ipython notebook ) - Qiita
- python anaconda環境にscrapyをインストールする - Qiita
上記を参考に、以下の設定をしました。
- 開発ディレクトリにpythonバージョンの紐付け
- scrapyのinstall
設定した結果、環境は以下の様な状態になっています。
(baseballEnv) 11:34AM pyenv versions [~/myDevelopPj/baseball]
system
3.5.1
anaconda3-4.2.0
anaconda3-4.2.0/envs/baseballEnv
* baseballEnv (set by /Users/username/myDevelopPj/baseball/.python-version)
(baseballEnv) 11:34AM scrapy version [
Scrapy 1.3.3
実装
プロジェクトの開始
startコマンドでプロジェクトを作成します。
scrapy startproject scrapy_baseball
結果、以下の様になります。
(baseballEnv) 7:32AM tree [~/myDevelopPj/baseball]
.
├── readme.md
└── scrapy_baseball
├── scrapy.cfg
└── scrapy_baseball
├── init.py
├── pycache
├── items.py
├── middlewares.py
├── pipelines.py
├── settings.py
└── spiders
├── init.py
└── pycache
5 directories, 8 files
最初に行う設定
settings.py に下記を設定します。
これを行わないとダウンロード間隔が0秒になるため、
高負荷をかけてしまいます。
DOWNLOAD_DELAY = 1
Spiderの作成
scrapy.cfg が存在するディレクトリにいることを確認します。
(baseballEnv) 10:13AM ls scrapy.cfg [~/myDevelopPj/baseball/scrapy_baseball]
scrapy.cfg
scrapy genspider battingResult xxxxxxxxx を実行します。
(xxxxxxxxx:ドメイン名)
すると、battingResult.pyが作成されます。
- battingResult.py
# -*- coding: utf-8 -*-
import scrapy
class BattingresultSpider(scrapy.Spider):
name = "battingResult"
allowed_domains = ["xxxxxxxxx"]
start_urls = ['http://xxxxxxxxx/']
def parse(self, response):
pass
そしたら、以下の様に実装します。
# -*- coding: utf-8 -*-
import scrapy
class BattingresultSpider(scrapy.Spider):
name = "battingResult"
allowed_domains = ["xxxxxxxxx"]
start_urls = ['http://xxxxxxxxx/']
xpath_team_name_home = '//*[@id="wrapper"]/div/dl[2]/dt'
xpath_batting_result_home = '//*[@id="wrapper"]/div/div[6]/table/tr'
xpath_team_name_visitor = '//*[@id="wrapper"]/div/dl[3]/dt'
xpath_batting_result_visitor = '//*[@id="wrapper"]/div/div[9]/table/tr'
def parse(self, response):
"""
start_urlに指定したページの○月のゲーム一覧から個々のゲームへのリンクを抜き出してたどる。
"""
for url in response.css("table.t007 tr td a").re(r'../pastgame.*?html'):
yield scrapy.Request(response.urljoin(url), self.parse_game)
def parse_game(self, response):
"""
個々のゲームから各選手の打撃成績を抜き出す
"""
# ホームチームのデータ
teamName = self.parse_team_name(response,self.xpath_team_name_home)
print(teamName)
self.prrse_batting_result(response,self.xpath_batting_result_home)
# ビジターチームのデータ
teamName = self.parse_team_name(response,self.xpath_team_name_visitor)
print(teamName)
self.prrse_batting_result(response,self.xpath_batting_result_visitor)
def parse_team_name(self,response,xpath):
teamName = response.xpath(xpath).css('dt::text').extract_first()
return teamName
def prrse_batting_result(self,response,xpath):
for record in response.xpath(xpath):
playerName = record.css('td.player::text').extract_first()
if playerName is None:
continue
outputData = ''
for result in record.css('td.result'):
outputData = ','.join([outputData,result.css('::text').extract_first()])
print(playerName + outputData)
pass
start_urls
に指定されるのは、○月の試合一覧ページ。
そこから、個々の試合ページに遷移し、打撃成績を抜き出す。
.css()
でcssセレクタにマッチするノードの一覧オブジェクトが取得出来るので、
.re()
で正規表現に一致した部分のみの取得をしたり、
css('td.player::text')
のように、 疑似セレクターでテキストノードを取得するなどします。
xpath()でtbodyを取得出来ない現象は、下記で解決しました
Not able to extract text from the td tag/element using python scrapy - Stack Overflow
xpathを取得したり、検証するのに下記が役立ちました。
ChromeでXPathを取る・検証する - Qiita
実行
以下コマンドを実行します。
scrapy crawl battingResult
その結果、以下の様なデータが取得できました。(データはイメージです。)
チーム名と選手名が取得出来ているので、
別チームに同名選手がいても区別出来そうです。
DeNA・打者成績
選手A,三ゴ,-,三ゴ,-,左線2,-,四球,-,遊ゴ併
選手B,空三振,-,左中本,-,空三振,-,左安,-,-
選手C,一ゴ,-,三直,-,左安,-,三ゴ併,-,-
選手D,-,四球,右安,-,遊ゴ併,-,-,左飛,-
選手E,-,左本,左飛,-,-,中安,-,右安,-
選手F,-,遊ゴ,-,四球,-,中飛,-,空三振,-
選手G,-,二ゴ,-,右中2,-,左飛,-,二飛,-
選手H,-,右飛,-,見三振,-,空三振,-,-,-
選手I,-,-,-,-,-,-,-,-,-
選手J,-,-,-,-,-,-,-,-,-
選手K,-,-,-,-,-,-,-,-,-
選手L,-,-,-,-,-,-,-,-,中飛
選手M,-,-,-,-,-,-,-,-,-
選手N,-,-,二ゴ失,左飛,-,-,空三振,-,右安
巨人・打者成績
選手1,空三振,-,右飛,-,-,一ゴ失,-,二ゴ,-
選手2,見三振,-,-,見三振,-,左飛,-,中安,-
選手3,左安,-,-,三ゴ,-,左中本,-,左安,-
選手4,二飛,-,-,中安,-,二ゴ,-,三邪飛,-
選手5,-,見三振,-,一ゴ,-,三ゴ,-,左飛,-
選手6,-,空三振,-,-,右飛,-,右飛,-,遊邪飛
選手7,-,見三振,-,-,右中2,-,左安,-,右飛
選手8,-,-,遊飛,-,遊ゴ,-,三ゴ,-,-
選手9,-,-,-,-,-,-,-,-,空三振
選手10,-,-,一ゴ,-,-,-,-,-,-
選手11,-,-,-,-,-,-,-,-,-
選手12,-,-,-,-,空三振,-,-,-,-
選手13,-,-,-,-,-,-,-,-,-
選手14,-,-,-,-,-,-,見三振,-,-
選手15,-,-,-,-,-,-,-,-,-
課題
データ取得を実施した結果、
目標'選手毎にシーズン全ての打撃成績を記録できる形式でデータ抽出する'を達成するにあたって、
以下の様な課題が残りました。
- 打撃情報'途中終わり'や延長試合などのデータパターンの網羅が大変