madokaのブログ

勉強したことのoutput先として使ってます。内容はpythonがらみが多いかもです。

Pythonをつかってドラゴンズの試合結果を取得する

ドラゴンズの試合状況をサクッと取得できるようにしたいということで、野球の試合状況について、試合の状況(1回表、試合前)や対戦相手、得点状況を取得するプログラムを組みました。

今回は下記のyahooスポーツから情報を取得することにしました。 baseball.yahoo.co.jp

必要なもの

  • python3.x
  • beautifulsoup4 (こちらは下記のコマンドで入れて下さい。)
$ pip install beautifulsoup4

BeatuifulSoupの使い方

from bs4 import BeautifulSoup


# web pageから情報を取ってくる
soup = BeautifulSoup(urlopen(url), "html.parser")

# fileから情報を取ってくる
with open("sample.html", "r") as f:
     soup = BeautifulSoup(f, "html.parser")

上記の処理にて対象のhtmlを操作する準備は整ったので、これを使って探索を行います。html.parserのところはparserを指定しています。lxmlの指定もできます。こちらの方が早いらしいです。

BeatuifulSoupのfind, find_all

soup.find('タグ名',id='id名',class_='クラス名',href='url',...)
# href='url'は属性の一例です

大体のものはこのfind、find_allで取って来れます。上記では引数を色々書きましたが、この中のどれかを引数にいれれば動きます。このfindで拾ってきたものについても、最初のBeatifulSoupで作ったもの同様にfind等を用いてその内部を取得することができます。


ここから下は実際にこのサイトのhtmlからどう情報を拾ってくるかの話になります。

yahoo スポーツ野球のurlについて

このsiteではhttps://baseball.yahoo.co.jp/npb/teams/team番号/scheduleというurlになっており、チーム番号を指定することで対象のチームに関する今月のスケジュールが見れるようになっています。

野球チームに関しては下記のようにナンバリングされてました。

{'巨人': 1, 
 'ヤクルト': 2,
 'ロッテ': 9,
 '日本ハム': 8,
 '阪神': 5,
 '中日': 4,
 '西武': 7,
 'オリックス': 11,
 '楽天': 376,
 '広島': 6,
 'ソフトバンク': 12,
 'DeNA': 3}

また、年月についての指定は、上記のurlにクエリをのせることで指定ができました。 https://baseball.yahoo.co.jp/npb/teams/team番号/schedule?date=201905 このように年月を西暦の4桁と月の2桁からなる6桁の数字をdateに入れるようになってました。

今回扱うhtmlについて

全体のhtmlについては長すぎるので、 スポーツナビ - プロ野球 -中日ドラゴンズ - 日程・結果 からDeveloperモードで見て下さい。Macだと option + command + iで出てきます。ここでは対象の日付の情報が書かれた要素だけ載せます。

<td class="gameday home active">
    <table>
        <tbody>
        <tr>
            <td class="game">
                <a href="https://baseball.yahoo.co.jp/npb/game/2019042703/" data-ylk="slk:result;pos:27"
                   data-rapid_p="46">
                    <em class="day">27</em>
                    <dl>
                        <dt>
                            vs.<span class="team pm T"><i>阪神</i></span>
                        </dt>
                        <dd class="yjMS">
                            <span class="score">
                            <strong>5</strong> - 4
                            </span>
                            <span class="status">
                            結果
                            </span>
                        </dd>
                    </dl>
                </a>
            </td>
        </tr>
        <tr>
            <td class="data"><p class="yjXS">ナゴヤドーム</p>
                <ul class="yjSt">
                    <li>勝:<a href="/npb/player/1300063/" data-ylk="slk:win_pit;pos:27" data-rapid_p="47">又吉</a></li>
                    <li>S:<a href="/npb/player/1700101/" data-ylk="slk:sav_pit;pos:27" data-rapid_p="48">鈴木博</a></li>
                </ul>
            </td>
        </tr>
        </tbody>
    </table>
