Python | 1 日 120 分 × 7 日アプリ学習:JSON保存アプリ(中級編)

Web APP Python
スポンサーリンク

7日目のゴール

7日目のテーマは
「JSON保存アプリを“完成形”としてまとめ、アプリとしての完成度を高める」
ことです。

ここまで6日間で、あなたはすでに

JSONの基本
Pythonのdict / listとの対応
JSONへの保存・読み込み
追加・編集・削除・検索
フィルタ・ソート
タグ・カテゴリ・完了フラグ
コードの役割分離

という、アプリ開発の土台を全部通っています。

7日目では、それらを統合して

「実際に使えるレベルのJSON保存メモアプリ」
「データの一生(作成→保存→読み込み→更新→保護)」

を意識した“完成版”を作ります。


JSON保存アプリの「完成形」をイメージする

アプリとして必要な視点を整理する

機能が揃ってきたら、次に考えるべきは
「アプリとしてちゃんとしているか」 です。

ここで意識したいポイントは次のようなものです。

データが壊れても、できるだけ復旧できるか
古いデータ形式でも動き続けられるか(後方互換性)
ユーザーが迷わないメニュー構成になっているか
エラーが起きてもアプリが落ちずにメッセージを出せるか
データの保存タイミングが明確か

7日目は、これらをJSON保存アプリに組み込んでいきます。


JSONデータの「後方互換性」を考える

データ構造が変わっても壊れないようにする

6日目でメモの構造をリッチにしました。

memo = {
    "text": "牛乳を買う",
    "created_at": "2025-05-05 10:30",
    "important": False,
    "category": "生活",
    "tags": ["買い物", "食料品"],
    "done": False
}
Python

しかし、すでに保存されている memos.json には
categorytagsdone がないメモも混ざっている可能性があります。

そこで重要になるのが
「読み込んだときに足りない項目を補う」 という考え方です。

def normalize_memo(memo):
    if "text" not in memo:
        memo["text"] = ""
    if "created_at" not in memo:
        memo["created_at"] = "日時不明"
    if "important" not in memo:
        memo["important"] = False
    if "category" not in memo:
        memo["category"] = "未分類"
    if "tags" not in memo:
        memo["tags"] = []
    if "done" not in memo:
        memo["done"] = False
    return memo
Python

そして、読み込み時に必ず正規化します。

def load_memos():
    if not os.path.exists(FILENAME):
        return []
    with open(FILENAME, "r", encoding="utf-8") as f:
        memos = json.load(f)
    return [normalize_memo(m) for m in memos]
Python

これで、

古い形式のJSON
新しい形式のJSON

どちらでも、アプリ側は
「必ず同じ形のメモ」として扱えるようになります。

ここは、実務でも超重要なポイントです。


JSONデータの「安全な読み書き」を考える

JSONが壊れていたときにどうするか

JSONファイルはテキストなので、
何かの拍子に中身が壊れることがあります。

例えば、途中で書き込みが中断されたり、
手で編集してミスしたり。

そのときに json.load()JSONDecodeError を投げて、
そのままだとアプリが落ちてしまいます。

そこで、例外処理を入れて「安全な読み込み」にします。

import json
import os
import shutil

FILENAME = "memos.json"
BACKUP_FILENAME = "memos.json.bak"

def load_memos_safe():
    if not os.path.exists(FILENAME):
        return []

    try:
        with open(FILENAME, "r", encoding="utf-8") as f:
            memos = json.load(f)
    except json.JSONDecodeError:
        print("JSONファイルが壊れています。バックアップから復元を試みます。")
        if os.path.exists(BACKUP_FILENAME):
            shutil.copy(BACKUP_FILENAME, FILENAME)
            with open(FILENAME, "r", encoding="utf-8") as f:
                memos = json.load(f)
        else:
            print("バックアップがありません。空のデータで開始します。")
            memos = []

    return [normalize_memo(m) for m in memos]
Python

ここでの重要ポイントは二つです。

