🐭

【ポケカ】pythonでシティリーグ一括申込ツールを作る!

に公開

ポケカやってて地味にめんどくさい事 第1位〜〜〜!

シティリーグの応募.....

1件しか応募しないわけにいかないし、
かといって何回も同じ動作を繰り返すのも億劫なんです。
で、後回しにして期限ギリギリに急いで応募する羽目になるんですよね...

「私の誰か代わりに応募しといてくれ!!」の気持ちを形にしたのが
今回作成したシティリーグ自動申込ツールです。

これで応募し忘れや、応募漏れも言い訳できなくなりますねェ...

既に出来上がったものがこちら↓↓
ぜひ使ってみてください〜!
https://github.com/meeeee05/pokeca-auto-entry

作るもの(こんな流れで開発したよ)

希望:希望する都道府県と期間を入力するだけで
   該当する全ての大会に勝手に応募してもらえるシステム

簡単なGUIから、ユーザが希望する都道府県と日程を入力できるようにする(tkinter)

入力情報を読み込む(Json化)

読み取ったデータ通りのページにアクセスする

勝手に応募してもらう(selenium)

こんな感じでしょうか...
一旦これで進めていくことにします。

ちなみに.....
トレーナーズウェブサイトはこんな感じで動作してますっていうのを簡単に書いておきます。

画像上のURLの部分を確認してみると、
選んだ都道府県や、日程、部門などは
どんどんクエリパラメータとして、URLに打ち込まれていました。
選択する都道府県が多いととんでもなくURLが長くなる仕組みです。

こちらをうまく活用して作成します!

環境

環境:MacBookAir(M2)
   Visual Studio Code
言語:python

あと、pythonインストールにHomebrewを使用するので
まだインストールされていない方は先に実施しておいてください。

①HomebrewでPCにpythonをインストールする

Homebrewを使用してインストールしていきます。
以下コマンドを打ち込んでみてください。

~/.zshrcファイルの末尾に以下を追記(環境変数を設定)

# Tcl/Tk for Tkinter (pyenv build support)
export LDFLAGS="-L$(brew --prefix tcl-tk)/lib"
export CPPFLAGS="-I$(brew --prefix tcl-tk)/include"
export PKG_CONFIG_PATH="$(brew --prefix tcl-tk)/lib/pkgconfig"
export PATH="$(brew --prefix tcl-tk)/bin:$PATH"

シェルの再読み込みを実施して、ターミナルで以下を実行してください。

brew install anyenv
anyenv install pyenv
pyenv install 3.12.6

ターミナル再起動後、バージョンを確認して、返答があればOKです!

python --version

あとは、好きな階層に任意のフォルダを作成してください。

mkdir pokeca-auto-entry

②tkinterでGUIを用意する

pythonで簡単なGUIを作成したい場合には、tkinterというライブラリが良いっぽい。
(元から搭載されている機能なのでインストールは不要)
とりあえずmain.pyというファイルを作成して
その中にtkinterをimportさせて使用できるようにしていきます。

import tkinter as tk
from tkinter import ttk, messagebox
from tkcalendar import DateEntry

この子達以外にもimportしてます。
gitに上げておきますね。

一旦見た目は気にせず、都道府県と日程を入力できるようにしたのがこれです。
都道府県には数値を振っておきます。(文字列では管理しません。)

PREFS = {
    1: "北海道", 2: "青森県", 3: "岩手県", 4: "宮城県", 5: "秋田県", 6: "山形県",
7: "福島県",8: "茨城県", 9: "栃木県", 10: "群馬県", 11: "埼玉県", 12: "千葉県",
 13: "東京都", 14: "神奈川県",15: "新潟県", 16: "富山県", 17: "石川県",
18: "福井県", 19: "山梨県", 20: "長野県",21: "岐阜県", 22: "静岡県", 23: "愛知県",
24: "三重県",25: "滋賀県", 26: "京都府", 27: "大阪府", 28: "兵庫県", 29: "奈良県",
 30: "和歌山県",31: "鳥取県", 32: "島根県", 33: "岡山県", 34: "広島県", 35: "山口
県",36: "徳島県", 37: "香川県", 38: "愛媛県", 39: "高知県",40: "福岡県", 41: "佐賀
県", 42: "長崎県", 43: "熊本県", 44: "大分県", 45: "宮崎県", 46: "鹿児島県",
47: "沖縄県"
}
frame_prefs = ttk.LabelFrame(root, text="都道府県を選択(複数可)", padding=10, style="BigLabel.TLabelframe")
frame_prefs.pack(fill="both", expand=True, padx=10, pady=10)

pref_vars = {pref: tk.IntVar() for pref in PREFS}

root.update_idletasks()
layout_checkbuttons()

frame_date = ttk.LabelFrame(root, text="期間を選択", padding=10, style="BigLabel.TLabelframe")
frame_date.pack(fill="x", padx=10, pady=10)

