PythonのtkinterでJ1・J2・J3の順位表GUIアプリの制作とComboboxで順位表切り替え方法【Docker & Jupyterlab環境】

はじめに

いつの間にか始まっていたJリーグ。 以前スクレイピングを検索した時に、実例としてJリーグの順位表を取得していたので、それを真似てみようと思い立ったが、色々問題が起こったのでその解決とともに順位表アプリの制作方法を解説していく。

ここで分かること・出来ること

  • Docker Container内の日本語化
  • J1・J2・J3の順位表のスクレイピング
  • Comboboxでのイベント処理(表の切り替え)
  • CanvasにJPG画像表示
  • Treeviewで作成の表で、1行おきに行の色を設定


開発環境

Python、およびそのモジュールは、Docker内に環境を整備し、Jupyterlabで操作していく。

  • マシン:MacBook Pro
  • OS:macOS Big Sur(11.6.4)
  • 仮想環境:Docker Desktop(4.5.0 (74594))
  • エディタ:JupyterLab(3.3.0)、Sublime Text(Build 4126)
  • XQuartz:2.8.1
  • python:3.8.10
  • tkinter:8.6.10
  • requests:2.22.0
  • beautifulsoup4:4.10.0


階層構造

ファイルのディレクトリは以下の通りに予め設定。

desktop
  └python_webscrapingフォルダ
       ├Docker Composeファイル
       ├Dockerfile
       └workフォルダ
          └pythonファイル(jupyterlabで作成・編集)


Xquartzの導入と設定

上記に追加で、予めXquartzの導入と設定が必要になる。
導入および設定の方法は、下記エントリの『1 MacにXQuartzをインストール』および『2 XQuartzの環境設定を開く』をご覧あれ。

MacでPythonのGUIライブラリをDocker Container内のJupyterlabから稼働させる方法

DockerのContainerに構築したPython環境ではJupyterlabからTkinterが動きませんが、ある方法で動かすことが出来るようになります。その方法を丁寧に分かりやすく解説します。



Docker Desktopをインストール

Docker Desktopをインストールしていない場合は、下記ブログの1と2を参考にしていただければ。

MacにDockerをインストールしPythonが使えるまでのまとめ【初心者向け】

初心者がDockerをインストールするのはハードルが高いですよね。初心者である筆者がDockerのインストール方法を調べて試してみたのでその手順を公開します。用語については、理解しないままとりあえず放置します。理解しようと用語を調べだしたらそれに時間を取られてインストールに進めないので。



Dockerfileの内容

今回は、tkinterに日本語表示させるので、日本語表示用の設定を盛り込んでおり、それはコメントで明示している。

FROM ubuntu:latest

LABEL version="2.0" \
      maintainer="eszett design" \
      description="Webスクレイピング用"