</td>

対象の日付の情報が書かれた要素を取り出すために

<td>で一日ずつの内容が書かれているのでまずはtdの要素一覧を取得します。 そして今日の日付(27日)がかかれた要素<em>の内部のtextと一致するかどうかを見て、一致すればその要素が今日の要素であると判断します。なので、ここでは<td>をfind_allで全件取得をしそれぞれの要素の<em>をfindで取り出して、findの中身が対象の日付であるかどうかをcheckして取り出すようにしました。

from urllib.parse import urlencode
from bs4 import BeautifulSoup


def __get_date_elements(self, _date):
        tds = BeautifulSoup(urlopen(self.url), "html.parser").find_all("td")
        for td in tds:
            em = td.find("em")
            if em is not None:
                if em.text == _date.strftime("%d"):
                    return td

試合があるかどうか

tdのclassにgamedayとあれば試合がある日なようです。一方試合がない日はfillinとなっていました。

def __have_game(self, _date):
    element = self.__get_date_elements(_date)

    return element is not None and 'gameday' in element.get("class") \
            and not self.__get_status(_date) == GameStatus.CALLED_OFF

試合の進行状況

id=yjMSclassを持つ要素中の<span class="status">内に書いてありました。 試合前、中止、結果 の他に1回表、1回裏、、、が入るようです。

def __get_status(self, _date):
    element = self.__get_date_elements(_date)
    span = element.find("span", class_="status")
    if span:
       return span.text.strip()

対戦相手

webページから見ると球団名の記述がないように見えますが、ちょうど球団の画像がある部分に<span class="team">の部分に球団名が入っていたのでそこから取り出します。

def __get_battle_team(self, _date):
    element = self.__get_date_elements(_date)
    if element:
        span = element.find("span", class_="team")
        if span:
            return span.text

得点

得点はid=yjMS<span class=score>から取り出します。

def __get_scores(self, _date):
    pattern = "[0-9]+"

    element = self.__get_date_elements(_date)
    if element:
        span = element.find("span", class_="score")
        if span.text:
            score = re.findall(pattern, span.text)
            return int(score[0]), int(score[1])
    return 0, 0

ニュース

試合がある日にはその日の試合状況に応じたニュース記事がでてきます。あります。リンクがあればそれがそのニュース記事へリンクです。とりあえずaで取得します。

def __get_news_page(self, _date):
        element = self.__get_date_elements(_date)
        if element:
            a = element.find("a")
            if a:
                url = a.get("href")
                return url

このurlからsoupをつくって、ニュースの文章を取り出します。このidがyjSNLiveBattlereviewのところにあります。

def __get_news_text(self, _date):
        news_elem = BeautifulSoup(urlopen(self.__get_news_page(_date)), "html.parser")
        if news_elem:
            div = news_elem.find("div", id="yjSNLiveBattlereview")
            if div:
                return div.text

こんな感じで、それぞれの試合情報を取ってくることができます。 これらをまとめたものが下記になります。

import re
from urllib.parse import urlencode
from urllib.request import urlopen

from bs4 import BeautifulSoup


