Lean Baseball

Engineering/Baseball/Python/Agile/SABR and more...

「人とWebに優しい」Scrapyの使い方サンプル〜 #PyConJP 2017のつづき(なお野球)

PyCon JP 2017で発表した野球×Pythonの分析ネタの詳細解説です.*1

プレゼンテーション:野球を科学する技術〜Pythonを用いた統計ライブラリ作成と分析基盤構築 | PyCon JP 2017 in TOKYO

speakerdeck.com

youtu.be

時間および諸々の都合(察し)で公開できなかった*2,

「人とWebに優しい」Scrapyアプリのサンプル(なお野球)

を作って公開したのでその紹介と,PyConのプレゼンで発表しきれなかった部分を簡単に紹介します.

おしながき

対象の読者

  • Pythonレベル初級〜中級
  • 好きなエディタ・環境でPythonを読み書きできる(本を読みながら・人に聞きながらでもOK)
  • データ分析とか機械学習やりたい!…けどデータ集めなきゃ!っていう方
  • なお,特段野球好き・マニアである必要はありません.野球アレルギーが無ければ大丈夫.*3

参考文献

このエントリーを読む前でも読んだ後でも目を通しておくことをオススメします.

Pythonクローリング&スクレイピング -データ収集・解析のための実践開発ガイド-

Pythonクローリング&スクレイピング -データ収集・解析のための実践開発ガイド-

Scrapyのみならず,スクレイピングとクローラーについて事細かかつわかりやすく解説している良著でホント参考にさせてもらいました.

Scrapyを用いた日本プロ野球データ取得Exampleアプリ

こちらに準備しました.

github.com

動作環境・使い方はREADME.mdを御覧ください.

大雑把に使い方を書くとこんな感じです.

  1. コードをcloneする
  2. Python 3.6およびscrapyをインストール
  3. 「scrapy crawl batter -a year=2017 -a league=1」とコマンドを叩くとsqlite3のDBができて打者の情報をドカドカ入れてくれる
  4. 「scrapy crawl pitcher -a year=2017 -a league=1」とコマンドを叩くと投手の情報を(上に同じく)

出来上がったDBは野球好き的にたまらないと思います.

ポイント

主にScrapyの解説となります…が,(アーキテクト図を除き)Scrapyそのものの解説は端折っているので詳細を知りたい方は先ほどの「Pythonクローリング&スクレイピング ―データ収集・解析のための実践開発ガイド―」を読むか,Scrapyの公式サイトを御覧ください.

Scrapy 1.4 documentation — Scrapy 1.4.0 documentation

全体像

Scrapyの全体アーキテクト図に今回の対象範囲を合わせてみました.

f:id:shinyorke:20170920005842p:plain

「人とWebに優しい」settings.pyの書き方

最低限守るべきポイントは以下の3つです.

  1. robots.txtに従う(Webクローリングする上での最低限のお作法)
  2. リクエストの発行回数を大幅に減らす.具体的には同一ドメインに対して並列数を絞る,ダウンロード間隔を60秒以上空ける
  3. キャッシュは必ず取る,再実行時はキャッシュを相手にする.

今回のサンプルアプリは,

  • 一日に一回,野球選手の成績を取る
  • 一覧ページを相手, 打撃成績は12球団分,投手成績も12球団分
  • Spiderは打者もしくは投手を相手するので1分以上空けたとしても15分前後で全チームの打撃成績と投手成績が入手できる

という要件のもと,

  • リクエスト数は2つもあれば十分
  • 一日一回取るぐらいならキャッシュも24時間効かせてOK(デバッグ目的でたくさん叩いても相手サイトに迷惑かけない)

という方針で設定しました.

設定内容はこんな感じです(コメントも一緒に読んでもらえるとなお良い!)

# -*- coding: utf-8 -*-

# Scrapy settings for baseball project
#
# For simplicity, this file contains only settings considered important or
# commonly used. You can find more settings consulting the documentation:
#
#     http://doc.scrapy.org/en/latest/topics/settings.html
#     http://scrapy.readthedocs.org/en/latest/topics/downloader-middleware.html
#     http://scrapy.readthedocs.org/en/latest/topics/spider-middleware.html

