MENU

【Python】競馬情報のスクレイピング GUIの作成

はじめに

競馬のデータ分析をテーマに、オッズやレース結果、騎手の成績などの情報を効率的に取得・活用することを目的としCustomTkinterを活用したGUIアプリを作成しました。将来的に機械学習の実装をしたいと考えておりますが、まだその実装はできておりません。ここではGUIに焦点を絞りソースコードを紹介し、簡易な解説を行います。

目次

GUI_main.py

このスクリプトは、Pythonのモジュールを利用してGUIアプリケーションを起動するエントリーポイントとなっています。

ソースコード

from Libs.GUI import GUI_main

if __name__ == "__main__":
    GUI_main.run_GUI()

header.py

ソースコード

import customtkinter as ctk


class Header(ctk.CTkFrame):
    def __init__(self, App):
        super().__init__(App)
        self.menu = ctk.CTkButton(
            self,
            font=App.font_set,
            text="≡",
            width=25,
            height=25,
            command=self.call_back,
        )
        self.menu.grid(row=0, column=0, padx=5, pady=5)
        self.title = ctk.CTkLabel(self, font=App.font_set, text="")
        self.title.grid(row=0, column=1, padx=5, pady=5)
        self.grid_rowconfigure(0, weight=1)
        self.grid_columnconfigure(1, weight=1)

    # menuフレームの表示、非表示を制御するメソッド
    def call_back(self):
        self.master.menu_bool = not self.master.menu_bool
        if self.master.menu_bool:
            self.master.menu.grid(row=1, column=0, padx=5, pady=5, sticky="nsew")
            self.master.active_frame.grid(
                row=1, column=1, padx=5, pady=5, sticky="nsew"
            )
            self.master.grid_columnconfigure(0, weight=0)
            self.master.grid_columnconfigure(1, weight=1)
        else:
            self.master.menu.grid_forget()
            self.master.active_frame.grid(
                row=1, column=0, padx=5, pady=5, sticky="nsew", columnspan=2
            )
            self.master.grid_columnconfigure(0, weight=1

Header クラスの概要

  • Header クラスは customtkinter.CTkFrame を継承しており、アプリのヘッダー部分を管理します。
  • menu ボタンをクリックすると call_back() メソッドが実行され、メニューの表示・非表示を切り替えます。
  • title ラベルはアプリのタイトルを表示するための要素です。

call_back() メソッドの動作

  • self.master.menu_bool の状態を切り替えることで、メニューの表示・非表示を管理します。
  • menu.grid()menu.grid_forget() を適切に使用して、メニューのレイアウトを変更します。
  • grid_columnconfigure() を活用し、レイアウトの動的な調整を行います。

menu.py

ソースコード

from functools import partial

import customtkinter as ctk


class Menu(ctk.CTkFrame):
    def __init__(self, App):
        super().__init__(App)
        self.func_buttons = []
        self.create_buttons(App)

    def create_buttons(self, App):
        # ボタンを生成し、グリッドに配置
        for i, name in enumerate(App.menu_buttons.keys()):
            button = ctk.CTkButton(
                self,
                corner_radius=0,
                height=38,
                border_spacing=6,
                text=name,
                fg_color="transparent",
                text_color="gray90",
                hover_color="gray30",
                anchor="w",
                font=App.font_set,
                command=partial(self.call_back, name),
            )
            button.grid(row=i + 1, column=0, sticky="ew")
            self.func_buttons.append(button)

    def call_back(self, name):
        # ボタンの色とアクティブフレームの切り替え
        for button in self.func_buttons:
            button.configure(
                fg_color="gray25" if button._text == name else "transparent"
            )

        # フレームとヘッダーの更新
        self.master.header.title.configure(text=name)
        self.master.active_frame.grid_forget()
        self.master.active_frame = getattr(
            self.master, self.master.menu_buttons[name][0]
        )
        self.master.active_frame.grid(row=1, column=1, padx=5, pady=5, sticky="nsew")

Menu クラスの概要

  • Menu クラスは customtkinter.CTkFrame を継承しており、アプリのメニュー部分を管理します。
  • create_buttons() メソッドで、アプリに必要なメニューボタンを動的に生成します。
  • call_back() メソッドで、ボタンの状態やフレームの切り替えを行います。

create_buttons() メソッドの動作

  • App.menu_buttons のキーを取得し、それぞれに対して CTkButton を作成。
  • command 引数に partial(self.call_back, name) を渡し、ボタンが押されたときに call_back() メソッドが実行されるように設定。
  • すべてのボタンを self.func_buttons に保存し、後でアクセスできるようにします。

call_back() メソッドの動作

  • クリックされたボタンの色を変更し、現在アクティブなフレームを self.master.active_frame に切り替え。
  • self.master.header.title を更新して、選択されたメニュー名をヘッダーに表示。
  • grid_forget() を使い、前のアクティブフレームを非表示にして新しいフレームを表示。

msgbox.py

ソースコード

import tkinter as tk

import customtkinter as ctk


class MsgBox(ctk.CTkFrame):
    def __init__(self, parent):
        super().__init__(parent)
        self.font_set = parent.font_set
        self.create_widgets()

    def create_widgets(self):
        # タイトルラベルの設定
        self.title = ctk.CTkLabel(self, font=self.font_set, text="出力")
        self.title.grid(row=0, column=0, padx=5, pady=5, sticky="w")

        # テキストボックスの設定
        self.msgbox = ctk.CTkTextbox(self, font=self.font_set)
        self.msgbox.grid(row=1, column=0, padx=5, pady=5, sticky="nsew")

        # グリッドの行・列の設定
        self.grid_rowconfigure(1, weight=1)
        self.grid_columnconfigure(0, weight=1)

    def print(self, text, line_brake=True):
        """テキストボックスにメッセージを出力します。"""
        if line_brake:
            self.msgbox.insert(tk.END, f"{text}\n")
        else:
            self.msgbox.insert(tk.END, f"{text}")
        self.msgbox.update()

MsgBox クラスの概要

  • MsgBox クラスは customtkinter.CTkFrame を継承しており、メッセージの出力領域を管理します。
  • create_widgets() メソッドで、ラベルとテキストボックスを作成し、レイアウトを設定します。
  • print() メソッドを使用して、外部からメッセージをテキストボックスに出力できます。

create_widgets() メソッドの動作

  • CTkLabel を用いてタイトルを表示。
  • CTkTextbox を作成し、メッセージを表示するエリアを確保。
  • grid_rowconfigure()grid_columnconfigure() でレイアウトの柔軟性を確保。

print() メソッドの動作

  • text をテキストボックスに挿入。
  • line_brake=True の場合は、改行を追加。
  • msgbox.update() で即座に変更を反映。

preparing.py

ソースコード

import customtkinter as ctk

from ...Preparing.date_scraper import date_scraper
from ...Preparing.horse_html_fetcher import horse_html_fetcher
from ...Preparing.horse_parser import horse_parser
from ...Preparing.race_html_fetcher import race_html_fetcher
from ...Preparing.race_html_parser import race_html_parser
from ...Preparing.race_id_loader import load_race_ids
from ...Preparing.race_id_scraper import RaceIdScraper
from ...Preparing.range_loader import range_loader
from .msgbox import MsgBox
from .set_range import SetRange


class Preparing(ctk.CTkFrame):
    def __init__(self, App):
        super().__init__(App)
        self.font_set = App.font_set
        self.initialize_components()

    def initialize_components(self):
        """GUIコンポーネントの初期化"""
        # メッセージボックスとプログレスバー
        self.msgbox = ProgressbarMsgbox(self)
        self.progressbar = self.msgbox.progressbar
        self.msgbox.grid(row=3, column=0, padx=5, pady=5, sticky="nsew")

        # レース結果取得用フレーム
        self.race_result = RaceResultScraper(self)
        self.race_result.grid(row=0, column=0, padx=5, pady=5, sticky="ew")

        # 馬情報取得用フレーム
        self.horse = HorseScraper(self)
        self.horse.grid(row=1, column=0, padx=5, pady=5, sticky="ew")

        # レイアウトの設定
        self.grid_rowconfigure(3, weight=1)
        self.grid_columnconfigure(0, weight=1)


class BaseScraper(ctk.CTkFrame):
    def __init__(self, parent, title_text, run_callback):
        super().__init__(parent)
        self.font_set = parent.font_set
        self.create_widgets(title_text, run_callback)

    def create_widgets(self, title_text, run_callback):
        """ウィジェットの作成"""
        # タイトル
        self.title = ctk.CTkLabel(self, font=self.font_set, text=title_text)
        self.title.grid(row=0, column=0, padx=5, pady=5, sticky="w")

        # 上書きチェックボックス
        self.overwrite = ctk.CTkCheckBox(self, text="Overwrite", font=self.font_set)
        self.overwrite.deselect()
        self.overwrite.grid(row=0, column=1, padx=5, pady=5, sticky="ew")

        # 実行ボタン
        self.run = ctk.CTkButton(
            self,
            text="実行",
            font=self.font_set,
            command=lambda: self.pre_run(run_callback),
        )
        self.run.grid(row=1, column=1, padx=5, pady=5, sticky="ew")

        # 入力フレーム
        self.set_range = SetRange(self)
        self.set_range.configure(fg_color="transparent")
        self.set_range.grid(row=1, column=0, padx=5, pady=5, sticky="ew")

        # レイアウト設定
        self.grid_columnconfigure(0, weight=1)

    def pre_run(self, run_callback):
        """共通の前処理(overwrite と ym_list の取得)を行った後に実行"""
        overwrite = self.overwrite.get()

        y1, m1 = self.set_range.y1.get(), self.set_range.m1.get()
        y2, m2 = self.set_range.y2.get(), self.set_range.m2.get()

        # データ取得
        ym_list = range_loader(y1, m1, y2, m2, self.master)

        # `ym_list` が `None` の場合、処理を中断
        if ym_list is None:
            return

        # サブクラスの処理を呼び出す
        run_callback(overwrite, ym_list)


class RaceResultScraper(BaseScraper):
    def __init__(self, parent):
        super().__init__(parent, "レース結果の取得・更新", self.run_callback)

    def run_callback(self, overwrite, ym_list):
        """レース結果の取得・更新を行う"""
        # レース開催日取得
        date_list = date_scraper(ym_list, self.master)

        # レースID取得
        race_id_scraper = RaceIdScraper(self.master, overwrite)
        race_id_scraper.scrape_race_id(date_list)

        # GUIの更新を待つ(フリーズ防止)
        self.master.update_idletasks()

        # レース結果HTMLの取得
        race_html_fetcher(ym_list, self.master, overwrite)
        race_html_parser(ym_list, self.master, overwrite)


class HorseScraper(BaseScraper):
    def __init__(self, parent):
        super().__init__(parent, "馬情報の取得・更新", self.run_callback)

    def run_callback(self, overwrite, ym_list):
        race_id_list = load_race_ids(ym_list, self.master)
        if not race_id_list:
            return
        horse_id_list = horse_html_fetcher(race_id_list, self.master, overwrite)
        horse_parser(horse_id_list, self.master, overwrite)


class ProgressbarMsgbox(MsgBox):
    def __init__(self, parent):
        super().__init__(parent)
        self.initialize_widgets()

    def initialize_widgets(self):
        """プログレスバーとメッセージボックスの初期化"""
        self.progressbar = ctk.CTkProgressBar(self, orientation="horizontal")
        self.progressbar.set(0)
        self.progressbar.grid(row=0, column=1, padx=5, sticky="ew")

        self.title.grid(row=0, column=0, padx=5, pady=5, sticky="w")
        self.msgbox.grid(row=1, column=0, padx=5, pady=5, sticky="nsew", columnspan=2)

        # レイアウト設定
        self.grid_rowconfigure(1, weight=1)
        self.grid_columnconfigure(1, weight=1)

Preparing クラスの概要

  • Preparing クラスは customtkinter.CTkFrame を継承しており、競馬データ取得の主要な処理を統括します。
  • initialize_components() メソッドで、各種データ取得フレームやメッセージボックスを設定します。

BaseScraper クラスの概要

  • BaseScraper クラスは、共通のデータ取得ロジックを提供するベースクラスです。
  • create_widgets() メソッドを用いて、タイトル、チェックボックス、実行ボタンなどのGUIコンポーネントを作成。
  • pre_run() メソッドで、処理の前処理として日付範囲の取得を行い、サブクラスで定義された run_callback() を呼び出します。

RaceResultScraper クラスの概要

  • RaceResultScraperBaseScraper を継承し、レース結果の取得と更新を管理します。
  • run_callback() メソッドで、レース開催日やレースIDを取得し、結果の解析を行います。

HorseScraper クラスの概要

  • HorseScraperBaseScraper を継承し、馬の情報取得と解析を行います。
  • run_callback() メソッドで、レースIDの読み込み、馬情報の取得、データ解析を実行します。

ProgressbarMsgbox クラスの概要

  • ProgressbarMsgboxMsgBox クラスを継承し、進捗状況を表示するプログレスバーを追加したメッセージボックスを提供します。
  • initialize_widgets() メソッドで、プログレスバーとメッセージボックスをレイアウトし、ユーザーに処理の進行状況を可視化します。

set_range.py

ソースコード

import datetime

import customtkinter as ctk


class SetRange(ctk.CTkFrame):
    def __init__(self, parent):
        super().__init__(parent)
        self.font_set = ("meiryo", 14)
        self.dt_now = datetime.datetime.now()
        self.year_list = [str(i) for i in range(2008, self.dt_now.year + 1)]
        self.month_list = [str(i) for i in range(1, 13)]

        # インスタンス変数としてコンポーネントを定義
        self.from_label = ctk.CTkLabel(self, text="From", font=self.font_set)
        self.y1 = ctk.CTkComboBox(self, values=self.year_list, font=self.font_set)
        self.y1_label = ctk.CTkLabel(self, text="年", font=self.font_set)
        self.m1 = ctk.CTkComboBox(self, values=self.month_list, font=self.font_set)
        self.m1_label = ctk.CTkLabel(self, text="月", font=self.font_set)
        self.spacer = ctk.CTkLabel(self, text=" ", font=self.font_set)
        self.to_label = ctk.CTkLabel(self, text="To", font=self.font_set)
        self.y2 = ctk.CTkComboBox(self, values=self.year_list, font=self.font_set)
        self.y2_label = ctk.CTkLabel(self, text="年", font=self.font_set)
        self.m2 = ctk.CTkComboBox(self, values=self.month_list, font=self.font_set)
        self.m2_label = ctk.CTkLabel(self, text="月", font=self.font_set)

        # コンポーネントの配置
        self.from_label.grid(row=0, column=0, padx=5, pady=5, sticky="ew")
        self.y1.grid(row=0, column=1, padx=5, pady=5, sticky="ew")
        self.y1_label.grid(row=0, column=2, padx=5, pady=5, sticky="ew")
        self.m1.grid(row=0, column=3, padx=5, pady=5, sticky="ew")
        self.m1_label.grid(row=0, column=4, padx=5, pady=5, sticky="ew")
        self.spacer.grid(row=0, column=5, padx=5, pady=5, sticky="ew")
        self.to_label.grid(row=0, column=6, padx=5, pady=5, sticky="ew")
        self.y2.grid(row=0, column=7, padx=5, pady=5, sticky="ew")
        self.y2_label.grid(row=0, column=8, padx=5, pady=5, sticky="ew")
        self.m2.grid(row=0, column=9, padx=5, pady=5, sticky="ew")
        self.m2_label.grid(row=0, column=10, padx=5, pady=5, sticky="ew")

        # 列幅の設定
        for col in [1, 3, 7, 9]:
            self.grid_columnconfigure(col, weight=1)

SetRange クラスの概要

  • SetRange クラスは customtkinter.CTkFrame を継承しており、日付範囲の選択を可能にするウィジェットを提供します。
  • year_listmonth_list を生成し、過去2008年から現在までの年、および1~12月の月を選択できるようにします。
  • CTkComboBox を使用して年と月の選択を実装します。

create_widgets() メソッドの動作

  • CTkLabel を用いて「From」と「To」のラベルを配置。
  • CTkComboBox を用いて、開始年・月 (y1, m1) と終了年・月 (y2, m2) を選択可能に。
  • grid() を用いて、ウィジェットを適切にレイアウト。
  • grid_columnconfigure() を使用し、年と月の選択部分に対してレスポンシブなデザインを適用。
よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

コメント

コメントする

目次