初心者向け No.070

星座占い

生年月日を入力して星座を判定し今日の運勢をランダム表示するアプリ。日付計算とdictの使い方を学べる。

🎯 難易度: ★☆☆ 📦 ライブラリ: tkinter(標準ライブラリ) ⏱️ 制作時間: 30〜90分

1. アプリ概要

生年月日を入力して星座を判定し今日の運勢をランダム表示するアプリ。日付計算とdictの使い方を学べる。

このアプリはtoolsカテゴリの実践的なPythonアプリです。使用ライブラリは tkinter(標準ライブラリ)、難易度は ★☆☆ です。

このアプリは「ツール」カテゴリです。日々の作業を自動化する実用ツールは Python が最も得意とする領域です。短いコードで実用性のある成果物が作れる点が魅力です。tkinter(標準ライブラリ) を活かして実装するこの構造は、他のアプリにも応用が効きます。

完成形を見てから細部の解説に進む流れが効果的です。実行→気になる箇所を解説で確認→自分の手で改造、というサイクルで定着が早まります。

カスタマイズ章では具体的な拡張アイデアを示しています。一つ実装するごとに動作確認することで、変更が予期せぬ副作用を起こさないかも体感できます。

星座占い 実行画面(Windows)
実行画面(Windows)
星座占い 実行画面(Linux Mint)
実行画面(Linux Mint)

2. 機能一覧

  • 「占う」ボタン(fortune())で操作
  • messagebox.showerror()messagebox.showwarning() によるダイアログ通知
  • ウィンドウはタイトル「星座占い」・サイズ 600x550 で起動

3. 事前準備・環境

ℹ️
動作確認環境

Python 3.10 以上 / Windows 11(実機)で起動・基本操作を確認 / Linux Mint 22.3(仮想マシン)で起動・画面表示を確認(macOS は未検証)

Windows 11(実機)で起動と基本操作を確認しています(全機能の網羅テストではありません)。Linux Mint 22.3(仮想マシン)では起動と画面表示を確認しました。macOS は標準ライブラリの範囲で動作する想定ですが、未検証です。

  • Python 3.10 以上
  • OS: Windows 11(実機で起動・基本操作を確認)・Linux Mint 22.3(仮想マシンで起動・画面表示を確認)

4. 完全なソースコード

💡
コードのコピー方法

右上の「コピー」ボタンをクリックするとコードをクリップボードにコピーできます。

追加インストール不要(標準ライブラリのみ使用)
app070.py
import tkinter as tk
from tkinter import ttk, messagebox
from datetime import datetime, date
import random