BOT_NAME = 'baseball'  # botの名前

SPIDER_MODULES = ['baseball.spiders']
NEWSPIDER_MODULE = 'baseball.spiders'


# Crawl responsibly by identifying yourself (and your website) on the user-agent
#USER_AGENT = 'baseball (+http://www.yourdomain.com)'

# Obey robots.txt rules
ROBOTSTXT_OBEY = True  # robots.txtに従うか否か

# Configure maximum concurrent requests performed by Scrapy (default: 16)
CONCURRENT_REQUESTS = 2  # リクエスト並行数,16もいらないので2

# Configure a delay for requests for the same website (default: 0)
# See http://scrapy.readthedocs.org/en/latest/topics/settings.html#download-delay
# See also autothrottle settings and docs
DOWNLOAD_DELAY = 60  # ダウンロード間隔(s),60秒に設定
# The download delay setting will honor only one of:
CONCURRENT_REQUESTS_PER_DOMAIN = 2  # 同一ドメインに対する並行リクエスト数. CONCURRENT_REQUESTSと同じ値でOK(サンプルは単一ドメインしか相手していない)
CONCURRENT_REQUESTS_PER_IP = 0  # 同一IPに対する並行リクエスト数. ドメインで絞るので無効化してOK

# Disable cookies (enabled by default)
#COOKIES_ENABLED = False

# Disable Telnet Console (enabled by default)
#TELNETCONSOLE_ENABLED = False

# Override the default request headers:
#DEFAULT_REQUEST_HEADERS = {
#   'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
#   'Accept-Language': 'en',
#}

# Enable or disable spider middlewares
# See http://scrapy.readthedocs.org/en/latest/topics/spider-middleware.html
#SPIDER_MIDDLEWARES = {
#    'baseball.middlewares.BaseballSpiderMiddleware': 543,
#}

# Enable or disable downloader middlewares
# See http://scrapy.readthedocs.org/en/latest/topics/downloader-middleware.html
#DOWNLOADER_MIDDLEWARES = {
#    'baseball.middlewares.MyCustomDownloaderMiddleware': 543,
#}

# Enable or disable extensions
# See http://scrapy.readthedocs.org/en/latest/topics/extensions.html
#EXTENSIONS = {
#    'scrapy.extensions.telnet.TelnetConsole': None,
#}

# Configure item pipelines
# See http://scrapy.readthedocs.org/en/latest/topics/item-pipeline.html
ITEM_PIPELINES = {
    'baseball.pipelines.BaseballPipeline': 100,  # DB保存用のミドルウェア
}

# Enable and configure the AutoThrottle extension (disabled by default)
# See http://doc.scrapy.org/en/latest/topics/autothrottle.html
#AUTOTHROTTLE_ENABLED = True
# The initial download delay
#AUTOTHROTTLE_START_DELAY = 5
# The maximum download delay to be set in case of high latencies
#AUTOTHROTTLE_MAX_DELAY = 60
# The average number of requests Scrapy should be sending in parallel to
# each remote server
#AUTOTHROTTLE_TARGET_CONCURRENCY = 1.0
# Enable showing throttling stats for every response received:
#AUTOTHROTTLE_DEBUG = False

# Enable and configure HTTP caching (disabled by default)
# See http://scrapy.readthedocs.org/en/latest/topics/downloader-middleware.html#httpcache-middleware-settings
HTTPCACHE_ENABLED = True  # キャッシュの有無,勿論True
HTTPCACHE_EXPIRATION_SECS = 60 * 60 * 24  # キャッシュのExpire期間. 24時間(秒数で指定)
HTTPCACHE_DIR = 'httpcache'  # キャッシュの保存先(どこでも良い)
#HTTPCACHE_IGNORE_HTTP_CODES = []
#HTTPCACHE_STORAGE = 'scrapy.extensions.httpcache.FilesystemCacheStorage'

リクエスト数やキャッシュ有効期限は要件・サイトに合わせて設定でOKですが,元々のScrapyが若干強気の設定なので注意しましょう.

「ひととWebにやさしく」しましょう!

Spider(クローラー本体)について〜Itemも添えて

