🚀 Day 4:打造「具備持久化與完整 CRUD 功能的自動存檔記帳系統」
5 天全日營隊 (Day 4 / 總時數 6 Hours) | 解鎖硬碟永久記憶、CRUD 大滿貫與報表匯出
📖 為什麼學 these?——「拯救金魚腦!硬碟黑盒子」關卡冒險故事線
前三天我們的程式資料都暫存在記憶體(RAM)裡,只要關掉 VS Code 重新開機,剛記下的帳務就全消失了。今天我們要幫理財咖啡館安裝 **硬碟黑盒子(JSON 檔案)與災難復原機制**:
| 單元名稱 | 記帳程式關卡角色 | 為什麼這個單元不可或缺?(技術關聯) |
|---|---|---|
| 單元 1:持久化概念與 JSON | 💾 硬碟黑盒子與通用文字檔 | RAM 有金魚腦失憶症!學習將 Python 的 List/Dict 資料序列化(Serialization)轉換為跨語言通用的 JSON 文字格式。 |
| 單元 2:檔案讀寫 (with open) | 📖 自動關檔的保險箱筆記本 | 使用 with open 安全讀寫檔案,不用擔心忘記關檔(f.close)造成資料遺失,並加上 UTF-8 防亂碼! |
| 單元 3:開機自動讀檔與防呆 | 🛰️ 無縫開機探針檢測 | 首次開機時 expenses.json 還不存在!透過 os.path.exists() 先做探針檢測,預防開機直接當機。 |
| 單元 4:CRUD 刪除與修改實作 | 🛠️ 完整增刪查改大滿貫 (CRUD) | 記錯金額怎麼辦?實作 Complete CRUD(新增 C、讀取 R、修改 U、刪除 D),掌握現代軟體最核心的四大操作。 |
| 單元 5:資料備份與報表匯出 | 📄 老闆最愛的財務月報表 (.txt) | 將 JSON 資料加工導出一份排版整齊的純文字財務月報表(.txt),體驗商業軟體匯出報表的功能! |
| 單元 6:破壞性測試與容錯修復 | 🚑 黑盒子災難復原機制 (os.rename) | JSON 檔被誤刪大括號損毀了!撰寫容錯(Fault Tolerance)復原程式,將壞檔備份改名並自動重置系統。 |
📌 課程前導與簡介說明
- 營隊定位:達成**「資料持久化(Data Persistence)」**,讓程式關機後資料依然完好如初。
- 高中生心理特性:體驗關閉 Terminal 重開資料還在的震撼,完成 Complete CRUD 實作。
- Django 銜接點:JSON 讀寫對應 Django ORM 的
.save()與.delete(),開機自動讀檔對應資料庫連線與 Serialization。
歡迎來到 Day 4!今天我們要把記帳資料永久保存在硬碟裡!請閱讀每個單元的 **故事單元說明**,點擊 **延伸閱讀** 展開深層觀念與 Google 搜尋,並善用每區塊的 **AI 協作除錯提示詞**!
💻 Day 4 專案總成果:具備持久化與完整 CRUD 功能的自動存檔記帳系統
【教師參考解答】學生於 Day 4 結束時將能獨立撰寫的完整程式:
"""
Day 4 最終成果:具備持久化與完整 CRUD 功能的自動存檔記帳系統
核心概念:JSON 檔案序列化/反序列化 -> 開機自動讀檔 -> CRUD 操作 -> 報表匯出
"""
import json
import os
import datetime
DATA_FILE = "expenses.json"
# ==========================================
# 💾 資料持久化模組 (對應 DB Access & ORM Persistence)
# ==========================================
def load_data():
""" 開機自動讀檔 (模擬 DB Connection & QuerySet Fetch) """
if not os.path.exists(DATA_FILE):
return []
try:
with open(DATA_FILE, "r", encoding="utf-8") as f:
return json.load(f)
except (json.JSONDecodeError, Exception) as e:
print(f"⚠️ 資料檔讀取異常或格式損毀 ({e}),已啟動預防性備份並重置清單。")
return []
def save_data(records):
""" 寫入 JSON 檔案 (模擬 DB .save() 寫入與 Commit) """
with open(DATA_FILE, "w", encoding="utf-8") as f:
json.dump(records, f, ensure_ascii=False, indent=4)
# ==========================================
# 🚀 系統主程式與 CRUD 路由
# ==========================================
records = load_data()
print(f"📂 開機成功!已自動從 [{DATA_FILE}] 載入 {len(records)} 筆歷史紀錄。")
while True:
print("\n" + "=" * 45)
print(" 💾 持久化 CRUD 終端機記帳系統 💾 ")
print("=" * 45)
print(" [1] ➕ 新增消費紀錄 (Create)")
print(" [2] 📊 查看所有明細 (Read)")
print(" [3] ✏️ 修改紀錄金額 (Update)")
print(" [4] ❌ 刪除特定紀錄 (Delete)")
print(" [5] 📑 匯出財務月報表 (.txt)")
print(" [6] 🚪 儲存並離開系統")
print("=" * 45)
choice = input("👉 請選擇功能選項 (1-6): ").strip()
if choice == "1":
# --- [C]reate 新增 ---
item = input("👉 請輸入品項名稱: ").strip()
if not item:
print("⚠️ 名稱不能為空!")
continue
try:
price = int(input("👉 請輸入金額: "))
except ValueError:
print("❌ 錯誤:金額必須是純數字!")
continue
cat = input("👉 請輸入分類 (預設: 一般): ").strip() or "一般"
today = datetime.date.today().strftime("%Y-%m-%d")
records.append({"name": item, "price": price, "category": cat, "date": today})
save_data(records)
print(f"✅ 已新增並自動存檔:{item} ${price} 元 [{cat}]")
elif choice == "2":
# --- [R]ead 讀取列表 ---
if not records:
print("📭 目前尚無任何記帳紀錄!")
continue
print("\n--- 📊 完整記帳明細列表 ---")
total = 0
for idx, r in enumerate(records, start=1):
print(f"[{idx}] {r['date']} | {r['name']:<10} | ${r['price']:<5} | [{r['category']}]")
total += r["price"]
print("-" * 45)
print(f"💵 總計累計金額:${total} 元")
elif choice == "3":
# --- [U]pdate 修改 ---
if not records:
print("📭 沒有可修改的資料!")
continue
print("\n--- ✏️ 修改紀錄金額 ---")
for idx, r in enumerate(records, start=1):
print(f"[{idx}] {r['name']} - 當前金額: ${r['price']}")
try:
target_idx = int(input("👉 請輸入要修改的編號: ")) - 1
if 0 <= target_idx < len(records):
new_price = int(input(f"👉 請輸入 [{records[target_idx]['name']}] 的新金額: "))
records[target_idx]["price"] = new_price
save_data(records)
print("✅ 修改完成並已更新檔案!")
else:
print("⚠️ 輸入編號超出範圍!")
except ValueError:
print("❌ 錯誤:請輸入有效的數字編號與金額!")
elif choice == "4":
# --- [D]elete 刪除 ---
if not records:
print("📭 沒有可刪除的資料!")
continue
print("\n--- ❌ 刪除特定紀錄 ---")
for idx, r in enumerate(records, start=1):
print(f"[{idx}] {r['name']} - ${r['price']} ({r['category']})")
try:
target_idx = int(input("👉 請輸入要刪除的編號: ")) - 1
if 0 <= target_idx < len(records):
deleted = records.pop(target_idx)
save_data(records)
print(f"🗑️ 已成功刪除:{deleted['name']} ${deleted['price']} 元")
else:
print("⚠️ 輸入編號超出範圍!")
except ValueError:
print("❌ 錯誤:請輸入有效的數字編號!")
elif choice == "5":
# --- 匯出文字報表 ---
report_file = "monthly_report.txt"
with open(report_file, "w", encoding="utf-8") as f:
f.write("=== 📊 個人財務月報表 ===\n")
f.write(f"匯出日期: {datetime.date.today()}\n")
f.write("-----------------------------------\n")
tot = 0
for r in records:
f.write(f"• {r['date']} | {r['name']:<10} | ${r['price']}\n")
tot += r["price"]
f.write("-----------------------------------\n")
f.write(f"總累計花費: ${tot} 元\n")
print(f"📄 成功匯出財務報表至檔名:[{report_file}]")
elif choice == "6":
save_data(records)
print("\n💾 系統資料已保存,期待下次運算!再見!👋")
break
else:
print("⚠️ 無效的選項,請輸入數字 1 至 6!")
【學生自主實作框架】請根據今日各單元所學,將空缺的 `____` 填入正確程式碼:
"""
🎯 Day 4 自主挑戰:請將下方的 ____ 填入正確的 JSON、with open 與 pop 語法!
"""
import json
import os
DATA_FILE = "expenses.json"
# 1. 寫檔存檔函式
def save_data(records):
# 提示:開啟檔案寫入 mode='w' 並指定 utf-8 編碼
with open(DATA_FILE, "____", encoding="utf-8") as f:
# 提示:將物件寫入檔案的 json 函式
json.____(records, f, ensure_ascii=False, indent=4)
# 2. 開機讀檔防呆
def load_data():
if not os.path.____(DATA_FILE): # 提示:檢查檔案是否存在的函數
return []
with open(DATA_FILE, "r", encoding="utf-8") as f:
return json.____(f) # 提示:從檔案載入 json 物件的函數
records = load_data()
# 3. 刪除資料 (Delete)
target_idx = int(input("輸入要刪除的編號 (1-based): ")) - 1
if 0 <= target_idx < len(records):
deleted = records.____(target_idx) # 提示:清單移除指定索引項目的方法
save_data(records)
print("✅ 已刪除並自動存檔!")
⏰ 單元 1:持久化概念與 JSON 解析(09:00 - 10:00)
電腦的記憶體(RAM)像是「快閃記憶卡」,速度極快但一斷電資料就全消失了!而硬碟(Disk)則是「金鋼不壞保險箱」。為了把 Python 的清單與字典存進硬碟,我們需要將它們轉換成跨語言通用的文字格式 **JSON (JavaScript Object Notation)**。這個過程稱為 **「序列化 (Serialization)」**!
將記憶體中的物件結構轉化為可儲存或傳輸的位元組/字串過程稱為序列化:
json.dumps()與json.loads():前者將 Python 物件轉為 JSON 字串;後者將 JSON 字串解析還原。- 關鍵參數
ensure_ascii=False:防止中文字元被強制轉換為萬國碼轉義字串 (如 `\u73cd\u4e76`)。
🔗 Google 搜尋關鍵字 (依相關度排序,紫羅蘭標記為進階內容,點擊開啟新分頁):
🤖 AI 自主學習探索提示詞 (Prompt):
請向高中生說明 RAM(記憶體)與 Hard Disk(硬碟)的差別,並解釋什麼是 JSON 格式以及序列化 (Serialization) 的運作機制。
- 比較 RAM (揮發性) 與 Disk (持久性)。
- 介紹 JSON 格式:List 映為 Array
[...],Dict 映為 Object{...}。 - Django 銜接點:Web API 序列化 (Serialization) 與資料庫儲存。
import json
# Python 結構轉換為 JSON 字串 (Serialization)
data = [{"name": "珍奶", "price": 60}]
json_str = json.dumps(data, ensure_ascii=False, indent=4)
print("轉換後的 JSON 字串:\n", json_str)
# JSON 字串解析還原為 Python List/Dict (Deserialization)
parsed_data = json.loads(json_str)
print("還原後的型別:", type(parsed_data)) #
- 漏寫 ensure_ascii=False:印出中文時全變成
\u73cd\u4e76亂碼轉義符。
我在用 json.dumps() 轉化包含中文的字典時,結果印出了 \u73cd\u4e76 這種 unicode 碼。請教我如何在語法中加入 ensure_ascii=False 來保持中文字顯示。
⏰ 單元 2:檔案讀寫 (with open)(10:00 - 11:00)
傳統寫檔如果忘記寫 f.close(),檔案會被作業系統鎖死甚至壞掉!with open(...) as f: 語法就像是「自動安全筆記本」,只要離開這塊程式碼,電腦就會自動幫你把筆記本合上蓋好。記得寫檔時永遠要加上 encoding="utf-8" 跨平台防亂碼防護!
with 關鍵字引進了 Python 安全的資源管理機制:
- 自動資源釋放:無論區塊內是否發生 Exception,
with語法皆保證觸發__exit__自動關閉檔案控制代碼。 json.dump(obj, f):直接將物件序列化寫入檔案指標;json.load(f)從檔案讀取還原。
🔗 Google 搜尋關鍵字 (依相關度排序,紫羅蘭標記為進階內容,點擊開啟新分頁):
🤖 AI 自主學習探索提示詞 (Prompt):
請說明為什麼在 Windows 系統用 Python 讀寫文字檔時一定要加上 encoding='utf-8',以及 with open 語法是如何自動幫我們安全關閉檔案 (close) 的。
- 採用 Context Manager:
with open(...) as f:最佳工程實踐。 - 顯式指定
encoding="utf-8"避免 Windows cp950 亂碼。 - Django 銜接點:後端進行 I/O 連線與讀寫資料庫檔案。
import json
records = [{"name": "排骨便當", "price": 110}]
# 2-1: 寫入 JSON 檔案
with open("expenses.json", "w", encoding="utf-8") as f:
json.dump(records, f, ensure_ascii=False, indent=4)
print("💾 資料已成功寫入 expenses.json!")
# 2-2: 從 JSON 檔案讀取
with open("expenses.json", "r", encoding="utf-8") as f:
loaded_records = json.load(f)
print("📂 讀取出來的資料:", loaded_records)
- 未指定 UTF-8 編碼:Windows 系統跳出
UnicodeDecodeError: 'cp950' codec can't decode...崩潰。
我在開啟檔案時跳出了 UnicodeDecodeError cp950 錯誤。請協助我檢查 open() 語法內部是否漏寫了 encoding='utf-8'。
⏰ 單元 3:開機自動讀檔與防呆(11:00 - 12:00)
當你第一次下載執行程式時,硬碟裡的 expenses.json 根本還不存在!如果直接強制讀取,程式就會跳出 FileNotFoundError 閃退。我們需要先發射雷達探針 os.path.exists("expenses.json"):若檔案存在就讀取;不存在就自動建立全新的空清單 []!
預防性程式設計的核心在於預先排除潛在的執行階段錯誤(Runtime Error):
os.path.exists(path):回傳布林值,檢查檔案或目錄路徑是否存在。- 無縫初始化 (Seamless Initialization):確保第一次軟體部署或檔案缺失時,系統能自動降級初始化不崩潰。
🔗 Google 搜尋關鍵字 (依相關度排序,紫羅蘭標記為進階內容,點擊開啟新分頁):
🤖 AI 自主學習探索提示詞 (Prompt):
我的 Python 程式一開機就因為 FileNotFoundError 崩潰。請教我如何用 os.path.exists() 先做檔案檢測,確保即使檔案不存在也能安全初始化。
- 探針檢測:
os.path.exists("expenses.json")避免開機破防。 - 軟體初始化流:若不存在傳回
[]。 - Django 銜接點:Django 啟動時自動檢查 DB 連線與 Migration 狀態。
import json
import os
DATA_FILE = "expenses.json"
def safe_load():
# 檢查檔案是否存在
if not os.path.exists(DATA_FILE):
print("ℹ️ 未檢測到歷史檔案,初始化全新記帳本。")
return []
try:
with open(DATA_FILE, "r", encoding="utf-8") as f:
return json.load(f)
except Exception as e:
print(f"⚠️ 讀取失敗,原因:{e}")
return []
records = safe_load()
print(f"系統順利啟動,目前包含 {len(records)} 筆紀錄。")
- 無視檔案存在直接讀檔:跳出
FileNotFoundError: [Errno 2] No such file or directory: 'expenses.json'。
我在讀取 json 檔案時跳出了 FileNotFoundError。請協助我撰寫一段包含 os.path.exists() 判斷式的 safe_load() 函式來解決這個開機崩潰問題。
⏰ 單元 4:CRUD 刪除與修改實作(13:00 - 14:00)
記錯金額或買錯東西怎麼辦?今天我們要解鎖所有軟體工程最核心的能力:**CRUD (Create 增、Read 查、Update 改、Delete 刪)**!特別注意:使用者看的編號是從 1 開始,但 Python 索引是從 0 開始,記得要做好 target_idx = user_input - 1 的換算喔!
CRUD 代表持久化儲存的四大基本功能:
- [C]reate:
append()+ 寫入檔案。[R]ead:走訪列印列表。 - [U]pdate:修改指定索引項目
records[idx]["price"] = new_price。 - [D]elete:
records.pop(idx)移除項目並自動同步存檔。 - Off-by-One 差一轉換:將人類習慣的 1-based 輸入減 1 轉為電腦的 0-based 索引。
🔗 Google 搜尋關鍵字 (依相關度排序,紫羅蘭標記為進階內容,點擊開啟新分頁):
🤖 AI 自主學習探索提示詞 (Prompt):
向初學者解釋什麼是 CRUD,以及在 Python 清單中透過索引(index)進行刪除 pop() 與修改時,如何處理使用者輸入的編號與內部 0-based 索引的轉換。
- 完整 CRUD:Create (append), Read (for), Update (dict key), Delete (pop)。
- Off-By-One 差一轉換:
idx = user_input - 1。 - Django 銜接點:Django ORM 的
.save()與.delete()。
records = [
{"name": "珍奶", "price": 60},
{"name": "便當", "price": 100}
]
# 刪除特定索引紀錄 (Delete)
del_idx = int(input("請輸入要刪除的編號 (1-based): ")) - 1
if 0 <= del_idx < len(records):
deleted_item = records.pop(del_idx)
print(f"🗑️ 已成功刪除:{deleted_item['name']}")
else:
print("⚠️ 無效的編號!")
- 未進行索引換算:使用者想刪第 1 筆輸入 1,結果刪除了第 2 筆 (索引 1 的項目)。
我在實作刪除功能時,點選刪除第 1 筆結果程式卻刪除了第 2 筆資料。請幫我檢查是否有做 target_idx = user_input - 1 的轉換。
⏰ 單元 5:專案實作:資料備份機制(14:00 - 15:00)
除了給電腦看的 JSON 資料外,老闆與使用者通常需要一份「可以直接列印閱讀的純文字報表」。這個單元我們會用 with open("report.txt", "w") 搭配走訪格式化,將記憶體中的帳務資料導出一份美觀的 `.txt` 財務月報!
將資料結構加工輸出為人類可讀文件是軟體應用的重要一環:
f.write()的換行關鍵:與print()不同,f.write()**不會自動換行**,必須手動加上\n。- 格式化排版:結合 f-string 置中或對齊語法,導出完美的純文字表格報表。
🔗 Google 搜尋關鍵字 (依相關度排序,紫羅蘭標記為進階內容,點擊開啟新分頁):
🤖 AI 自主學習探索提示詞 (Prompt):
我想寫一個 Python 函式將記帳清單轉成一份格式整齊的 .txt 月報表,包含項目、金額與總計,請給我優雅的範例程式碼並說明 f.write() 換行 (\n) 的注意事項。
- 將 JSON 加工導出純文字 `.txt` 報表。
- 強調
f.write()不會自動換行,需加\n。 - Django 銜接點:Web 後台「匯出 CSV/PDF 報表」與資料庫備份機制。
records = [
{"date": "2026-08-03", "name": "珍珠奶茶", "price": 60},
{"date": "2026-08-03", "name": "排骨便當", "price": 110}
]
# 匯出排版報表
with open("report.txt", "w", encoding="utf-8") as f:
f.write("=== 📊 財務月報表 ===\n")
total = 0
for r in records:
f.write(f"• {r['date']} | {r['name']:<8} | ${r['price']} 元\n")
total += r["price"]
f.write(f"\n總累計消費:${total} 元\n")
print("📄 報表匯出成功!請查看 report.txt 檔案。")
- 忘記手動換行:匯出的 `.txt` 檔案所有文字通通黏在同一行無法閱讀。
我在用 f.write() 寫入 .txt 檔案時,導出的文字通通黏在同一行沒有換行。請幫我檢查是否漏寫了 \n 換行符號。
⏰ 單元 6:破壞性測試與容錯修復(15:00 - 16:00)
在現實世界中,檔案可能因為突然斷電或被不小心誤刪大括號而格式毀損!當 expenses.json 壞掉時,json.load() 會拋出 JSONDecodeError 當機。真正的工程師會寫出 **容錯修復 (Fault Tolerance)** 程式:自動捕捉這個錯誤,把壞掉的檔案改名備份(os.rename),並重置一個乾淨的全新帳本!
高可靠度軟體具備良好的自我修復與降級運作能力:
json.decoder.JSONDecodeError:捕捉 JSON 格式語法毀損的特定例外。- 備份離場
os.rename("expenses.json", "corrupted.bak"):保護使用者壞掉的舊資料不被直接覆蓋覆滅。
🔗 Google 搜尋關鍵字 (依相關度排序,紫羅蘭標記為進階內容,點擊開啟新分頁):
🤖 AI 自主學習探索提示詞 (Prompt):
如果 JSON 資料檔被意外損毀導致開機時跳出 JSONDecodeError,如何寫出包含捕捉此例外、自動用 os.rename() 備份舊檔,並重新初始化清單的容錯修復機制?
- 讓學生刻意刪除
expenses.json內部大括號,觀察崩潰訊息。 - 捕捉
json.JSONDecodeError,並用os.rename()安全離場備份。 - Django 銜接點:資料庫 Migration 失敗防護與災難復原 (Disaster Recovery)。
import json
import os
DATA_FILE = "expenses.json"
def resilient_load():
if not os.path.exists(DATA_FILE):
return []
try:
with open(DATA_FILE, "r", encoding="utf-8") as f:
return json.load(f)
except json.JSONDecodeError:
print("🚨 警告:檢測到 JSON 檔案格式毀損!")
# 自動修復邏輯:將壞掉的檔案改名備份,並重置全新檔案
os.rename(DATA_FILE, "expenses_corrupted.bak")
print("🛠️ 已將損毀檔案備份為 expenses_corrupted.bak,並重置系統清單。")
return []
records = resilient_load()
- 未捕捉 JSONDecodeError:檔案損毀後開機必當機拋出
json.decoder.JSONDecodeError。
我的 JSON 檔案毀損導致程式開機跳出 json.decoder.JSONDecodeError。請教我如何用 except json.JSONDecodeError: 捕捉它並自動改名備份。