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

Web APP Python
スポンサーリンク

6日目のゴール

6日目のテーマは
「JSONに“少しリッチなデータ構造”を持たせて、アプリの表現力を上げる」 ことです。

ここまでであなたはすでに、

JSONに複数データを保存できる
起動時に復元できる
追加・編集・削除・検索・フィルタ・ソートができる
コードを役割ごとに整理できる

というところまで来ています。

6日目では一歩進んで、

メモに「タグ」や「カテゴリ」を持たせる
JSONに“入れ子のデータ”を保存する
データの「形(構造)」を意識して設計する

という、「データ設計」の感覚を育てていきます。


今日の題材:メモを“ただの文字列”から“情報のかたまり”へ

メモにどんな情報を持たせるか考える

今までのメモは、こうでした。

{
    "text": "牛乳を買う",
    "created_at": "2025-05-05 10:30",
    "important": False
}

これはこれでシンプルで良いのですが、
現実の「メモ」はもっと情報を持っています。

例えば、こんな項目が考えられます。

カテゴリ(仕事 / プライベート / 勉強 など)
タグ(複数可:買い物, アイデア, 読書 など)
完了フラグ(やり終えたタスク系メモかどうか)

これをPythonの辞書で表現すると、こうなります。

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

JSONにすると、ほぼそのままです。

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

ここで大事なのは、
「メモ=文字列」ではなく「メモ=情報のまとまり」
として扱い始めることです。


データ構造を変えるときの考え方

「既存のデータにどう影響するか」を意識する

メモの形を変えるときは、
次の二つを必ず意識します。

新しいメモを作るとき、どの項目を必須にするか
既存の JSON ファイルとの互換性をどうするか

例えば、すでに memos.json

{
  "text": "Pythonの勉強をする",
  "created_at": "2025-05-04 21:00",
  "important": true
}

のようなデータが入っている状態で、
新しく categorytagsdone を追加すると、
古いデータにはそのキーがありません。

このときに大事なのは、

「キーがない場合はどう扱うか」を決めておくことです。

例えば、

category がなければ “未分類” とみなす
tags がなければ空リスト [] とみなす
done がなければ False とみなす

といったルールを決めておくと、
古いデータも壊さずに扱えます。


メモの新しい形をコードに落とし込む

メモ作成処理を拡張する

メモ追加関数を、
新しい構造に合わせて書き直します。

from datetime import datetime

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 category is None or 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 input_and_add_memo(memos):
    text = input("メモ内容: ")
    category = input("カテゴリ(未入力なら未分類): ")
    tags_text = input("タグ(カンマ区切りで複数指定可。例: 買い物, 食料品): ")

    if tags_text.strip() == "":
        tags = []
    else:
        tags = [t.strip() for t in tags_text.split(",")]

    memo = create_memo(text, category, tags)
    if memo is None:
        print("空のメモは追加しません。")
        return

    memos.append(memo)
    print("メモを追加しました。")
Python

ここで深掘りしたいポイントは三つあります。

一つ目は、「入力値をそのまま信じない」ことです。
カテゴリが空なら "未分類" に置き換え、
タグは空文字を取り除いています。
これは「データの一貫性」を守るための大事な考え方です。

二つ目は、「tags を必ずリストにしている」ことです。
JSONでは配列、Pythonではリストとして扱うことで、
後で「タグで検索」「タグでフィルタ」がしやすくなります。

三つ目は、「メモの形をここで統一している」ことです。
アプリのどこからメモを見ても、
必ず text / created_at / important / category / tags / done が存在する、
という前提で話ができるようになります。


表示を“情報量のある形”に変える

カテゴリ・タグ・完了状態を見せる

メモの構造がリッチになったので、
表示もそれに合わせて変えます。

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

ここでの重要ポイントを丁寧に見ていきます。

memo.get("important")memo.get("done") を使っているのは、
古い JSON にそのキーがない場合でもエラーにしないためです。
get は、キーがなければ None を返します。

important_markdone_mark で、
「重要かどうか」「完了しているかどうか」を
一目で分かる記号に変えています。

カテゴリは <仕事> のように表示し、
タグは [買い物 / 食料品] のように並べています。

このように、
「内部のデータ構造」と「画面に見せる形」を分ける
というのが、とても大事な考え方です。


タグでフィルタする

「タグを含むメモだけ」を取り出す

タグを持たせたら、
「特定のタグを持つメモだけ見たい」という欲が出てきます。

そのためのフィルタ関数を作ります。

def filter_memos_by_tag(memos, tag):
    result = []
    for memo in memos:
        tags = memo.get("tags", [])
        if tag in tags:
            result.append(memo)
    return result
Python

ユーザー入力と組み合わせると、こうなります。

def input_and_show_by_tag(memos):
    tag = input("表示したいタグを入力してください: ")
    if tag == "":
        print("タグが空です。")
        return

    filtered = filter_memos_by_tag(memos, tag)
    if not filtered:
        print(f"タグ「{tag}」を持つメモはありません。")
        return

    print(f"=== タグ「{tag}」のメモ ===")
    show_memos(filtered)
Python

ここでのポイントは、

タグは「1対多」の関係(メモ1件にタグ複数)なので、
tags をリストとして扱い、
if tag in tags: で判定していることです。


カテゴリでグループ表示する

「カテゴリごとにまとめて表示する」感覚