取得先のURLルールを把握した上で作成しています.

なお,取得メソッドはxpathです.

Spider…の前にItem(構造体)

items.pyにあります.

投打共に野球用語の英語略称を用いてます,コメントともに見てもらえれば.*4

# -*- coding: utf-8 -*-

# Define here the models for your scraped items
#
# See documentation in:
# http://doc.scrapy.org/en/latest/topics/items.html

from scrapy import Item, Field


class BatterItem(Item):
    year = Field()      # 年度
    team = Field()      # チーム
    name = Field()      # 名前
    bat = Field()       # 右打ち or 左打ち or 両打ち
    games = Field()     # 試合数
    pa = Field()        # 打席数
    ab = Field()        # 打数
    r = Field()         # 得点
    h = Field()         # 安打
    double = Field()    # 二塁打
    triple = Field()    # 三塁打
    hr = Field()        # 本塁打
    tb = Field()        # 塁打
    rbi = Field()       # 打点
    sb = Field()        # 盗塁
    cs = Field()        # 盗塁死
    sh = Field()        # 犠打(バント)
    sf = Field()        # 犠飛(犠牲フライ)
    bb = Field()        # 四球
    ibb = Field()       # 故意四球(敬遠)
    hbp = Field()       # 死球(デットボール)
    so = Field()        # 三振
    dp = Field()        # 併殺
    ba = Field()        # 打率
    slg = Field()       # 長打率
    obp = Field()       # 出塁率


class PitcherItem(Item):
    year = Field()      # 年度
    team = Field()      # チーム
    name = Field()      # 名前
    throw = Field()     # 右投げ or 左投げ
    games = Field()     # 登板数
    w = Field()         # 勝利
    l = Field()         # 敗北
    sv = Field()        # セーブ
    hld = Field()       # ホールド
    hp = Field()        # HP(ホールドポイント
    cg = Field()        # 完投
    sho = Field()       # 完封
    non_bb = Field()    # 無四球
    w_per = Field()     # 勝率
    bf = Field()        # 打者
    ip = Field()        # 投球回
    h = Field()         # 被安打
    hr = Field()        # 被本塁打
    bb = Field()        # 与四球
    ibb = Field()       # 故意四球(敬遠)
    hbp = Field()       # 与死球
    so = Field()        # 三振
    wp = Field()        # 暴投
    bk = Field()        # ボーク
    r = Field()         # 失点
    er = Field()        # 自責点
    era = Field()       # 防御率

batter.py(打者成績)

一つのtableタグに入ってるので愚直にtrとtdを得る感じでグイグイ回して取得しています.

# -*- coding: utf-8 -*-
import scrapy

from baseball.items import BatterItem
from baseball.spiders import TEAMS, LEAGUE_TOP, BAT_RIGHT, BAT_LEFT, BAT_SWITCH
from baseball.spiders import BaseballSpidersUtil as Util