class BaseballNewsFactory:
    def __init__(self, ):
        self.url = "https://baseball.yahoo.co.jp/npb/teams/4/schedule"
        # self.__news_picker = news_picker

    def create_news(self, _date=None):
        _date = datetime.now() if _date is None else _date

        news = BaseballNews()
        news.my_team = "中日"
        news.date = _date
        news.status = self.__get_status(_date)

        if news.have_game():
            news.news_text = self.__get_news_text(_date)
            news.my_team_point, news.battle_team_point = self.__get_scores(_date)
            news.battle_team = self.__get_battle_team(_date)
        return news

    def __get_date_elements(self, _date):
        year_month = _date.strftime('%Y%m')
        elements = BeautifulSoup(urlopen("{}?{}".format(self.url, urlencode({"date": year_month}))))
        tds = elements.find_all("td")
        for td in tds:
            em = td.find("em")
            if em is not None:
                if em.text == _date.strftime("%d"):
                    return td

    def __get_status(self, _date):
        element = self.__get_date_elements(_date)
        if 'gameday' in element.get("class"):
            span = element.find("span", class_="status")
            if span:
                return span.text.strip()
        else:
            return "試合なし"

    def __get_scores(self, _date):
        pattern = "[0-9]+"

        element = self.__get_date_elements(_date)
        if element:
            span = element.find("span", class_="score")
            if span.text:
                score = re.findall(pattern, span.text)
                return int(score[0]), int(score[1])
        return 0, 0

    def __get_news_page(self, _date):
        element = self.__get_date_elements(_date)
        if element:
            a = element.find("a")
            if a:
                url = a.get("href")
                if url:
                    return BeautifulSoup(urlopen(url))

    def __get_news_text(self, _date):
        news_elem = self.__get_news_page(_date)
        if news_elem:
            div = news_elem.find("div", id="yjSNLiveBattlereview")
            if div:
                return div.text

    def __get_battle_team(self, _date):
        element = self.__get_date_elements(_date)
        if element:
            span = element.find("span", class_="team")
            if span:
                return span.text


from datetime import datetime


class BaseballNews(object):
    def __init__(self):
        self.__my_team = ""
        self.__battle_team = ""
        self.__my_team_point = 0
        self.__battle_team_point = 0
        self.__status = ""
        self.__news_text = ""
        self.__date = None

    @property
    def my_team(self):
        return self.__my_team

    @my_team.setter
    def my_team(self, value):
        self.__my_team = value

    @property
    def battle_team(self):
        return self.__battle_team

    @battle_team.setter
    def battle_team(self, value):
        self.__battle_team = value

    @property
    def my_team_point(self):
        return self.__my_team_point

    @my_team_point.setter
    def my_team_point(self, value):
        if isinstance(value, int):
            self.__my_team_point = value

    @property
    def battle_team_point(self):
        return self.__battle_team_point

    @battle_team_point.setter
    def battle_team_point(self, value):
        if isinstance(value, int):
            self.__battle_team_point = value

    @property
    def status(self):
        return self.__status

    @status.setter
    def status(self, value):
        if isinstance(value, str):
            self.__status = value

    @property
    def news_text(self):
        return self.__news_text

    @news_text.setter
    def news_text(self, value):
        if isinstance(value, str):
            self.__news_text = value

    @property
    def date(self):
        return self.__date

    @date.setter
    def date(self, value):
        if isinstance(value, datetime):
            self.__date = value

    def issue(self):
        return self.my_team_point > self.battle_team_point

    def have_game(self):
        return not (self.status == "試合なし"
                    or self.status == "中止")


if __name__ == '__main__':
    factory = BaseballNewsFactory()
    news = factory.create_news()
    print(news.date)
    print("{} vs {}".format(news.my_team, news.battle_team))
    print(news.status)
    print("{} - {}".format(news.my_team_point, news.battle_team_point))
    print(news.news_text)

これを実行すること、こんな出力が得られます。

2019-05-11 19:41:44.993285
中日 vs 阪神
結果
5 - 1

5月11日(土)阪神 vs. 中日 8回戦
中日は初回、高橋と阿部の連続適時打で3点を先制する。その後は8回表に、高橋の適時打で加点すると、9回には福田のソロが飛び出し、リードを広げた。投げては、先発・柳が8回5安打無失点の快投で今季3勝目。敗れた阪神は、打線が7安打1得点とつながりを欠いた。

勝ってる!!柳!!!

まとめ

今回のコードを使ってalexaちゃんでスキルを作ったりしました。ソースコードは下記になります。

github.com

対象の日付について書かれた要素を取り出せば一度の取得でいろいろ取り出せると思いましたが、一回読み出すとclassの情報が抜けてしまい取り出せませんでした。とりあえず毎度アクセスで対処しました。原因は不明です。。。