Python 課程 – 檔案操作(File I/O)
主題:JSON 檔案讀寫(json 模組)
簡介
在日常的程式開發中,資料的持久化 是不可或缺的需求。無論是設定檔、使用者偏好、或是 API 回傳的結果,最常見的儲存格式之一就是 JSON(JavaScript Object Notation)。JSON 具備結構簡潔、易讀、跨語言相容的特性,使得它成為前後端溝通與資料交換的事實標準。
Python 內建的 json 模組提供了 序列化(object → JSON) 與 反序列化(JSON → object) 的完整支援,搭配檔案 I/O,就能輕鬆地把 Python 資料寫入磁碟、或從磁碟讀回原本的資料結構。掌握這項技能,對於 設定管理、快取、資料備份 等實務需求都有直接的幫助。
核心概念
1. json 模組的基本 API
| 函式 | 功能 | 常見參數 |
|---|---|---|
json.dump(obj, fp, *, ensure_ascii=True, indent=None, sort_keys=False, **kw) |
把 Python 物件 序列化 後直接寫入已開啟的檔案物件 fp。 |
ensure_ascii=False(保留中文),indent=4(產生可讀的縮排),sort_keys=True(鍵排序) |
json.dumps(obj, *, ensure_ascii=True, indent=None, sort_keys=False, **kw) |
把 Python 物件 序列化 成字串,常用於網路傳輸或日誌。 | 同上 |
json.load(fp, *, object_hook=None, parse_float=None, parse_int=None, **kw) |
從已開啟的檔案物件 fp 讀取 JSON 並 反序列化 成 Python 物件。 |
object_hook(自訂物件轉換) |
json.loads(s, *, object_hook=None, parse_float=None, parse_int=None, **kw) |
把 JSON 字串 反序列化成 Python 物件。 | 同上 |
Tip:
json只能處理 JSON 可表示的資料類型(dict、list、str、int、float、bool、None),若要序列化自訂類別,需要提供 自訂編碼器(default參數)或在讀回時使用object_hook。
2. 基本讀寫範例
範例 1:將 Python 字典寫入 data.json(保持中文)
import json
# 需要寫入的資料
config = {
"app_name": "MyApp",
"version": "1.2.3",
"features": ["login", "export", "report"],
"max_users": 1000,
"debug": True,
"說明": "這是一段中文說明"
}
# 使用 with 開啟檔案,確保自動關閉
with open("data.json", "w", encoding="utf-8") as f:
# ensure_ascii=False 讓中文直接顯示,而不是 \uXXXX
json.dump(config, f, ensure_ascii=False, indent=4, sort_keys=True)
print("JSON 已寫入 data.json")
說明:
with open(..., encoding="utf-8")讓檔案以 UTF‑8 編碼寫入,避免中文變成亂碼。indent=4產生 4 個空格的縮排,使檔案易於閱讀。sort_keys=True會把字典的鍵依字母順序排列,方便比對版本差異。
範例 2:從 data.json 讀回資料並存成 Python 變數
import json
with open("data.json", "r", encoding="utf-8") as f:
loaded_config = json.load(f)
print("讀取結果:")
print(loaded_config)
print("版本資訊:", loaded_config["version"])
說明:
json.load會自動把 JSON 中的true/false/null轉換成 Python 的True/False/None,而字串、數字則保持原樣。
範例 3:序列化自訂類別(使用 default 參數)
import json
from datetime import datetime
class User:
def __init__(self, name, email, joined):
self.name = name
self.email = email
self.joined = joined # datetime 物件
def user_encoder(obj):
if isinstance(obj, User):
# 手動把 User 轉成可 JSON 序列化的 dict
return {
"__type__": "User",
"name": obj.name,
"email": obj.email,
"joined": obj.joined.isoformat() # 轉成 ISO 8601 字串
}
raise TypeError(f"Object of type {type(obj)} is not JSON serializable")
u = User("Alice", "alice@example.com", datetime(2023, 5, 17, 14, 30))
with open("user.json", "w", encoding="utf-8") as f:
json.dump(u, f, default=user_encoder, ensure_ascii=False, indent=2)
print("User 物件已寫入 user.json")
說明:
default參數允許我們自訂 非內建類別 的序列化方式。- 在
user_encoder中,我們把datetime轉成 ISO 格式的字串,這是 JSON 常見的時間表示法。
範例 4:讀回自訂類別(使用 object_hook)
import json
from datetime import datetime
def user_decoder(dct):
if "__type__" in dct and dct["__type__"] == "User":
return User(
name=dct["name"],
email=dct["email"],
joined=datetime.fromisoformat(dct["joined"])
)
return dct # 其他 dict 保持原樣
with open("user.json", "r", encoding="utf-8") as f:
user_obj = json.load(f, object_hook=user_decoder)
print("讀回的 User 物件:", user_obj.__dict__)
說明:
object_hook會在每個 JSON 物件(即 dict)被解碼後呼叫,我們檢查特別的__type__標記,若符合就重建User實例。
範例 5:一次寫入大量資料(使用 jsonlines 風格)
在需要 逐筆寫入 大量紀錄時,常見做法是把每筆 JSON 放在同一檔案的獨立行上(.jsonl),方便串流讀取。
import json, random, string
def random_user():
name = ''.join(random.choices(string.ascii_letters, k=6))
return {
"id": random.randint(1000, 9999),
"name": name,
"score": round(random.uniform(0, 100), 2)
}
with open("users.jsonl", "w", encoding="utf-8") as f:
for _ in range(1000): # 假設要寫入 1000 筆
json_line = json.dumps(random_user(), ensure_ascii=False)
f.write(json_line + "\n")
print("1000 筆使用者資料已寫入 users.jsonl")
說明:
- 每筆資料都是獨立的 JSON 行,使用
readline()或for line in f:可一次讀取一筆,記憶體占用極低。- 這種格式在 日誌、資料匯入匯出、機器學習前處理等情境非常常見。
3. 常見參數說明
| 參數 | 作用 | 常見值 |
|---|---|---|
ensure_ascii |
是否把非 ASCII 字元轉成 \uXXXX |
True(預設)或 False(保留原始 Unicode) |
indent |
控制輸出縮排的空白數量或字串 | None(單行)或 4(四個空格) |
sort_keys |
是否依鍵名排序 | False(預設)或 True |
default |
自訂無法序列化物件的處理函式 | 參考範例 3 |
object_hook |
讀取時自訂 dict 轉換 | 參考範例 4 |
常見陷阱與最佳實踐
忘記指定檔案編碼
- 在 Windows 系統上,預設編碼不是 UTF‑8,若不加
encoding="utf-8",中文會變成亂碼或讀取失敗。
- 在 Windows 系統上,預設編碼不是 UTF‑8,若不加
ensure_ascii=True造成中文被轉義- 預設會把中文變成
\u4e2d\u6587,雖然合法但不易閱讀。寫檔時建議改為ensure_ascii=False。
- 預設會把中文變成
一次性寫入大量資料會佔用過多記憶體
- 使用
json.dump直接寫入檔案或 JSON Lines(.jsonl)格式,可避免一次把整個資料結構載入記憶體。
- 使用
自訂類別未提供
default會拋出TypeError- 若要序列化
datetime、Decimal、Enum等非 JSON 原生類型,務必要實作default函式或先自行轉換成可序列化的形式(字串、數字)。
- 若要序列化
讀取時未使用
object_hook會得到純 dict- 若程式需要重新產生自訂類別實例,務必在
json.load時加入object_hook,否則只能得到普通的dict,失去類別方法。
- 若程式需要重新產生自訂類別實例,務必在
同時開啟多個檔案寫入同一檔案會產生 race condition
- 建議使用 檔案鎖定(
fcntl/msvcrt)或將寫入動作集中在單一執行緒/程序中。
- 建議使用 檔案鎖定(
最佳實踐清單
- ✅ 使用
with open(..., encoding="utf-8")確保檔案正確關閉且編碼一致。 - ✅ 加上
indent與ensure_ascii=False,讓產出的 JSON 友好且可讀。 - ✅ 對於大型資料,考慮使用 JSON Lines 或 分批寫入(
json.dump+flush())。 - ✅ 加入錯誤處理(
try/except json.JSONDecodeError)避免讀取損毀檔案時程式崩潰。 - ✅ 使用
sort_keys=True在版本控制系統(Git)中比較差異時更直觀。 - ✅ 為自訂類別加入
__type__標記,配合object_hook讓序列化/反序列化雙向可逆。
實際應用場景
| 場景 | 目的 | 典型程式碼片段 |
|---|---|---|
| 設定檔 | 儲存應用程式參數、使用者偏好 | json.dump(settings, open("config.json","w",encoding="utf-8"), ensure_ascii=False, indent=2) |
| 快取 API 回傳結果 | 減少重複網路請求,提高效能 | json.dump(response_json, open(cache_path,"w",encoding="utf-8")) |
| 資料備份 | 把資料庫查詢結果匯出成可讀檔案 | json.dump(rows, open("backup_2025-11-20.json","w",encoding="utf-8"), indent=4) |
| 機器學習前處理 | 大量標註資料以 JSON Lines 方式儲存 | json.dump(record, f); f.write("\n") |
| 跨平台資料交換 | 讓不同語言(Node.js、Java、Go)共用同一檔案格式 | json.load(open("shared.json","r",encoding="utf-8")) |
| 日誌 (log) | 把每筆操作紀錄為單行 JSON,方便後續搜尋 | json.dump(log_entry, log_file); log_file.write("\n") |
案例說明:
假設你在開發一個爬蟲程式,抓取網頁後的 JSON 結構需要暫存至本機,以免重跑時被封鎖。只要把每次抓取的結果寫入results.jsonl,之後再用for line in open(...): data = json.loads(line)逐筆讀取,即可在不佔用過多記憶體的情況下完成資料彙整。
總結
- JSON 是最常見的輕量級資料交換格式,Python 的
json模組提供完整的序列化與反序列化功能。 - 讀寫檔案時,務必使用
with open(..., encoding="utf-8")、ensure_ascii=False、indent等參數,讓檔案兼容中文且易於維護。 - 自訂類別 需要透過
default(寫入)與object_hook(讀取)兩個鉤子函式才能順利轉換。 - 大型資料 建議採用 JSON Lines(
.jsonl)或分批寫入的方式,以降低記憶體佔用並提升效能。 - 常見陷阱 包括編碼問題、忘記提供自訂編碼器、以及同時寫入同一檔案的競爭條件;只要遵守最佳實踐,就能寫出安全、可維護的 JSON I/O 程式碼。
掌握了上述概念與技巧後,你就能在 設定管理、快取、資料備份、機器學習前處理 等多種實務情境中,快速且可靠地使用 JSON 進行檔案讀寫。祝你在 Python 開發之路上,玩得開心、寫得順手!