class BatterSpider(scrapy.Spider):
    name = 'batter'
    allowed_domains = ['npb.jp']
    URL_TEMPLATE = 'http://npb.jp/bis/{year}/stats/idb{league}_{team}.html'

    def __init__(self, year=2017, league=LEAGUE_TOP):
        """
        初期処理(年度とURLの設定)
        :param year: シーズン年
        :param league: 1 or 2(1軍もしくは2軍)
        """
        self.year = year
        # URLリスト(12球団)
        self.start_urls = [self.URL_TEMPLATE.format(year=year, league=league, team=t) for t in TEAMS]

    def parse(self, response):
        """
        選手一人分の打撃成績
        :param response: 取得した結果(Response)
        :return: 打撃成績
        """
        for tr in response.xpath('//*[@id="stdivmaintbl"]/table').xpath('tr'):
            item = BatterItem()
            if not tr.xpath('td[2]/text()').extract_first():
                continue
            item['year'] = self.year
            item['team'] = Util.get_team(response.url)
            item['bat'] = self._get_bat(Util.get_text(tr.xpath('td[1]/text()').extract_first()))
            item['name'] = Util.get_text(tr.xpath('td[2]/text()').extract_first())
            item['games'] = Util.text2digit(tr.xpath('td[3]/text()').extract_first(), digit_type=int)
            item['pa'] = Util.text2digit(tr.xpath('td[4]/text()').extract_first(), digit_type=int)
            item['ab'] = Util.text2digit(tr.xpath('td[5]/text()').extract_first(), digit_type=int)
            item['r'] = Util.text2digit(tr.xpath('td[6]/text()').extract_first(), digit_type=int)
            item['h'] = Util.text2digit(tr.xpath('td[7]/text()').extract_first(), digit_type=int)
            item['double'] = Util.text2digit(tr.xpath('td[8]/text()').extract_first(), digit_type=int)
            item['triple'] = Util.text2digit(tr.xpath('td[9]/text()').extract_first(), digit_type=int)
            item['hr'] = Util.text2digit(tr.xpath('td[10]/text()').extract_first(), digit_type=int)
            item['tb'] = Util.text2digit(tr.xpath('td[11]/text()').extract_first(), digit_type=int)
            item['rbi'] = Util.text2digit(tr.xpath('td[12]/text()').extract_first(), digit_type=int)
            item['sb'] = Util.text2digit(tr.xpath('td[13]/text()').extract_first(), digit_type=int)
            item['cs'] = Util.text2digit(tr.xpath('td[14]/text()').extract_first(), digit_type=int)
            item['sh'] = Util.text2digit(tr.xpath('td[15]/text()').extract_first(), digit_type=int)
            item['sf'] = Util.text2digit(tr.xpath('td[16]/text()').extract_first(), digit_type=int)
            item['bb'] = Util.text2digit(tr.xpath('td[17]/text()').extract_first(), digit_type=int)
            item['ibb'] = Util.text2digit(tr.xpath('td[18]/text()').extract_first(), digit_type=int)
            item['hbp'] = Util.text2digit(tr.xpath('td[19]/text()').extract_first(), digit_type=int)
            item['so'] = Util.text2digit(tr.xpath('td[20]/text()').extract_first(), digit_type=int)
            item['dp'] = Util.text2digit(tr.xpath('td[21]/text()').extract_first(), digit_type=int)
            item['ba'] = Util.text2digit(tr.xpath('td[22]/text()').extract_first(), digit_type=float)
            item['slg'] = Util.text2digit(tr.xpath('td[23]/text()').extract_first(), digit_type=float)
            item['obp'] = Util.text2digit(tr.xpath('td[24]/text()').extract_first(), digit_type=float)
            yield item

    def _get_bat(self, text):
        """
        右打ち or 左打ち or 両打ち
        :param text: テキスト
        :return: 右打ち or 左打ち or 両打ち
        """
        if text == '*':
            return BAT_LEFT
        elif text == '+':
            return BAT_SWITCH
        return BAT_RIGHT

結構愚直に泥臭く書いています笑

なお,ところどころでリーグやチームの設定,数値変換などが登場するのでこれらについてはspiderパッケージ直下に便利ライブラリを作っています.

# baseball/spider/__init__.pyに書いてます
# 打撃・投球で共通して使う定義
LEAGUE_TOP = 1  # 一軍
LEAGUE_MINOR = 2  # 二軍

THROW_RIGHT = 'R'   # 右投げ
THROW_LEFT = 'L'    # 左投げ

BAT_RIGHT = 'R'   # 右打ち
BAT_LEFT = 'L'    # 左打ち
BAT_SWITCH = 'T'    # 両打ち

TEAMS = {
    'f': 'fighters',
    'h': 'hawks',
    'm': 'marines',
    'l': 'lions',
    'e': 'eagles',
    'bs': 'buffalos',
    'c': 'carp',
    'g': 'giants',
    'db': 'baystars',
    's': 'swallows',
    't': 'tigers',
    'd': 'dragons',
}


class BaseballSpidersUtil:

    @classmethod
    def get_team(cls, url):
        try:
            return TEAMS.get(url.replace('.html', '').split('_')[-1], 'Unknown')
        except IndexError:
            return 'Unknown'

    @classmethod
    def get_text(cls, text):
        """
        テキスト取得(ゴミは取り除く)
        :param text: text
        :return: str
        """
        if not text:
            return ''
        return text.replace('\u3000', ' ')

    @classmethod
    def text2digit(cls, text, digit_type=int):
        """
        数値にキャストする(例外時はゼロを返す)
        :param text: text
        :param digit_type: digit type(default:int)
        :return: str
        """
        if not text:
            return digit_type(0)
        try:
            return digit_type(text)
        except ValueError as e:
            print("変換に失敗しているよ:{}".format(e))
            return digit_type(0)