ttk.Label(frame_date, text="開始日 :").grid(row=0, column=0, padx=5, pady=5, sticky="e")
ttk.Label(frame_date, text="例: 2025-10-4", font=("Meiryo", 9), foreground="gray").grid(row=1, column=1, sticky="w", padx=5)
start_entry = ttk.Entry(frame_date, width=15)
start_entry.grid(row=0, column=1, padx=5, pady=5)

ttk.Label(frame_date, text="終了日 :").grid(row=0, column=2, padx=5, pady=5, sticky="e")
end_entry = ttk.Entry(frame_date, width=15)

続けて、チェックボックスで選択できるようにします。

 def layout_checkbuttons():
    #ウィンドウ幅に応じてチェックボックスを再配置
    for widget in frame_prefs.winfo_children():
        widget.grid_forget()

    width = frame_prefs.winfo_width()
    if width < 200:
        width = 200

    btn_width = 120  #チェックボックスの幅目安
    cols = max(1, width // btn_width)

    for i, (num, name) in enumerate(PREFS.items()):
        row, col = divmod(i, cols)
        chk = ttk.Checkbutton(frame_prefs, text=name,variable=pref_vars[num])
        chk.grid(row=row, column=col, sticky="w", padx=5, pady=2)

あと、念の為簡単なバリデーション機能も作成しました。
載せきれなかったのでgitから見てみてください。
コメントを残しておいたので、読んでもらえたら嬉しいです。

最後に肝心な応募するボタンを作成したら...

ttk.Button(root, text="応募する", command=submit).pack(pady=20)
root.mainloop()

以下コマンドから実行してみてください。

python main.py 

こんな感じの小さい画面が出てくるようになりました!↓↓

③GUIから入力した内容をJson化する

上記GUIを使用して、入力した内容を
ぜーーんぶJson形式にまとめます。これを後々機械に読み込んでもらいたい...

def submit():
    #選択内容の取得
    selected_nums = [num for num, var in pref_vars.items() if var.get()]
    start_str = start_entry.get().strip()
    end_str = end_entry.get().strip()  

    #JSONに保存
    data = {
        "prefectures": selected_nums,
        "start_date": start_str,
        "end_date": end_str
    }

    filepath = os.path.abspath("config.json")
    with open(filepath, "w", encoding="utf-8") as f:
        json.dump(data, f, indent=4, ensure_ascii=False)

    messagebox.showinfo("保存完了", f"設定を保存しました!\n保存場所: {filepath}")

ここまで実際に動かしてみると
入力結果を元にファイルが生成されて(config.json)
その中に先ほど入力した都道府県や日程が入っているはずです!

④Jsonの内容をURLにぶち込む

こっからですね...長々とすみません。
③で作成したJsonを読み取ってもらいます。
読み込ませて何をしたいかっていう話なんですけど、トレーナーズウェブサイトにアクセスしたいです。

見慣れたこの画面。
先ほどGUIから入力した値を、クエリパラメータとしてURLに入れると
直接ページにアクセスできるので、既に絞り込みされた状態の画面を映してくれるはずです。

それをやっていきます。
ファイル内の行が増えてきたのでファイルを分けます。
auto_entry.pyというファイルを新たに作成しました。
これで読んでくれるはず。

auto_entry.py↓↓

import json

#Json読み込み
with open("config.json", "r", encoding="utf-8") as f:
    config = json.load(f)

pref_nums = config["prefectures"]
start_date = config["start_date"].replace("-", "/")
end_date = config["end_date"].replace("-", "/")

#可変URL生成 
#クエリパラメータとしてJsonの内容をURLに挿入
BASE_URL = "https://players.pokemon-card.com/event/search"
FIXED_PARAMS = "&event_type=3:2&league_type=1&offset=0&accepting=true&order=1"
pref_str = ",".join(map(str, pref_nums))
target_url = f"{BASE_URL}?start_date={start_date}&end_date={end_date}&prefecture={pref_str}{FIXED_PARAMS}"

main.py↓↓

    #script_pathを定義
    #auto_entry.pyを自動で実行
    script_path = os.path.join(os.path.dirname(__file__), "auto_entry.py")

    try:
        subprocess.Popen([sys.executable, script_path])
        messagebox.showinfo("実行開始", "URL生成スクリプトを実行しました!")
    except Exception as e:
        messagebox.showerror("エラー", f"auto_entry.py の実行に失敗しました。\n{e}")

GUIから任意の都道府県、日付を入力し、
その通りに絞り込まれたサイトが開けばうまくいってます!

⑤Seleniumに画面操作させる

いよいよ最終局面です。サイド枚数で言うと残り2枚くらいです。

ブラウザを自動で操作するためのライブラリSeleniumを使用します。
以下コマンドを実行してください。

pip install selenium

seleniumにやってほしいことはこんな感じです。

大会タイトルをクリック

「イベント応募へ」ボタン押下

利用規約に同意する

「応募する」ボタン押下

これらを大会の数だけループします。

実際に手動で応募するときも、同じ手段を踏んでいると思います。
全く同じことをseleniumにお願いします。

ただ!!これらの動作の前にログインしなければなりません。
セキュリティの観点からユーザ自身で入力する手法をとります。
ログインするまで(300s)の間、seleniumには一旦待ってもらう作戦です。

こんなかんじ↓↓

ユーザのログイン待ち部分↓↓

#Selenium起動
driver = webdriver.Chrome()
wait = WebDriverWait(driver, 20)
driver.get(target_url)
print("ページを読み込み中...")
time.sleep(4)    # JS描画待ち

#Userログイン待ち
print("\n 必要に応じて手動でログインしてください。")
input("ログインが完了したら return を押してください。")

#ログイン後に、再度検索URLを開く
print("\n 検索ページ再読み込み中...")
driver.get(target_url)
time.sleep(4)
print("ログイン完了を確認、大会一覧を再取得中...")

応募処理のループ処理↓↓

#応募処理ループ
for idx in range(len(titles)):
    titles = get_title_elements()
    if idx >= len(titles):
        print(f"インデックス {idx} は現在のタイトル数を超えています。終了します。")
        break

    title_elem = titles[idx]
    title_text = title_elem.text.strip()
    print(f"\n[{idx+1}] '{title_text}' をクリックして大会に申し込みます...")

    try:
        #大会タイトルクリック
        driver.execute_script("arguments[0].scrollIntoView(true);", title_elem)
        time.sleep(0.5)
        driver.execute_script("arguments[0].click();", title_elem)

        # 詳細ページロード待機
        wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, "button.c-btn.c-btn-primary")))
        time.sleep(1)
        #print("大会詳細ページを開きました。")

        #「イベント応募へ」押下
        try:
            entry_btn = wait.until(
                EC.element_to_be_clickable((By.XPATH, "//button[contains(text(),'イベント応募へ')]"))
            )
            driver.execute_script("arguments[0].click();", entry_btn)
            print("「イベント応募へ」をクリックしました。")
        except Exception as ee:
            print("「イベント応募へ」が見つかりませんでした(スキップ):", ee)
            driver.get(target_url)
            time.sleep(2)
            continue

        #「利用規約」にチェック(input or label)
        try:
            try:
                agree = wait.until(EC.element_to_be_clickable((By.ID, "agreement2")))
                driver.execute_script("arguments[0].click();", agree)
                print("利用規約に同意しました。(inputクリック)")
            except Exception:
                label_elem = wait.until(
                    EC.element_to_be_clickable((By.XPATH, "//label[contains(text(),'利用規約に同意する')]"))
                )
                driver.execute_script("arguments[0].click();", label_elem)
                print("利用規約に同意しました。(ラベルクリック)")
            time.sleep(0.5)
        except Exception as ee:
            print("利用規約チェックボックスが見つかりません(スキップ):", ee)
            driver.get(target_url)
            time.sleep(2)
            continue

        #「応募する」押下
        try:
            apply_btn = wait.until(EC.element_to_be_clickable((By.XPATH, "//button[contains(text(),'応募する')]")))
            driver.execute_script("arguments[0].click();", apply_btn)
            print("「応募する」をクリックしました。")
        except Exception as ee:
            print("「応募する」が見つかりません(スキップ):", ee)
            driver.get(target_url)
            time.sleep(2)
            continue

        time.sleep(2)
        print(f"[{idx+1}] '{title_text}' に応募しました。")

        # 一覧に戻る
        driver.get(target_url)
        time.sleep(3)

    except Exception as e:
        print(f"[{idx+1}] 処理中にエラーが発生しました: {e}")
        driver.get(target_url)
        time.sleep(3)
        continue

print("\n全処理完了。ブラウザを閉じます。")
driver.quit()

どうしても長くなってしまって、少し端折っています。
gitに全部上げておきます...

⑥応募完了!!!

自分が出場したい会場と期間を入力して...
画面の下にずっとあった応募するボタンを押下すると...

応募中...

!!!
応募できています!!!
※上の画像と応募件数が合ってませんが、ご了承ください...
たくさん動作確認しているうちに溜まってしまいました...

流石にワンクリックで〜などと言うわけにいきませんが、随分楽に応募できます!
応募完了メールも確認したらしっかり届いてました。

どうしても2026-S2に間に合わせたくて...
完全に自分用だし、随分シンプルな作りになってます。
来シーズン中には、誰でも使えるようにパッケージ化したいですね...ここまで頑張ったし!

最後に

最後まで読んでくださり、ありがとうございます。
改善の余地しかないツールですので、自分流にアレンジできる方は
どんどんアレンジしてくださると嬉しいです。(あわよくば私に共有してくださるともっと嬉しいです)

ここまで読んでくださった皆様が、良い結果を残せますように!!!

Discussion