class App070:
    """星座占い: 生年月日から星座を判定し、今日の運勢を表示"""

    # 星座データ: (名前, シンボル, 開始月日, 終了月日)
    ZODIACS = [
        ("やぎ座",   "♑", (12, 22), (1, 19)),
        ("みずがめ座", "♒", (1, 20), (2, 18)),
        ("うお座",   "♓", (2, 19), (3, 20)),
        ("おひつじ座", "♈", (3, 21), (4, 19)),
        ("おうし座",  "♉", (4, 20), (5, 20)),
        ("ふたご座",  "♊", (5, 21), (6, 21)),
        ("かに座",   "♋", (6, 22), (7, 22)),
        ("しし座",   "♌", (7, 23), (8, 22)),
        ("おとめ座",  "♍", (8, 23), (9, 22)),
        ("てんびん座", "♎", (9, 23), (10, 23)),
        ("さそり座",  "♏", (10, 24), (11, 22)),
        ("いて座",   "♐", (11, 23), (12, 21)),
    ]

    LUCK_LEVELS = ["大吉", "中吉", "小吉", "吉", "末吉", "凶"]
    LUCKY_COLORS = ["赤", "青", "緑", "黄", "ピンク", "紫", "白", "金", "オレンジ"]
    LUCKY_ITEMS = ["腕時計", "ハンカチ", "コーヒー", "本", "鉛筆", "イヤホン", "観葉植物", "アロマ", "メガネ"]
    MESSAGES = [
        "新しい挑戦が幸運を呼びそうです。",
        "周囲の人への感謝が運気アップの鍵。",
        "落ち着いた行動が吉と出るでしょう。",
        "アイデアが冴える1日。メモを忘れずに。",
        "予想外の出来事から良い縁が生まれそう。",
        "健康管理に気を配ると好調をキープできます。",
        "古い友人との再会が幸運を運びます。",
        "金銭は慎重に、衝動買いは避けて。",
    ]

    def __init__(self, root):
        self.root = root
        self.root.title("星座占い")
        self.root.geometry("600x550")
        self.root.minsize(600, 550)
        self.root.configure(bg="#f8f9fc")
        self._build_ui()

    def _build_ui(self):
        """UI を構築する"""
        # タイトル
        title_frame = tk.Frame(self.root, bg="#3776ab", pady=12)
        title_frame.pack(fill=tk.X)
        tk.Label(
            title_frame, text="✨ 星座占い ✨",
            font=("Noto Sans JP", 16, "bold"),
            bg="#3776ab", fg="white"
        ).pack()

        main = tk.Frame(self.root, bg="#f8f9fc", padx=20, pady=20)
        main.pack(fill=tk.BOTH, expand=True)

        # 入力
        form = ttk.LabelFrame(main, text="生年月日を入力", padding=10)
        form.pack(fill=tk.X)
        today = date.today()
        ttk.Label(form, text="年").grid(row=0, column=0, sticky="w")
        self.year_var = tk.StringVar(value=str(today.year - 25))
        ttk.Combobox(form, textvariable=self.year_var, width=6,
                     values=[str(y) for y in range(1900, today.year + 1)]
                     ).grid(row=0, column=1, padx=4)
        ttk.Label(form, text="月").grid(row=0, column=2, sticky="w")
        self.month_var = tk.StringVar(value="1")
        ttk.Combobox(form, textvariable=self.month_var, width=4,
                     values=[str(m) for m in range(1, 13)]
                     ).grid(row=0, column=3, padx=4)
        ttk.Label(form, text="日").grid(row=0, column=4, sticky="w")
        self.day_var = tk.StringVar(value="1")
        ttk.Combobox(form, textvariable=self.day_var, width=4,
                     values=[str(d) for d in range(1, 32)]
                     ).grid(row=0, column=5, padx=4)
        ttk.Button(form, text="占う", command=self.fortune).grid(row=0, column=6, padx=10)

        # 結果表示
        result_frame = ttk.LabelFrame(main, text="占い結果", padding=12)
        result_frame.pack(fill=tk.BOTH, expand=True, pady=10)

        self.zodiac_var = tk.StringVar(value="生年月日を入力して占ってください")
        tk.Label(result_frame, textvariable=self.zodiac_var, bg="#f8f9fc",
                 font=("Noto Sans JP", 22, "bold"), fg="#3776ab").pack(pady=10)

        self.detail_text = tk.Text(result_frame, height=10, font=("Noto Sans JP", 11),
                                   bg="white", relief=tk.FLAT, wrap=tk.WORD, state=tk.DISABLED)
        self.detail_text.pack(fill=tk.BOTH, expand=True)

    def _detect_zodiac(self, month, day):
        """月日から星座を判定する"""
        for name, sym, start, end in self.ZODIACS:
            sm, sd = start
            em, ed = end
            # 年をまたぐ (やぎ座) の場合
            if sm > em:
                if (month == sm and day >= sd) or (month == em and day <= ed):
                    return name, sym
            else:
                if (month == sm and day >= sd) or (month == em and day <= ed) or (sm < month < em):
                    return name, sym
        return None, None

    def fortune(self):
        """生年月日から星座と運勢を表示する"""
        try:
            y = int(self.year_var.get())
            m = int(self.month_var.get())
            d = int(self.day_var.get())
            birth = date(y, m, d)
        except ValueError:
            messagebox.showwarning("入力エラー", "正しい生年月日を入力してください")
            return
        if birth > date.today():
            messagebox.showwarning("入力エラー", "未来の日付は入力できません")
            return
        name, sym = self._detect_zodiac(m, d)
        if not name:
            messagebox.showerror("エラー", "星座を判定できませんでした")
            return
        # 同じ日付なら同じ運勢になるよう seed を固定 (今日の日付 + 生年月日)
        today = date.today()
        seed_str = f"{name}-{today.isoformat()}"
        rng = random.Random(seed_str)
        luck = rng.choice(self.LUCK_LEVELS)
        color = rng.choice(self.LUCKY_COLORS)
        item = rng.choice(self.LUCKY_ITEMS)
        msg = rng.choice(self.MESSAGES)
        score = rng.randint(40, 100)
        # 年齢
        age = today.year - birth.year - ((today.month, today.day) < (birth.month, birth.day))
        # 表示
        self.zodiac_var.set(f"{sym} {name}")
        self.detail_text.config(state=tk.NORMAL)
        self.detail_text.delete("1.0", tk.END)
        lines = [
            f"生年月日: {birth.isoformat()} (満 {age} 歳)",
            f"今日の日付: {today.isoformat()}",
            "",
            f"今日の運勢: {luck}  (運気スコア: {score}/100)",
            f"ラッキーカラー: {color}",
            f"ラッキーアイテム: {item}",
            "",
            f"💬 {msg}",
        ]
        self.detail_text.insert("1.0", "\n".join(lines))
        self.detail_text.config(state=tk.DISABLED)