pitcher.py(投手成績)

打撃成績とほぼ同じ.

なお,投球回(ip)は小数点以下でカラムが別れるという謎仕様だったので真ん中ら辺のコードが若干面白いことになっています笑

# -*- coding: utf-8 -*-
import scrapy

from baseball.items import PitcherItem
from baseball.spiders import TEAMS, LEAGUE_TOP, THROW_RIGHT, THROW_LEFT
from baseball.spiders import BaseballSpidersUtil as Util


class PitcherSpider(scrapy.Spider):
    name = 'pitcher'
    allowed_domains = ['npb.jp']
    allowed_domains = ['npb.jp']
    URL_TEMPLATE = 'http://npb.jp/bis/{year}/stats/idp{league}_{team}.html'

    def __init__(self, year=2017, league=LEAGUE_TOP):
        """
        初期処理(年度とURLの設定)
        :param year: シーズン年
        :param league: 1 or 2(1軍もしくは2軍)
        """
        self.year = year
        # URLリスト(12球団)
        self.start_urls = [self.URL_TEMPLATE.format(year=year, league=league, team=t) for t in TEAMS]

    def parse(self, response):
        """
        選手一人分の投球成績
        :param response: 取得した結果(Response)
        :return: 投球成績
        """
        for tr in response.xpath('//*[@id="stdivmaintbl"]/table').xpath('tr'):
            item = PitcherItem()
            if not tr.xpath('td[3]/text()').extract_first():
                continue
            item['year'] = self.year
            item['team'] = Util.get_team(response.url)
            item['name'] = Util.get_text(tr.xpath('td[2]/text()').extract_first())
            item['throw'] = self._get_throw(Util.get_text(tr.xpath('td[1]/text()').extract_first()))
            item['games'] = Util.text2digit(tr.xpath('td[3]/text()').extract_first(), digit_type=int)
            item['w'] = Util.text2digit(tr.xpath('td[4]/text()').extract_first(), digit_type=int)
            item['l'] = Util.text2digit(tr.xpath('td[5]/text()').extract_first(), digit_type=int)
            item['sv'] = Util.text2digit(tr.xpath('td[6]/text()').extract_first(), digit_type=int)
            item['hld'] = Util.text2digit(tr.xpath('td[7]/text()').extract_first(), digit_type=int)
            item['hp'] = Util.text2digit(tr.xpath('td[8]/text()').extract_first(), digit_type=int)
            item['cg'] = Util.text2digit(tr.xpath('td[9]/text()').extract_first(), digit_type=int)
            item['sho'] = Util.text2digit(tr.xpath('td[10]/text()').extract_first(), digit_type=int)
            item['non_bb'] = Util.text2digit(tr.xpath('td[11]/text()').extract_first(), digit_type=int)
            item['w_per'] = Util.text2digit(tr.xpath('td[12]/text()').extract_first(), digit_type=float)
            item['bf'] = Util.text2digit(tr.xpath('td[13]/text()').extract_first(), digit_type=int)
            # 小数点以下が別のカラムに入ってるのでこういう感じになります
            if tr.xpath('td[15]/text()').extract_first():
                ip = tr.xpath('td[14]/text()').extract_first() + tr.xpath('td[15]/text()').extract_first()
            else:
                ip = tr.xpath('td[14]/text()').extract_first()
            item['ip'] = Util.text2digit(ip, digit_type=float)
            item['h'] = Util.text2digit(tr.xpath('td[16]/text()').extract_first(), digit_type=int)
            item['hr'] = Util.text2digit(tr.xpath('td[17]/text()').extract_first(), digit_type=int)
            item['bb'] = Util.text2digit(tr.xpath('td[18]/text()').extract_first(), digit_type=int)
            item['ibb'] = Util.text2digit(tr.xpath('td[19]/text()').extract_first(), digit_type=int)
            item['hbp'] = Util.text2digit(tr.xpath('td[20]/text()').extract_first(), digit_type=int)
            item['so'] = Util.text2digit(tr.xpath('td[21]/text()').extract_first(), digit_type=int)
            item['wp'] = Util.text2digit(tr.xpath('td[22]/text()').extract_first(), digit_type=int)
            item['bk'] = Util.text2digit(tr.xpath('td[23]/text()').extract_first(), digit_type=int)
            item['r'] = Util.text2digit(tr.xpath('td[24]/text()').extract_first(), digit_type=int)
            item['er'] = Util.text2digit(tr.xpath('td[25]/text()').extract_first(), digit_type=int)
            item['era'] = Util.text2digit(tr.xpath('td[26]/text()').extract_first(), digit_type=float)
            yield item

    def _get_throw(self, text):
        """
        右投げもしくは左投げか
        :param text: テキスト
        :return: 右投げ or 左投げ
        """
        if text == '*':
            return THROW_LEFT
        return THROW_RIGHT