カテゴリを持たせたら、
「カテゴリごとにメモを眺めたい」というニーズも出てきます。

まずは、カテゴリ一覧を集めます。

def collect_categories(memos):
    categories = set()
    for memo in memos:
        category = memo.get("category", "未分類")
        categories.add(category)
    return sorted(categories)
Python

次に、カテゴリごとにメモを表示します。

def show_memos_grouped_by_category(memos):
    if not memos:
        print("メモはまだありません。")
        return

    categories = collect_categories(memos)
    for category in categories:
        print(f"\n=== カテゴリ: {category} ===")
        group = [m for m in memos if m.get("category", "未分類") == category]
        show_memos(group)
Python

ここでの重要ポイントは、
「カテゴリ一覧を先に集めてから、カテゴリごとにフィルタする」
という流れです。

これは、データベースや集計処理でもよく使うパターンで、
「グルーピング」の基本的な考え方です。


完了フラグをタスク風に使う

メモを「やることリスト」としても使えるようにする

done フラグを持たせたので、
メモを「タスクっぽく」扱うこともできます。

完了フラグを切り替えるロジックは、重要フラグとほぼ同じです。

def toggle_done(memos, index):
    if index < 0 or index >= len(memos):
        return False
    memo = memos[index]
    memo["done"] = not memo.get("done", False)
    return True
Python

ユーザー入力と組み合わせると、こうなります。

def input_and_toggle_done(memos):
    if not memos:
        print("完了フラグを変更できるメモがありません。")
        return

    show_memos(memos)
    num_text = input("完了フラグを切り替えるメモの番号を入力してください: ")

    if not num_text.isdigit():
        print("数字を入力してください。")
        return

    num = int(num_text)
    index = num - 1

    if toggle_done(memos, index):
        print("完了フラグを切り替えました。")
    else:
        print("その番号のメモはありません。")
Python

さらに、「未完了のメモだけ」「完了済みのメモだけ」を表示することもできます。

def filter_done_memos(memos, done=True):
    result = []
    for memo in memos:
        if memo.get("done", False) == done:
            result.append(memo)
    return result
Python
def show_done_memos(memos):
    done_memos = filter_done_memos(memos, done=True)
    if not done_memos:
        print("完了したメモはありません。")
        return
    show_memos(done_memos)

def show_undone_memos(memos):
    undone_memos = filter_done_memos(memos, done=False)
    if not undone_memos:
        print("未完了のメモはありません。")
        return
    show_memos(undone_memos)
Python

6日目のミニアプリ:タグ・カテゴリ・完了付きメモJSONアプリ

全体像をまとめて眺める

ここまでの内容を組み合わせると、
6日目のアプリはこんな形になります(主要部分のみ)。

import json
import os
from datetime import datetime

FILENAME = "memos.json"

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

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

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 category is None or 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

def input_and_add_memo(memos):
    text = input("メモ内容: ")
    category = input("カテゴリ(未入力なら未分類): ")
    tags_text = input("タグ(カンマ区切り。例: 買い物, 食料品): ")
    if tags_text.strip() == "":
        tags = []
    else:
        tags = [t.strip() for t in tags_text.split(",")]
    memo = create_memo(text, category, tags)
    if memo is None:
        print("空のメモは追加しません。")
        return
    memos.append(memo)
    print("メモを追加しました。")

def toggle_important(memos, index):
    if index < 0 or index >= len(memos):
        return False
    memo = memos[index]
    memo["important"] = not memo.get("important", False)
    return True

def toggle_done(memos, index):
    if index < 0 or index >= len(memos):
        return False
    memo = memos[index]
    memo["done"] = not memo.get("done", False)
    return True

def filter_memos_by_tag(memos, tag):
    result = []
    for memo in memos:
        tags = memo.get("tags", [])
        if tag in tags:
            result.append(memo)
    return result

def collect_categories(memos):
    categories = set()
    for memo in memos:
        category = memo.get("category", "未分類")
        categories.add(category)
    return sorted(categories)

def show_memos_grouped_by_category(memos):
    if not memos:
        print("メモはまだありません。")
        return
    categories = collect_categories(memos)
    for category in categories:
        print(f"\n=== カテゴリ: {category} ===")
        group = [m for m in memos if m.get("category", "未分類") == category]
        show_memos(group)

def filter_done_memos(memos, done=True):
    result = []
    for memo in memos:
        if memo.get("done", False) == done:
            result.append(memo)
    return result

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

メニュー側で、

タグで絞る
カテゴリごとに表示する
完了・未完了だけ表示する

といった選択肢を用意すれば、
かなり“使えるメモアプリ”になってきます。


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

「JSONは、ただの保存形式ではなく“データ設計のキャンバス”」

今日の一番大事なポイントは、

メモにどんな情報を持たせるか
その情報をどう表現するか(文字列・リスト・フラグなど)
古いデータとの整合性をどう保つか

といった「データの形」を意識し始めたことです。

JSONは、
Pythonの dict / list をそのまま外に出したものです。
だからこそ、

構造を工夫すればするほど、
アプリの表現力も一緒に上がっていきます。

もし「サブメモを持たせたい」「チェックリストを入れたい」など
さらに複雑な構造を試したくなったら、
それはもう“データモデリング”の世界に足を踏み入れています。

7日目は、その集大成として
「完成形アプリ」と「データの一生」をまとめていきましょう。

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