ひとつは、「壊れていたら即終了」ではなく
「バックアップから復元を試みる」 というステップを挟んでいること。

もうひとつは、
最終的に必ず normalize_memo を通して
データの形をそろえていることです。


保存前に「バックアップ」を取る

データを守るための一手間

バックアップを活かすには、
保存のたびにバックアップを作る必要があります。

def backup_memos():
    if os.path.exists(FILENAME):
        shutil.copy(FILENAME, BACKUP_FILENAME)
Python

そして、保存処理をこうします。

def save_memos(memos):
    backup_memos()
    with open(FILENAME, "w", encoding="utf-8") as f:
        json.dump(memos, f, ensure_ascii=False, indent=2)
Python

これで、

保存前の状態 → memos.json.bak
保存後の状態 → memos.json

という二重構えになります。

「壊れたら終わり」ではなく
「壊れても戻せる」 という状態にしておくのが、
アプリとしての優しさです。


アプリのメニューを「完成版」として整える

ユーザー目線でメニューを並べ直す

機能が増えてきたので、
メニューも整理して“完成版”にします。

例えば、こんな構成が考えられます。

1: メモを追加
2: メモ一覧(作成順)
3: メモ一覧(新しい順)
4: 重要なメモだけ表示
5: 未完了のメモだけ表示
6: 完了したメモだけ表示
7: タグで絞り込み
8: カテゴリごとに表示
9: メモを編集
10: メモを削除
11: 重要フラグを切り替え
12: 完了フラグを切り替え
0: 終了

ここで意識しているのは、

よく使う「追加・一覧」を上に置く
「見る系(一覧・フィルタ)」と「変える系(編集・削除)」を分ける
番号の意味が直感的になるようにする

ということです。

メニューは「アプリの顔」なので、
ここを丁寧に設計すると、
自分で使っていても気持ちよくなります。


7日目版「完成アプリ」の全体像

データの一生を意識した構成

ポイントとなる部分だけ、まとめて載せます。

データ読み書き+正規化+バックアップ

import json
import os
import shutil
from datetime import datetime

FILENAME = "memos.json"
BACKUP_FILENAME = "memos.json.bak"

def normalize_memo(memo):
    if "text" not in memo:
        memo["text"] = ""
    if "created_at" not in memo:
        memo["created_at"] = "日時不明"
    if "important" not in memo:
        memo["important"] = False
    if "category" not in memo:
        memo["category"] = "未分類"
    if "tags" not in memo:
        memo["tags"] = []
    if "done" not in memo:
        memo["done"] = False
    return memo

def load_memos():
    if not os.path.exists(FILENAME):
        return []
    try:
        with open(FILENAME, "r", encoding="utf-8") as f:
            memos = json.load(f)
    except json.JSONDecodeError:
        print("JSONファイルが壊れています。バックアップから復元します。")
        if os.path.exists(BACKUP_FILENAME):
            shutil.copy(BACKUP_FILENAME, FILENAME)
            with open(FILENAME, "r", encoding="utf-8") as f:
                memos = json.load(f)
        else:
            print("バックアップがありません。空のデータで開始します。")
            memos = []
    return [normalize_memo(m) for m in memos]

def backup_memos():
    if os.path.exists(FILENAME):
        shutil.copy(FILENAME, BACKUP_FILENAME)

def save_memos(memos):
    backup_memos()
    with open(FILENAME, "w", encoding="utf-8") as f:
        json.dump(memos, f, ensure_ascii=False, indent=2)
Python

メモ作成(完成版)

def create_memo(text, category=None, tags=None):
    if text == "":
        return None
    now = datetime.now()
    created_at = now.strftime("%Y-%m-%d %H:%M")
    if not category:
        category = "未分類"
    if tags is None:
        tags = []
    else:
        tags = [t for t in tags if t != ""]
    memo = {
        "text": text,
        "created_at": created_at,
        "important": False,
        "category": category,
        "tags": tags,
        "done": False
    }
    return memo