# TZを設定する
ENV TZ Asia/Tokyo
RUN apt-get update \
    && apt-get install -y tzdata \
    && rm -rf /var/lib/apt/lists/* \
    && echo "${TZ}" > /etc/timezone \
    && rm /etc/localtime \
    && ln -s /usr/share/zoneinfo/Asia/Tokyo /etc/localtime \
    && dpkg-reconfigure -f noninteractive tzdata

# 日本語化
ENV LANG ja_JP.UTF-8
ENV LANGUAGE ja_JP:ja
ENV LC_ALL ja_JP.UTF-8
RUN apt-get update && apt-get install -y language-pack-ja-base language-pack-ja locales \
    && locale-gen ja_JP.UTF-8 \
    && apt-get clean \
    && rm -rf /var/lib/apt/lists/*

ENV DEBIAN_FRONTEND=noninteractive

RUN apt-get update && apt-get install -y \
        python3 \
        python3-pip \
        libx11-dev \
        python3-tk \
        fonts-migmix \
        dbus-x11 \
        fonts-noto-cjk \
        fcitx-mozc \
        firefox \
    && apt-get clean \
    && rm -rf /var/lib/apt/lists/* 

RUN pip3 install --upgrade pip \
    && pip3 install jupyterlab \
        pillow \
        requests \
        beautifulsoup4 \
        cchardet


Docker Composeの内容

version: "3.8"
services:
  web:
    build:
      context: .
      dockerfile: Dockerfile
    environment:
      - DISPLAY=host.docker.internal:0.0
    volumes:
      - ./work:/work
    ports:
      - 8030:8888
    image: python_webscr:latest
    container_name: python_webscr
    command:
      jupyter lab
        --ip=0.0.0.0
        --allow-root
        --notebook-dir='/work'
        --LabApp.token=''

Docker Container内のtkinterで日本語表示させるのに参考にしたサイトは以下の通り。

【Docker入門】日本語fontの表示方法-fonts-migmix-

ここでは「Docker」における日本語フォントの表示方法について解説しています。この記事の対象Dockerを初めて使用する人日本語フォントが化ける人これから日本語の環境構築をする人日本語fontの表示設定「fonts-migmix」のインス

Dockerコンテナを起動後、日本語が文字化けしてしまうなら

Dockerコンテナを起動後、テキスト入出力時に日本語の文字化けに遭遇したら、まず以下のコマンドで言語設定がどうなっているか確認する # locale すると、おそらく以下の画面のように、ほとんどの設定値が「POSIX」となっていると思う こうなっていると、日本語...

Dockerコンテナ内での日本語利用方法 | NagareLab

Dockerコンテナ環境内で日本語を利用したいケースは多々ある。 しかし、DockerHub上の各種公式イメージ等はイメージが肥大化しないよう、必要最小限の環境に留められている場合が多く、日本語が利用で出来ないケースが多い。 本ナレッジではコンテナ環境で日本語を使えるようにする手順を記載する。



Docker ImageとContainerの作成

以下のエントリに解説しているのでご覧あれ。

ImageとContainerの作成|【完成版】MacでJupyterlabからPythonのGUIライブラリが使えるDockerの設定方法

PythonのGUIライブラリ(Tkinter、pillow、openpyxl)をDocker Container内にインストールし、JupyterlabからXQuartzを介して稼働させる方法と、Docker ComposeとDockrfileの作成方法を紹介します。



完成したコード

コードの解説は、コード内にコメントしている。

# 標準モジュール
import tkinter as tk
from tkinter import ttk
import tkinter.font as font
# インストール必要なモジュール
from PIL import Image, ImageTk   # Pillow JPG画像を扱う時に必要
import requests   # Webサイトの情報取得や画像の収集用
from bs4 import BeautifulSoup   # HTMLやXMLファイルからデータを取得解析用
import cchardet   # 文字コード自動推定ライブラリ

# Tkクラス生成
Root = tk.Tk()
# 画面サイズ
Root.geometry("1920x1176")
# 画面タイトル
Root.title("Ranking Tables | J League")
# スタイル設定
Style = ttk.Style()
# 色指定
Orange = "#ff7e00"   # https://encycolorpedia.jp/ff7e00
Light_Orange = "#ff9e40"
Pale_Orange = "#ffead8"
Kon = "#1c2f56"   # https://encycolorpedia.jp/1c2f56
White = "#ffffff"


# Canvasの作成と配置
Canvas = tk.Canvas(Root, bg = Kon, width = 1920, height = 1176)
Canvas.place(x=0, y=0)

# PNG画像の読み込み時は以下のコード
#Spulse = tk.PhotoImage(file="2203-wp-ca-1920.png", master = Root)   # 「master = Root」が必須

# JPG画像の読み込み時は以下のコード
Spulse = Image.open("2203-wp-ca-1920.jpg")   # JPG画像の読み込み

# 画像の表示、「master = Root」が必須
Spulse = ImageTk.PhotoImage(Spulse, master = Root)

# 画像をCanvasに書き出し
Canvas.create_image(0, 0, image = Spulse, anchor = "nw")


# Combobox(プルダウンメニュー)作成
League = ["J1", "J2", "J3"]   # プルダウンの選択項目
Txt = tk.StringVar()   # Comboboxに表示中の文字を取得
Combo_Font = font.Font(Root, family = "Yu Gothic UI", size = 16 , weight = "bold")   # Comboboxの文字表示設定

Style.theme_use("clam")
Style.configure("office.TCombobox", padding = 5)   # Combobox内の上下余白設定

# Comboboxの色を設定(Comboboxの挙動ごとに色が変化するのを防止)
Style.map("office.TCombobox",
    # Comboboxのフィールド背景色
    fieldbackground=[
        ("readonly", "!focus", Pale_Orange),
        ("readonly", "focus", Pale_Orange)
        ],
    # Comboboxで他を選択した時の文字の背景色
    selectbackground=[
        ("readonly", "!focus", Pale_Orange),
        ("readonly", "focus", Pale_Orange)
        ],
    # Comboboxで他を選択した時の文字色
    selectforeground=[
        ("readonly", "!focus", Kon),
        ("readonly", "focus", Kon)
        ],
    # Comboboxの矢印ボタンの背景色
    background=[
        ("readonly", "!focus", Orange),
        ("readonly", "focus", Orange)
        ],
    # Comboboxの文字色
    foreground=[
        ("readonly", "!focus", Kon),
        ("readonly", "focus", Kon)
        ],
    # Comboboxの矢印ボタン色
    arrowcolor=[
        ("readonly", "!focus", White),
        ("readonly", "focus", White)
        ],
    # Comboboxの矢印ボタンの大きさ
    arrowsize=[
        ("readonly", "!focus", "18"),
        ("readonly", "focus", "18")
        ],
    # Comboboxの枠色
    bordercolor=[
        ("readonly", "!focus", White),
        ("readonly", "focus", White)
        ]
    )

# Comboboxのpopdown listbox(ドロップダウンリスト又はプルダウン)の選択時の色を設定
Root.option_add("*TCombobox*Listbox*selectBackground", Light_Orange)   # 背景色
Root.option_add("*TCombobox*Listbox*selectForeground", White)   # 文字色
# Comboboxのpopdown listbox(ドロップダウンリスト又はプルダウン)の文字色を設定
Root.option_add("*TCombobox*Listbox*foreground", Kon)   # 文字色


# Comboboxの生成
Combobox = ttk.Combobox(Root, textvariable = Txt, values = League, style = "office.TCombobox", state = "readonly", width = 5, justify = tk.CENTER, font = Combo_Font)
# Comboboxの配置
Combobox.place(x=925, y=15, width=70)
# Windowを開いた時「J1」が表示
Combobox.current(0)   # 「J2」にしたい場合は(1)に、「J3」は(2)にする


# 順位表の表示
def Display(*args):   # 引数を*argsにしておくと色々便利(イベント処理したい時用にeventと入れなくて済む、など)
    # URLの指定
    URL = "https://www.jleague.jp/standings/" + Combobox.get().lower() + "/"   # Combobox.get().lower()は、Comboboxに表示中の文字を取得し、小文字化している
    HTML = requests.get(URL)   # Webサイトの情報取得
    Soup = BeautifulSoup(HTML.content, "html.parser", from_encoding = cchardet.detect(HTML.content)["encoding"])   # HTMLからデータを取得。from_encodingは不要かもしれない
    
    # テーブルと行のHTMLタグを指定
    Rows = Soup.select("table." + Combobox.get() + "table tr")   # 全チーム分

    # クラブデータの取得とリスト化(ヘッダーも取得している)
    getRows = []   # 各リーグの全クラブデータのリスト
    for Row in Rows:
        Tmp = []   # 各クラブのリスト
        for Cell in Row.select("td, th"):
            if Cell.select("span"):   # エンブレムのタグにもクラブ名があり、クラブ名ダブって取得するのを防止
                Tmp.append(Cell.span.get_text().replace("\n", ""))   # Tmpに各クラブデータを格納しクラブ毎にリスト化。データ取得時に改行まで取得されるので、replaceで改行を削除
            else:   # クラブ名ダブりない場合
                Tmp.append(Cell.get_text().replace("\n", ""))
        del Tmp[0]   # 順位の前節から上下したかを表示する列を削除
        del Tmp[-1]   # 直近5試合列を削除
        getRows.append(Tmp)   # getRowsに各クラブのデータリストをまとめてリスト化

    # 表の作成
    Column = ("順位", "クラブ名", "勝点", "試合数", "勝", "分", "負", "得点", "失点", "得失点")   # 列の識別名を指定
    global Tree   # 別の場所でも使用するのでglobal化しておく
    Tree = ttk.Treeview(Root, columns=Column, height = len(getRows)-1)   # 表の設定
    Tree["show"] = "headings"   # ヘッダーが1つの列として表示されない用

    # 表のスタイル設定
    Style.configure("Treeview.Heading", font=("Yu Gothic UI", 12, "bold"), padding = (0,7,0,7), background = Orange, foreground = White, borderwidth = 0)   # ヘッダーのスタイル
    Style.configure("Treeview", font=("Yu Gothic UI", 14), rowheight = 40, foreground = Kon)   # 各クラブのスタイル

    # 列の設定(先に指定列の識別名を当てる)
    Width = 60   # 列幅(クラブ名以外)
    Tree.column("順位", anchor = "center", width = Width)
    Tree.column("クラブ名", anchor = "center", width = 300)
    Tree.column("勝点", anchor = "center", width = Width)
    Tree.column("試合数", anchor = "center", width = Width)
    Tree.column("勝", anchor = "center", width = Width)
    Tree.column("分", anchor = "center", width = Width)
    Tree.column("負", anchor = "center", width = Width)
    Tree.column("得点", anchor = "center", width = Width)
    Tree.column("失点", anchor = "center", width = Width)
    Tree.column("得失点", anchor = "center", width = Width)

    # レコードの追加とデータの格納
    j=0   # 1行おきの色指定用
    for i in range(1, len(getRows)):   # レコード数:0はヘッダー名なので飛ばしている
        Tree.insert("", "end", tags = j, values = getRows[i])   # クラブデータ格納
        # 1行おきに色指定用
        if j & 1:
            # tagが奇数(レコードは偶数)の場合のみ、背景色の設定
            Tree.tag_configure(j, background = Pale_Orange)
        j += 1
        # ヘッダー名取得・格納用
        for k in range(0, len(getRows[i])):   # 1行のセル数
            Tree.heading(k, text = getRows[0][k])   # ヘッダー名格納

    # 表の配置
    Tree.place(x=540, y= 70, width=840)

# Windowを開いた時のイベント
Display()   # Combobox.currentで予め指定したリーグの順位表が表示される

# Combobox(プルダウンメニュー)で他リーグを選択した時のイベント
Combobox.bind("<<ComboboxSelected>>", lambda event: Tree.destroy())   # 表示されている順位表を削除
Combobox.bind("<<ComboboxSelected>>", Display, "+")   # 新たに選択されたリーグの順位表を表示

Root.mainloop()


Jupyterlabを開く

Container作成後、ブラウザを開き、URL欄にlocalhost:8030と入力すると、Jupyterlabが開くので、それに上記コードを入力してShift+ReturnでGUIアプリが開く
詳細は、以下をご覧あれ。

Jupyterlabをブラウザで開きtkinterを稼働させる|【完成版】MacでJupyterlabからPythonのGUIライブラリが使えるDockerの設定方法<

PythonのGUIライブラリ(Tkinter、pillow、openpyxl)をDocker Container内にインストールし、JupyterlabからXQuartzを介して稼働させる方法と、Docker ComposeとDockrfileの作成方法を紹介します。



J1・J2・J3の順位表のスクレイピングとエラー

J1・J2・J3の順位表のスクレイピング方法は、以下のサイトを参考にした。

その際、「Some characters could not be decoded, and were replaced with REPLACEMENT CHARACTER.」というエラーが表示され、また参考サイトでも書かれておりその対策もあるのだが、どうしても解消されず仕舞いなのでスルーしておく

[Python入門]Beautiful Soup 4によるスクレイピングの基礎

Beautiful Soup 4を使って、urllib.request.urlopen関数などで取得したHTMLファイルから情報を抜き出す基本的な方法を見てみよう。

【Python】サッカーのランキング表をスクレイピングする | yamagablog

pythonのbeatifulsoupを使ってテーブルのデータを取得してみよう。すぐに使えるサンプルコード付き。

RequestsとBeautiful Soupでのスクレイピング時に文字化けを減らす - orangain flavor

多様なWebサイトからスクレイピングする際、Webサイトによっては文字化けが発生することがあります。 RequestsとBeautiful Soupを組み合わせる場合に、なるべく文字化けを減らす方法を解説します。 Beautiful Soupはパーサーを選択できますが、ここではhtml.parserに絞って解説します*1。 結論 以下の2点を守ると概ね幸せです。 Content-Typeヘッダーのエンコーディングを参照するコードは下の方に掲載しています。 1. Chardetをインストールしておく。 $ pip install chardet 2. RequestsのResponseオブジェク…



Comboboxで選択したら表が切り替わる

ComboboxでJ1からJ2、J2からJ3と、選択を変更したら該当するリーグの順位表が表示されるようにする方法は、選択を変更したら、

  • 変更したら、表示中の順位表を削除する
  • 削除後、該当するリーグの順位表を表示する

という2つのイベント処理を実装する。

Combobox(プルダウンメニュー)には、選択変更時のイベントとして、「<<ComboboxSelected>>」があるので、bindを使用してその2つの処理を以下の通りにした。
Combobox.bind('<<ComboboxSelected>>', lambda event: Tree.destroy())   # 表示されている順位表を削除
Combobox.bind('<<ComboboxSelected>>', Display, "+")   # 新たに選択されたリーグの順位表を表示

lambda関数でdestroy()を指定して表示中の順位表を削除し、続けて、定義したDisplay()で順位表を作成表示する。

2つのbindを紐付けるには、2つめに「"+"」を追加することで実現できる。

bindのオプションについての参考サイトは以下の通り。

【Tkinter】bindメソッドによるトリガーイベントとコールバック関数の紐づけ

GUIアプリケーションではクリックやキー入力といったユーザーの操作(イベント)をトリガーにして何らかの処理(アクション)を行います。その紐づけを行うためにTkinterに用意されているのがbindメソッドです。

【コード付】Tkinterで使われるbindとは?bindの仕組みを交えて解説 | 「モノづくりから始まるエンジニア」

今回はTkinterで使われるbindに関して、bindの仕組みを交えて徹底解説いたします。なんとなくbindを使っていた、bindの仕組みを理解して、知見を深めたい方へおすすめです。ぜひ最後までご一読ください。



CanvasにJPG形式の画像を表示するには

tkinterは、PNGには対応しているが、JPGには対応していない

が、Pillowモジュールを追加することで、JPGに対応可能となる。

JPG画像の読み込み時は以下のコードになる。

Spulse = Image.open("2203-wp-ca-1920.jpg")

以下のコードも紹介されている。

Spulse = Image.open(open("2203-wp-ca-1920.jpg", "rb"))   # mode = "r"は「読み込み」、"b"は「バイナリ」、つまりバイナリデータの読み込み

因みに、PNG画像の読み込み時は以下のコードになる。

Spulse = tk.PhotoImage(file="2203-wp-ca-1920.png", master = Root)   # 「master = Root」が必須

Canvasに画像表示

(備忘録)[Python + tkinter] tk.PhotoImage でエラーが出る - Qiita

備忘録。下記ソースのtk.PhotoImageで画像を読み込む際、image "pyimage2" doesn't existというエラーが出た。調べた結果、tk.PhotoImageの引数としてmasterを指定する必要があって、その画像を表示するwidgetが格納される、rootなりframeなりを master=root のように明示すると解決した。


JPG画像をCanvasに表示

tkinterで画像を表示させる

tkinterで画像を表示させる今回の記事は、「tkinterでpngやjpgなどのgif以外の画像を表示させる方法」です。結論から言います。解決方法は、PILのImageとImageTkを使うことです。それでは、画像を表示させていきましょ


JPG画像をCanvasに表示(よりシンプルなコード)

【Python/tkinter】Canvasに画像を表示する

まず、Canvasを作成し、画像ファイルを開き、Canvasに画像を表示するサンプルは以下のようになります。import tkinter as tkfrom PIL import ImageTkclass Application(tk.Fr



Treeviewで作成の表で、1行おきに行の色を設定

以下のサイトを参考に、丁度表の各行(各クラブデータ)を格納する時に色を指示するタグも付属出来る箇所に組み込んだ。

⑨ 表(テーブル)の作成【python tkinter sqlite3で家計簿を作る】 - memopy

今回は表示画面のGUI作成を行う。始めに、表(テーブル)の作成だ。 表(テーブル)の作成は、ttkモジュールのツリービュー(Treeview)ウィジェットを用いる。ツリービュー Treeview はじめに簡単なサンプルスクリプトを作成して、ウィジェットの機能を確認する。



Comboboxと表の色変更

表の色変更は、全くややこしくない。

ところが、Comboboxの色変更はちょっとややこしいことになっている。

Comboboxと表の色変更は、以下にて解説しているのでご覧あれ。

【Python・tkinter】Comboboxのフィールドとドロップダウンリストの色の指定・変更方法

PythonのtkinterでJリーグの順位表をスクレイピングして表示させるGUIアプリを制作してみた結果、Comboboxと表の色変更方法が判明したのでその報告。



完成と動作動画

ということで完成したアプリはこんな感じ。

Combobox(プルダウンメニュー)を変更して順位表が切り替わる動作はこんな感じ。

画像は、清水エスパルス公式サイトにて配布している壁紙を使用している。深謝。

ファン ・グッズ | 壁紙ギャラリー | 清水エスパルス公式WEBサイト

最新ニュース、試合速報・結果、スタジアム観戦ガイド、選手ブログ、選手インタビュー、ユース・Jrユース情報など、清水エスパルスに関する情報を提供していきます。 | ファン ・グッズ | 壁紙ギャラリー | 清水エスパルス公式WEBサイト

清水エスパルス推しである。













コメント

よく読まれている記事

CSSボタンでテキストを天地中央に揃えるとき、なぜボタン高と行高を一緒にするのか

FullCalendarの導入からカレンダー毎の色指定まで

FacebookページのフィードURLを取得しウォールを自サイトに表示