if __name__ == "__main__":
    root = tk.Tk()
    app = App070(root)
    root.mainloop()

5. コード解説

星座占い: 生年月日から星座を判定し、今日の運勢を表示のコードを、実際に書かれている実装に沿って解説します。

クラス設計とコンストラクタ

App070 クラスにアプリの全機能をまとめています(メソッド4個・全158行)。__init__ ではタイトル「星座占い」とウィンドウサイズ 600x550 を設定します。最後に _build_ui() で画面を組み立てます。

※ 該当部分のコード本体は 「4. 完全なソースコード」 をご参照ください(重複表示を避けるため再掲を省略しています)。

ウィジェット構成

画面は tk.Frame×2・tk.Label×2・ttk.LabelFrame×2・ttk.Label×3・ttk.Combobox×3・ttk.Buttontk.Text で構成しています。見出し付きの枠(LabelFrame)は「生年月日を入力」「占い結果」のラベルでエリアを区切っています。

※ 該当部分のコード本体は 「4. 完全なソースコード」 をご参照ください(重複表示を避けるため再掲を省略しています)。

イベント処理とボタンの接続

「占う」ボタン(fortune())を command= で接続しています。

※ 該当部分のコード本体は 「4. 完全なソースコード」 をご参照ください(重複表示を避けるため再掲を省略しています)。

クラス定数によるデータ定義

クラスの先頭でデータを定数として定義しています:LUCK_LEVELS = ['大吉', '中吉', '小吉', '吉', '末吉', '凶']LUCKY_COLORS = ['赤', '青', '緑', '黄', 'ピンク', '紫', '白', '金', 'オレンジ']。ロジックの中に直接書かず定数にまとめておくと、内容を変えたいときに1か所を書き換えるだけで済みます。

※ 該当部分のコード本体は 「4. 完全なソースコード」 をご参照ください(重複表示を避けるため再掲を省略しています)。

例外処理

try-except で ValueError を捕捉しています。あわせて messagebox.showerror()messagebox.showwarning() のダイアログで、ユーザーへの通知・確認を行います。

※ 該当部分のコード本体は 「4. 完全なソースコード」 をご参照ください(重複表示を避けるため再掲を省略しています)。

6. ステップバイステップガイド

このアプリをゼロから自分で作る手順を解説します。コードをコピーするだけでなく、実際に手順を追って自分で書いてみましょう。

  1. 1
    ファイルを作成する

    新しいファイルを作成して app070.py と保存します。使うのは datetimerandomtkinter だけなので、追加インストールは不要です。

  2. 2
    クラスの骨格を作る

    App070 クラスを定義し、__init__ と、末尾の root = tk.Tk()mainloop() の最小構成を書いて、まず空のウィンドウが出ることを確認します。

  3. 3
    ウィンドウを設定する

    title("星座占い")geometry("600x550")configure(bg="#f8f9fc")minsize(600, 550) をコンストラクタで設定します。

  4. 4
    画面部品を並べる

    _build_ui() の中で、tk.Frame×2・tk.Label×2・ttk.LabelFrame×2・ttk.Label×3・ttk.Combobox×3・ttk.Buttontk.Text を作って配置します(掲載コードと同じ並び順で書くとレイアウトが一致します)。まず表示だけ確認しましょう。

  5. 5
    イベントを接続する

    「2. 機能一覧」で挙げた各ボタンを command= で対応するメソッド(fortune())につなぎます。

  6. 6
    中心になるメソッドを実装する

    アプリの本体である fortune()(44行)・_detect_zodiac()(13行) を実装します。

  7. 7
    動作確認する

    python app070.py で起動し、各ボタンが反応することを確認します。

7. カスタマイズアイデア

基本機能を習得したら、以下のカスタマイズに挑戦してみましょう。

💡 ダークモードを追加する

bg色・fg色を辞書で管理し、ボタン1つでダークモード・ライトモードを切り替えられるようにしましょう。

💡 クラス定数を書き換えて動きを変える

データはクラス定数(ZODIACSLUCK_LEVELS)にまとまっています。中身を書き換えて動きが変わることを確かめましょう。複数の定数が対応関係にある場合は、セットで整合させる必要がある点に注意してください。

💡 設定ダイアログ

フォントサイズや色などの設定をユーザーが変更できるオプションダイアログを追加しましょう。

8. よくある問題と解決法

❌ 文字のフォントが崩れる・見た目が違う