pipelines.py(データ保存)について

spiderが生成したItemをもらってデータとして保存する処理を記述しています.

SQLite3はPython標準で使えるので追加ライブラリは不要です.*5

実装のポイントは

  • 初期処理でTable生成の有無を判断
  • spiderの名前でInsert先のTableを振り分け

です.

本来的にはスキーマ定義(Create Table)は別のクラスに書くべきでしょうが,今回はサンプルとしての見通しを良くするためにあえて同じファイルに書いています.

# -*- coding: utf-8 -*-

# Define your item pipelines here
#
# Don't forget to add your pipeline to the ITEM_PIPELINES setting
# See: http://doc.scrapy.org/en/latest/topics/item-pipeline.html

import sqlite3
from scrapy.exceptions import DropItem


class BaseballPipeline(object):

    CREATE_TABLE_BATTER ="""
    CREATE TABLE batter (
      id integer primary key,
      year integer,
      name text ,
      team text ,
      bat text ,
      games integer ,
      pa integer ,
      ab integer ,
      r integer ,
      h integer ,
      double integer ,
      triple integer ,
      hr integer ,
      tb integer ,
      rbi integer ,
      so integer ,
      bb integer ,
      ibb integer ,
      hbp integer ,
      sh integer ,
      sf integer ,
      sb integer ,
      cs integer ,
      dp integer ,
      ba real ,
      slg real ,
      obp real,
      create_date date,
      update_date date
    ) 
    """

    CREATE_TABLE_PITCHER ="""
    CREATE TABLE pitcher (
      id integer primary key,
      year integer,
      name text ,
      team text ,
      throw text ,
      games integer ,
      w integer ,
      l integer ,
      sv integer ,
      hld integer ,
      hp integer ,
      cg integer ,
      sho integer ,
      non_bb integer ,
      w_per real ,
      bf integer ,
      ip real ,
      h integer ,
      hr integer ,
      bb integer ,
      ibb integer ,
      hbp integer ,
      so integer ,
      wp integer ,
      bk integer ,
      r integer ,
      er integer ,
      era real ,
      create_date date,
      update_date date
    ) 
    """

    INSERT_BATTER = """
    insert into batter(
    year, 
    name, 
    team, 
    bat, 
    games, 
    pa, 
    ab, 
    r, 
    h, 
    double, 
    triple, 
    hr, 
    tb, 
    rbi, 
    so, 
    bb, 
    ibb, 
    hbp, 
    sh, 
    sf,
    sb,
    cs,
    dp,
    ba,
    slg,
    obp,
    create_date,
    update_date
    ) 
    values(
    ?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?, 
    datetime('now', 'localtime'), 
    datetime('now', 'localtime')
    )
    """

    INSERT_PITCHER = """
    insert into pitcher(
    year, 
    name, 
    team, 
    throw, 
    games, 
    w, 
    l, 
    sv, 
    hld, 
    hp, 
    cg, 
    sho, 
    non_bb, 
    w_per, 
    bf, 
    ip, 
    h, 
    hr, 
    bb, 
    ibb, 
    hbp, 
    so,
    wp,
    bk,
    r,
    er,
    era,
    create_date,
    update_date
    ) 
    values(
    ?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?, 
    datetime('now', 'localtime'), 
    datetime('now', 'localtime')
    )
    """

    DATABASE_NAME = 'baseball.db'
    conn = None

    def __init__(self):
        """
        Tableの有無をチェック,無ければ作る
        """
        conn = sqlite3.connect(self.DATABASE_NAME)
        if conn.execute("select count(*) from sqlite_master where name='batter'").fetchone()[0] == 0:
            conn.execute(self.CREATE_TABLE_BATTER)
        if conn.execute("select count(*) from sqlite_master where name='pitcher'").fetchone()[0] == 0:
            conn.execute(self.CREATE_TABLE_PITCHER)
        conn.close()

    def open_spider(self, spider):
        """
        初期処理(DBを開く)
        :param spider: ScrapyのSpiderオブジェクト
        """
        self.conn = sqlite3.connect(self.DATABASE_NAME)

    def process_item(self, item, spider):
        """
        成績をSQLite3に保存
        :param item: Itemの名前
        :param spider: ScrapyのSpiderオブジェクト
        :return: Item
        """
        # Spiderの名前で投入先のテーブルを判断
        if spider.name == 'batter':
            # 打者成績
            self.conn.execute(self.INSERT_BATTER,(
                item['year'], item['name'], item['team'], item['bat'], item['games'], item['pa'], item['ab'], item['r'],
                item['h'], item['double'], item['triple'], item['hr'], item['tb'], item['rbi'], item['so'], item['bb'],
                item['ibb'], item['hbp'], item['sh'], item['sf'], item['sb'], item['cs'], item['dp'], item['ba'],
                item['slg'], item['obp'],
            ))
        elif spider.name == 'pitcher':
            # 投手成績
            self.conn.execute(self.INSERT_PITCHER,(
                item['year'], item['name'], item['team'], item['throw'], item['games'], item['w'], item['l'],
                item['sv'], item['hld'], item['hp'], item['cg'], item['sho'], item['non_bb'], item['w_per'], item['bf'],
                item['ip'], item['h'], item['hr'], item['bb'], item['ibb'], item['hbp'], item['so'], item['wp'],
                item['bk'], item['r'], item['er'], item['era'],
            ))
        else:
            raise DropItem('spider not found')
        self.conn.commit()
        return item

    def close_spider(self, spider):
        """
        終了処理(DBを閉じる)
        :param spider: ScrapyのSpiderオブジェクト
        """
        self.conn.close()