Python

表示(完成版)

def show_memos(memos):
    if not memos:
        print("メモはまだありません。")
        return
    print("=== メモ一覧 ===")
    for i, memo in enumerate(memos, start=1):
        important_mark = "★" if memo.get("important") else " "
        done_mark = "✔" if memo.get("done") else "✗"
        text = memo.get("text", "")
        created_at = memo.get("created_at", "日時不明")
        category = memo.get("category", "未分類")
        tags = memo.get("tags", [])
        if tags:
            tags_label = " / ".join(tags)
            tags_part = f"[{tags_label}]"
        else:
            tags_part = ""
        print(f"{i}. {important_mark}{done_mark} {text} ({created_at}) <{category}> {tags_part}")
Python

フィルタ・ソートの型

def sort_by_created_at(memos, reverse=False):
    return sorted(memos, key=lambda m: m.get("created_at", ""), reverse=reverse)

def filter_by_tag(memos, tag):
    return [m for m in memos if tag in m.get("tags", [])]

def filter_by_done(memos, done=True):
    return [m for m in memos if m.get("done", False) == done]

def filter_important(memos):
    return [m for m in memos if m.get("important", False)]
Python

メインループ(完成版イメージ)

def main():
    memos = load_memos()
    print("JSONファイルからメモを読み込みました。")

    while True:
        print("\n=== メニュー ===")
        print("1: メモを追加")
        print("2: メモ一覧(作成順)")
        print("3: メモ一覧(新しい順)")
        print("4: 重要なメモだけ表示")
        print("5: 未完了のメモだけ表示")
        print("6: 完了したメモだけ表示")
        print("7: タグで絞り込み")
        print("8: カテゴリごとに表示")
        print("9: メモを編集")
        print("10: メモを削除")
        print("11: 重要フラグを切り替え")
        print("12: 完了フラグを切り替え")
        print("0: 終了")

        choice = input("番号を選んでください: ")

        if choice == "1":
            input_and_add_memo(memos)
            save_memos(memos)

        elif choice == "2":
            show_memos(sort_by_created_at(memos, reverse=False))

        elif choice == "3":
            show_memos(sort_by_created_at(memos, reverse=True))

        elif choice == "4":
            show_memos(filter_important(memos))

        elif choice == "5":
            show_memos(filter_by_done(memos, done=False))

        elif choice == "6":
            show_memos(filter_by_done(memos, done=True))

        elif choice == "7":
            input_and_show_by_tag(memos)

        elif choice == "8":
            show_memos_grouped_by_category(memos)

        elif choice == "9":
            input_and_edit_memo(memos)
            save_memos(memos)

        elif choice == "10":
            input_and_delete_memo(memos)
            save_memos(memos)

        elif choice == "11":
            input_and_toggle_important(memos)
            save_memos(memos)

        elif choice == "12":
            input_and_toggle_done(memos)
            save_memos(memos)

        elif choice == "0":
            save_memos(memos)
            print("終了します。")
            break

        else:
            print("正しい番号を入力してください。")
Python

7日目で絶対に押さえてほしい本質

JSON保存アプリは「データの一生」を扱う

7日目で意識してほしいのは、
JSON保存アプリが単なる「ファイル入出力」ではなく、

データを設計する
データを作る
データを保存する
データを読み込む
データを更新する
データを守る(バックアップ・エラー処理)

という「データの一生」を丸ごと扱っている、ということです。

あなたはもう、

Pythonのdict / listでデータを設計し
JSONとして外に出し
またJSONからアプリの世界に戻し
そのデータを安全に育てていける

そんなレベルに来ています。

ここから先は、

GUIにする(Tkinter)
Webアプリにする(Flask / FastAPI)
JSONからSQLiteなどのDBに発展させる

といった方向に、いくらでも広げていけます。

「次はどんなアプリを作ってみたいか」
それを決めるところから、もう次の一歩が始まっています。

タイトルとURLをコピーしました