昨天我們讓程式學會把資料寫進檔案,但用的是純文字檔,一行一個待辦事項。
問題來了:如果一筆待辦不只有「標題」,還有「優先度」、「是否完成」、「建立時間」呢?
用 txt 檔的話,我們可能會想到這種土法煉鋼的做法:
買牛奶,高,False,2026-09-24
寫鐵人賽文章,高,True,2026-09-24
然後讀的時候 line.split(",") 拆開。看起來可以,但只要遇到「買牛奶, 順便買麵包」這種標題裡有逗號的情況,整個就崩了。而且第三欄的 "False" 讀回來是字串不是布林值,還要自己轉。
這種問題有標準答案,就是今天的主角:JSON。
JSON(JavaScript Object Notation)是一種文字格式的資料交換標準。雖然名字裡有 JavaScript,但現在幾乎所有語言、所有 API 都在用它。
它長這樣:
{
"name": "Alice",
"age": 25,
"is_student": false,
"hobbies": ["閱讀", "跑步"],
"address": {
"city": "台北",
"district": "大安區"
}
}
有沒有覺得很眼熟?它跟 Python 的 Dictionary 幾乎一模一樣。
這就是 JSON 好用的原因——它的結構天然對應到我們已經會的資料型態:
| JSON | Python |
|---|---|
object {...} |
dict |
array [...] |
list |
string "..." |
str |
number 25 / 3.14 |
int / float |
true / false |
True / False |
null |
None |
只要記住三個差異:布林值是小寫的 true/false、空值是 null、字串一律用雙引號(JSON 不接受單引號)。
Python 內建 json 模組,不用安裝。核心只有四個函式,而且名字有規律:
| 函式 | 方向 | 對象 |
|---|---|---|
json.dumps() |
Python → JSON 字串 | 字串 |
json.loads() |
JSON 字串 → Python | 字串 |
json.dump() |
Python → JSON 檔案 | 檔案 |
json.load() |
JSON 檔案 → Python | 檔案 |
記法:s 代表 string。有 s 的處理字串,沒 s 的處理檔案。dump 是倒出去(寫),load 是載進來(讀)。
import json
data = {
"name": "Alice",
"age": 25,
"hobbies": ["閱讀", "跑步"],
}
# Python → JSON 字串
text = json.dumps(data, ensure_ascii=False, indent=2)
print(text)
輸出:
{
"name": "Alice",
"age": 25,
"hobbies": [
"閱讀",
"跑步"
]
}
兩個很重要的參數:
ensure_ascii=False:不加的話,中文會變成 "阅讀" 這種跳脫字元。雖然電腦讀得懂,但人看不懂。處理中文一定要加。
indent=2:加上縮排讓人類好讀。不加的話會全部擠成一行(檔案比較小,適合傳輸;要存檔給人看就加)。反過來:
text = '{"name": "Alice", "age": 25}'
data = json.loads(text)
print(type(data)) # <class 'dict'>
print(data["name"]) # Alice
print(data["age"] + 1) # 26 ← 是真的數字,不是字串
注意 data["age"] + 1 可以直接運算——JSON 幫我們把型態保留下來了。這是純文字檔做不到的。
import json
data = {"name": "Alice", "age": 25}
# 寫檔
with open("profile.json", "w", encoding="utf-8") as f:
json.dump(data, f, ensure_ascii=False, indent=2)
# 讀檔
with open("profile.json", "r", encoding="utf-8") as f:
loaded = json.load(f)
print(loaded) # {'name': 'Alice', 'age': 25}
跟昨天的 with open() 完全一樣,只是把 f.write() 換成 json.dump(data, f)。
把昨天的程式升級成 JSON 版:
import json
from datetime import datetime
from pathlib import Path
DATA_DIR = Path("data")
TODO_FILE = DATA_DIR / "todos.json"
def load_todos():
"""讀取待辦清單。檔案不存在或格式壞掉時回傳空清單。"""
if not TODO_FILE.exists():
return []
try:
with open(TODO_FILE, "r", encoding="utf-8") as f:
return json.load(f)
except json.JSONDecodeError:
print("⚠️ 待辦檔案格式有問題,當作空的處理")
return []
def save_todos(todos):
"""把待辦清單寫回 JSON 檔。"""
DATA_DIR.mkdir(exist_ok=True)
with open(TODO_FILE, "w", encoding="utf-8") as f:
json.dump(todos, f, ensure_ascii=False, indent=2)
def add_todo(title, priority="中"):
if not title.strip():
raise ValueError("標題不能是空的")
if priority not in ("高", "中", "低"):
raise ValueError(f"優先度只能是 高/中/低,收到的是「{priority}」")
todos = load_todos()
todos.append({
"id": len(todos) + 1,
"title": title.strip(),
"priority": priority,
"done": False,
"created_at": datetime.now().isoformat(timespec="seconds"),
})
save_todos(todos)
return todos[-1]
def complete_todo(todo_id):
todos = load_todos()
for todo in todos:
if todo["id"] == todo_id:
todo["done"] = True
save_todos(todos)
return todo
raise ValueError(f"找不到 id={todo_id} 的待辦")
# 試跑
add_todo("買牛奶", "高")
add_todo("寫鐵人賽文章", "高")
add_todo("整理房間", "低")
complete_todo(2)
for todo in load_todos():
mark = "✓" if todo["done"] else " "
print(f"[{mark}] #{todo['id']} {todo['title']}({todo['priority']})")
輸出:
[ ] #1 買牛奶(高)
[✓] #2 寫鐵人賽文章(高)
[ ] #3 整理房間(低)
打開 data/todos.json 看看:
[
{
"id": 1,
"title": "買牛奶",
"priority": "高",
"done": false,
"created_at": "2026-09-24T21:30:15"
},
...
]
**這個檔案人看得懂,程式也讀得回來,而且型態不會跑掉。**這就是結構化資料的價值。
注意我加了 except json.JSONDecodeError——如果有人手動去改這個檔案,改壞了格式,程式不會直接爆掉。這是 Day 8 的觀念延續。
JSON 不是萬能的,有些 Python 物件它裝不下:
import json
from datetime import datetime
json.dumps({"now": datetime.now()})
# TypeError: Object of type datetime is not JSON serializable
datetime、set、自訂的 class,JSON 都不認識。常見解法是先轉成字串:
data = {"now": datetime.now().isoformat()}
json.dumps(data) # {"now": "2026-09-24T21:30:15.123456"}
讀回來的時候再轉回去:
from datetime import datetime
dt = datetime.fromisoformat(data["now"])
上面的待辦清單我就是用 isoformat() 來存時間的。ISO 8601 格式(2026-09-24T21:30:15)是跨語言的標準,而且字串排序等於時間排序,很好用。
另外,字典的 key 在 JSON 裡一定是字串:
json.loads(json.dumps({1: "a"}))
# {'1': 'a'} ← 數字 key 變成字串了
這個坑很隱晦,要注意。
今天的內容看起來只是「另一種存檔方式」,但 JSON 其實是整個 Agent 架構的通用語言。接下來每一天幾乎都會用到它:
1. API 的回應格式(Day 13–15)
幾乎所有 Web API 回傳的都是 JSON。中央氣象署的天氣資料、Google 行事曆、Notion、Slack——全都是 JSON。我們要串任何服務,第一步就是 json.loads()。
2. AI 的結構化輸出(Day 20)
這點最關鍵。我們希望 AI 回答的不只是一段文字,而是程式可以直接用的資料。
比較一下:
使用者:幫我記一下,明天下午三點跟客戶開會
# 如果 AI 回傳文字:
「好的,我幫你記下明天下午三點跟客戶開會。」
→ 程式看得懂嗎?要用正規表示式去抓「明天」「下午三點」嗎?惡夢。
# 如果 AI 回傳 JSON:
{
"action": "create_event",
"title": "跟客戶開會",
"date": "2026-09-25",
"time": "15:00"
}
→ json.loads() 之後直接就能用。
**這個差別,就是「聊天機器人」和「Agent」的分界線。**一個只能講話,一個能做事。
3. 工具的定義與呼叫(Day 21–22)
當我們告訴 AI「你可以使用這些工具」,工具的定義本身就是 JSON Schema:
tool = {
"name": "get_weather",
"description": "查詢指定城市的今日天氣",
"input_schema": {
"type": "object",
"properties": {
"city": {"type": "string", "description": "城市名稱,例如「台北」"}
},
"required": ["city"],
},
}
而 AI 決定要呼叫工具時,它回傳的參數也是 JSON。
4. 狀態的保存(Day 26)
Agent 的記憶、對話歷史、使用者偏好,全部都可以序列化成 JSON 存檔。
幫生活助理做一個記憶檔,先建立雛形:
import json
from pathlib import Path
PREF_FILE = Path("data/preferences.json")
DEFAULT_PREFS = {
"name": None,
"city": "台北",
"wake_up_time": "07:00",
"language": "zh-TW",
"dietary_restrictions": [],
"reminder_enabled": True,
}
def load_preferences():
"""讀取偏好設定,缺少的欄位用預設值補上。"""
if not PREF_FILE.exists():
return DEFAULT_PREFS.copy()
try:
with open(PREF_FILE, "r", encoding="utf-8") as f:
saved = json.load(f)
except json.JSONDecodeError:
return DEFAULT_PREFS.copy()
# 用預設值當底,再把存檔的內容蓋上去
prefs = DEFAULT_PREFS.copy()
prefs.update(saved)
return prefs
def save_preferences(prefs):
PREF_FILE.parent.mkdir(exist_ok=True)
with open(PREF_FILE, "w", encoding="utf-8") as f:
json.dump(prefs, f, ensure_ascii=False, indent=2)
prefs = load_preferences()
prefs["name"] = "pzhiqi"
prefs["dietary_restrictions"] = ["不吃牛肉"]
save_preferences(prefs)
print(load_preferences())
prefs.update(saved) 這招很實用——先鋪一層預設值,再把使用者存過的蓋上去。這樣以後我新增一個設定項目,舊的存檔也不會因為缺欄位而出錯。
dumps/loads 處理字串,dump/load 處理檔案,有 s 的是 stringensure_ascii=False,要人看得懂記得 indent=2
datetime 之類的物件要先轉成字串(建議用 isoformat())明天要講用 Class 描述一件事——當資料的結構越來越複雜,我們需要一個更好的方式來組織它。