原因:このコードは font=("Noto Sans JP", ...) のようにフォント名を直接指定しています。環境にこのフォントが入っていないと、OS が代わりのフォントで表示するため、見本と見た目が変わることがあります。

解決法:動作には支障ありません。気になる場合は font 引数の名前を自分の OS に入っているフォントへ書き換えるか、font 引数を省略してください。

❌ ウィンドウの大きさが画面に合わない

原因:geometry("600x550") の固定サイズで起動するためです。

解決法:geometry()minsize() の両方の数値を書き換えて起動サイズを調整してください。なお、このコードにはウィンドウサイズを固定する設定(resizable の指定)が無いため、ウィンドウの端をドラッグしたサイズ変更は既定どおり可能です。ただし minsize(600, 550) を指定しているため、起動時の大きさより小さくは縮められません(縮めると画面部品が表示されなくなるのを防ぐためです)。

9. 練習問題

アプリの理解を深めるための練習問題です。気になるものから挑戦してみてください(課題3は定型の発展課題です)。表示される運勢は、あらかじめ用意した候補からランダムに選んでいるだけです。占いとして楽しむためのもので、なにかを予測したり判断の根拠にしたりするものではありません。

  1. 課題1:同じ星座でも生年月日ごとに運勢を変える

    seed_str は星座名と今日の日付だけで作っています。コードのコメントには「今日の日付 + 生年月日」とありますが、実際には生年月日が入っていません。そのため同じ星座の人は全員が同じ運勢になります。

    期待結果:1990年4月1日と2005年4月1日はどちらもおひつじ座で、いまは運勢もスコアも同じ。生年月日を混ぜると別々の結果になり、同じ生年月日で占い直せば結果は変わらない。

    合格条件

    • 同じ星座で誕生日が違う2人を占うと、表示が変わる(一部が偶然そろうことはある)
    • 同じ生年月日で続けて2回占うと、同じ結果が出る
    • 星座名の表示(♈ おひつじ座)は今までどおり

    ヒント①(どこを触るか): 触るのは fortune() の中の seed_str = ... の1行だけです。その下の rng.choice() 4回と rng.randint() 1回は、そのまま使います。_detect_zodiac() も触りません。
    ヒント②(使うもの): 入力された日付は birth に入っているので、seed_str = f"{name}-{birth.isoformat()}-{today.isoformat()}" のように混ぜます。random.Random() は渡した文字列ごとに決まった並びの乱数を返すため、日付が変われば翌日は別の結果になります。
    つまずきやすい点: rng をやめて random.choice() を直接呼ぶと、占うたびに結果が変わり「その日のうちは同じ」という作りが壊れます。birth.isoformat()2005-04-01 のような文字列を返すので、str() で囲み直す必要はありません。

  2. 課題2:誕生日当日にお祝いの1行を足す

    結果は lines というリストを組み立ててから、まとめて detail_text へ書き込んでいます。月日が今日と同じときだけ、この末尾にお祝いの行を足しましょう。

    期待結果:月日を今日と同じにして占うと、結果の最後に「🎂 お誕生日おめでとう!」が出る。別の日付では、いままでどおり増えない。

    合格条件

    • 月日が今日と同じ生年月日で占うと、行が1つ増える
    • 年が違っても(1990年でも2005年でも)同じように出る
    • 別の日付では表示が今までと変わらない

    ヒント①(どこを触るか): 触るのは fortune()lines = [...] の1か所です。age を計算している行のすぐ下にあります。書き込みを行う insert()config() は、そのまま使います。
    ヒント②(使うもの): 判定は if birth.month == today.month and birth.day == today.day: です。lines を作ったあとに lines.append("🎂 お誕生日おめでとう!") を足すと、既存の "\n".join(lines) がそのまま拾ってくれます。
    つまずきやすい点: birth == today と日付ごと比べると年まで一致が必要になり、ほとんどの人には出ません。比べるのは月と日の2つだけです。なお2月29日生まれの場合は、うるう年にしか一致しません。

  3. 課題3:保存機能の追加

    処理結果をファイルに保存する機能を追加しましょう。

🚀
次に挑戦するアプリ

このアプリをマスターしたら、次のアプリに挑戦しましょう。

🐛
エラーが出て動かないときは

写経中に赤いエラー文が出たら、Pythonエラー一覧&解決法(英語メッセージ逆引き)で原因と直し方をすぐ確認できます。

📖
独学を1冊で体系化するなら

写経しながら「なぜこう書くのか」が気になり始めたら、入門書を1冊通して読むと断片的な知識がつながります。Python入門書のおすすめ2冊(当サイトの参考書ランキング総合1〜2位)で、独学者向けの最初の1冊を比較しています。