これからスクレイピングしてみよう!という方へ

まずは本やサンプルの写経がベストかなと思います.

今回紹介した野球スクレイピングについても自由にpullしたりダウンロードしたりforkしたりしてお試しで使って好きなサイトを(迷惑を書けない程度に)クローリングしてもらればと思います.

その上で,スクレイピングをやりたい方は是非,

  • Webの基本的なルールを学ぶ(HTML・Javascript・http・サーバーのしくみetc…)
  • スクレイピングの手段(今回はPythonとScrapy)をしっかり学んで自分の手足とする
  • スクレイピングする目的とゴールを明確に

という観点でぜひぜひ学んでみては如何でしょうか?

…という提案でこの件は一旦終わります.

次回予告

実際に取得した野球データで何が出来るか?を,

  • Jupyter
  • pandas
  • matplotlib

あたりでお料理してみたいと思います.

PythonユーザのためのJupyter[実践]入門

PythonユーザのためのJupyter[実践]入門

ついにこの本の出番かな…wkwk

*1:Scrapyの事をもっと知りたいニーズが多かったこと,Sprint Day(PyCon JP 2017四日目)のScrapyスプリントも盛り上がったので記事にすることにしました.

*2:後者「察し」のところは…まあね(察し)

*3:このブログを読んでる方はPythonか野球もしくはその両方に興味ある方多いので特段心配はしていない

*4:例えばデットボールは英語の正式名称「Hit By Pitch」の略称でhbpと呼びます.

*5:余談ですがガチの野球分析基盤はSQLAlchemyからMySQLを扱う方式にしています.