JSON(JavaScript Object Notation)是現代應用程式中最常用的資料交換格式。Python 的 json 模組提供了簡單的 API 來處理 JSON 資料。

基本操作

序列化(Python 物件 → JSON 字串)

 1import json
 2
 3# 字典轉 JSON 字串
 4data = {"name": "Python", "version": 3.11}
 5json_str = json.dumps(data)
 6# '{"name": "Python", "version": 3.11}'
 7
 8# 格式化輸出
 9json_str = json.dumps(data, indent=2)
10# {
11#   "name": "Python",
12#   "version": 3.11
13# }

反序列化(JSON 字串 → Python 物件)

1import json
2
3json_str = '{"name": "Python", "version": 3.11}'
4data = json.loads(json_str)
5# {'name': 'Python', 'version': 3.11}

檔案讀寫

1import json
2
3# 寫入檔案
4with open("config.json", "w", encoding="utf-8") as f:
5    json.dump(data, f, indent=2)
6
7# 讀取檔案
8with open("config.json", "r", encoding="utf-8") as f:
9    data = json.load(f)

實際範例:Hook 系統

Hook 輸入讀取

來自 .claude/lib/hook_io.py

 1import json
 2import sys
 3
 4def read_hook_input() -> dict:
 5    """
 6    從 stdin 讀取 Hook 輸入
 7
 8    Returns:
 9        dict: 解析後的 JSON 資料,解析失敗時返回空字典
10    """
11    try:
12        return json.load(sys.stdin)
13    except json.JSONDecodeError:
14        return {}
15    except Exception:
16        return {}

Hook 輸出寫入

 1def write_hook_output(
 2    output: dict,
 3    ensure_ascii: bool = False,
 4    indent: int = 2
 5) -> None:
 6    """
 7    輸出 Hook 結果到 stdout
 8
 9    Args:
10        output: 要輸出的字典
11        ensure_ascii: 是否確保 ASCII 編碼
12        indent: JSON 縮排空格數
13    """
14    print(json.dumps(output, ensure_ascii=ensure_ascii, indent=indent))

重要參數

ensure_ascii

控制是否將非 ASCII 字元轉換為跳脫序列:

 1import json
 2
 3data = {"message": "你好"}
 4
 5# ensure_ascii=True(預設)
 6json.dumps(data)
 7# '{"message": "\\u4f60\\u597d"}'
 8
 9# ensure_ascii=False(保留原字元)
10json.dumps(data, ensure_ascii=False)
11# '{"message": "你好"}'

在 Hook 系統中,我們使用 ensure_ascii=False 來保留中文字元。

indent

控制輸出的縮排:

 1data = {"name": "Python", "features": ["simple", "readable"]}
 2
 3# 無縮排
 4json.dumps(data)
 5# '{"name": "Python", "features": ["simple", "readable"]}'
 6
 7# 有縮排
 8json.dumps(data, indent=2)
 9# {
10#   "name": "Python",
11#   "features": [
12#     "simple",
13#     "readable"
14#   ]
15# }

sort_keys

按鍵名排序輸出:

1data = {"z": 1, "a": 2, "m": 3}
2
3json.dumps(data, sort_keys=True)
4# '{"a": 2, "m": 3, "z": 1}'

default

處理無法序列化的物件:

 1from datetime import datetime
 2import json
 3
 4def json_serializer(obj):
 5    if isinstance(obj, datetime):
 6        return obj.isoformat()
 7    raise TypeError(f"Object of type {type(obj)} is not JSON serializable")
 8
 9data = {"timestamp": datetime.now()}
10json.dumps(data, default=json_serializer)
11# '{"timestamp": "2024-01-20T15:30:00"}'

型別對應

Python 型別JSON 型別
dictobject
list, tuplearray
strstring
int, floatnumber
Truetrue
Falsefalse
Nonenull

常見錯誤處理

JSONDecodeError

1import json
2
3def safe_parse_json(json_str: str) -> dict:
4    """安全解析 JSON,失敗時返回空字典"""
5    try:
6        return json.loads(json_str)
7    except json.JSONDecodeError as e:
8        print(f"JSON 解析錯誤: {e}")
9        return {}

無法序列化的物件

 1import json
 2from dataclasses import dataclass, asdict
 3
 4@dataclass
 5class Config:
 6    name: str
 7    timeout: int
 8
 9config = Config("test", 30)
10
11# 錯誤:dataclass 無法直接序列化
12# json.dumps(config)  # TypeError
13
14# 正確:轉換為字典
15json.dumps(asdict(config))
16# '{"name": "test", "timeout": 30}'

實際應用:配置檔案載入

來自 .claude/lib/config_loader.py

1def _load_json_file(file_path: Path) -> dict:
2    """載入 JSON 檔案"""
3    import json
4    with open(file_path, "r", encoding="utf-8") as f:
5        return json.load(f)

與 YAML 的比較

Hook 系統同時支援 JSON 和 YAML:

 1# 嘗試導入 PyYAML,如果失敗則使用 JSON 作為備案
 2try:
 3    import yaml
 4    HAS_YAML = True
 5except ImportError:
 6    HAS_YAML = False
 7    import json
 8
 9def load_config(config_name: str) -> dict:
10    if yaml_path.exists() and HAS_YAML:
11        return _load_yaml_file(yaml_path)
12    elif json_path.exists():
13        return _load_json_file(json_path)

最佳實踐

1. 總是指定編碼

1# 好
2with open("data.json", "w", encoding="utf-8") as f:
3    json.dump(data, f)
4
5# 不好(可能在不同系統有不同行為)
6with open("data.json", "w") as f:
7    json.dump(data, f)

2. 處理解析錯誤

1def read_config(path: str) -> dict:
2    try:
3        with open(path, "r", encoding="utf-8") as f:
4            return json.load(f)
5    except FileNotFoundError:
6        return {}
7    except json.JSONDecodeError:
8        return {}

3. 使用 ensure_ascii=False 處理中文

1# 輸出中文友好的 JSON
2json.dumps(data, ensure_ascii=False, indent=2)

思考題

  1. json.dump()json.dumps() 有什麼區別?
  2. 為什麼 Hook 系統的 read_hook_input() 捕獲 JSONDecodeError 後返回空字典而不是拋出異常?
  3. 如何將包含 datetime 物件的字典序列化為 JSON?

實作練習

  1. 寫一個函式,合併多個 JSON 檔案
  2. 實作一個支援註解的 JSON 讀取器(移除 // 開頭的行)
  3. 寫一個函式,比較兩個 JSON 檔案的差異

上一章:pathlib - 路徑操作 下一章:subprocess - 執行外部命令