M8 JSON
- 說出 JSON 的六種值,以及它們讀入 Python 後變成甚麼型別;
- 認出 JSON 和 Python dict 寫法上的分別,寫出合法的 JSON;
- 分清
load、loads、dump、dumps; - 從
JSONDecodeError的行號和欄號,找出 JSON 壞在哪裏。
1JSON 的六種值 The six kinds of value
JSON 的全部語法,就是六種值:
| JSON | 例子 | 讀入 Python 後 |
|---|---|---|
| 物件 object | {"title": "看得見的資料"} |
dict |
| 陣列 array | ["python", "json"] |
list |
| 字串 string | "看得見的資料" |
str |
| 數字 number | 120、21.5、-0.5、1e3 |
int(沒有小數點)或 float |
| 布林 boolean | true、false |
True、False |
| 空值 null | null |
None |
就是這麼多。日期、時間、集合,JSON 都沒有:日期通常寫成字串 "2026-10-03",讀入後要自己轉換(M7 的工程線做過)。
2Python 寫出 JSON 時 From Python to JSON
反方向,json.dumps() 把 Python 物件寫成 JSON 文字。大部分型別一一對應,但有三點要留意:
json.dumps((1, 2)) # '[1, 2]':tuple 變成陣列,讀回來是 list
json.dumps({"a"}) # TypeError: Object of type set is not JSON serializable
json.dumps({1: "a"}) # '{"1": "a"}':鍵一律變成字串
最後一點最容易出事:JSON 物件的鍵只能是字串。寫出一個以數字為鍵的 dict,讀回來鍵就變成了字串,d[1] 會報 KeyError,要寫 d["1"]。
3嚴格的語法 A strict syntax
JSON 看起來很像 Python 的 dict,但規則嚴格得多:
| 規則 | 正確 | 錯誤 |
|---|---|---|
| 字串和鍵一律用雙引號 | {"title": "Python"} |
{'title': 'Python'} |
| 鍵一定要有引號 | {"price": 120} |
{price: 120} |
| 最後一項後面不可以有逗號 | [1, 2, 3] |
[1, 2, 3,] |
true、false、null 全部小寫 |
true |
True、None |
| 不可以寫註解 | — | // 書名、# 書名 |
json.dumps({"v": float("nan")}) 會寫出 {"v": NaN},Python 自己也讀得回來;但 NaN 不是合法的 JSON,交給網頁或其他語言就會出錯。寫出資料時加 allow_nan=False,遇到 NaN 就立即報錯,比交出一份別人讀不了的檔案好。工程線的練習會處理這個問題。
4load、loads、dump、dumps Four functions
json 模組最常用的四個函式,名字只差一個 s。s 代表 string:
flowchart LR
F[("JSON 檔案")] -->|"json.load(f)"| O["Python 物件<br/>dict、list…"]
O -->|"json.dump(obj, f)"| F
S["JSON 字串"] -->|"json.loads(s)"| O
O -->|"json.dumps(obj)"| S
classDef f fill:#f1f1ef,stroke:#d3d1cb,color:#37352f
classDef o fill:#ffffff,stroke:#d3d1cb,color:#37352f
class F,S f
class O o
with open("book.json", encoding="utf-8") as f:
book = json.load(f) # 檔案 → 物件
book = json.loads(Path("book.json").read_text(encoding="utf-8")) # 字串 → 物件,效果一樣
本課多數時候用第二種(read_text + loads):先拿到文字,出錯時可以把文字印出來看。
5看懂錯誤訊息 Reading the error message
JSON 壞了,json.loads 會報 JSONDecodeError,訊息最後告訴你在第幾行第幾欄發現問題:
JSONDecodeError: Expecting ',' delimiter: line 3 column 3 (char 24)
{
"title": "看得見的資料"
"price": 120
}
第 2 行尾漏了逗號,Python 卻說是第 3 行。因為它讀到第 3 行的 "price" 時,才發現「這裏應該先有一個逗號」。所以:錯誤的位置,常常在它報告的位置之前。先看那一行,再看上一行的行尾。
常見的訊息和最可能的原因:
| 訊息 | 最可能的原因 |
|---|---|
Expecting property name enclosed in double quotes |
用了單引號;鍵沒有引號;物件最後一項後面多了逗號;有註解(視乎註解位置,也可能報 Expecting ',' delimiter 或 Extra data) |
Expecting ',' delimiter |
上一項後面漏了逗號 |
Expecting value |
用了 True、None 等 Python 寫法;值漏了;陣列最後一項後面多了逗號;或者整個檔是空的(line 1 column 1) |
Extra data |
一個檔裏有兩個 JSON(例如把 JSON Lines 當成普通 JSON 讀) |
(以上是本課所用的 Python 3.12 的訊息。Python 3.13 起,多了的逗號會直接報 Illegal trailing comma before end of object 或 … of array,清楚得多。)
錯誤物件本身也帶着位置,可以用程式取出,把那一行印出來:
try:
json.loads(text)
except json.JSONDecodeError as err:
print(err.msg, "第", err.lineno, "行,第", err.colno, "欄")
print(text.splitlines()[err.lineno - 1])
try/except 的意思是「試着做;出了這種錯,就改為執行 except 那段,而不是停下來」。
6寫出好讀的 JSON Writing readable JSON
json.dumps(book, ensure_ascii=False, indent=2)
ensure_ascii=False:中文照原樣寫出。不加的話,"看"會寫成"\u770b":仍然合法,但人讀不懂;indent=2:每層縮排兩格,分行寫出。給人看的檔案就加;要盡量細小的檔案(例如網絡傳輸)就不加;sort_keys=True:按鍵排序。檔案放進 Git 時特別有用:每次寫出的次序一樣,diff 才看得出真正改了甚麼。
7JSON Lines One record per line
記錄檔、匯出檔常常用 JSON Lines(副檔名 .jsonl):每一行是一個獨立的 JSON,而不是整個檔一個大陣列。M7 的工程線讀過:
{"time": "2026-10-01T08:00", "sensor": "T-01", "value": 21.4}
{"time": "2026-10-01T12:00", "sensor": "T-01", "value": 21.6}
好處是可以一直在尾加新的一行(M7 的 "a" 模式),也可以一行一行讀,不用把整個大檔讀進記憶體。讀法是逐行 json.loads;把整個檔交給 json.loads 會報 Extra data。
8容易混淆 Easily confused
| 這兩樣 | 分別 |
|---|---|
| JSON 和 Python dict | 前者是文字,雙引號、true、null、不可有尾逗號;後者是物件,True、None,單雙引號都可以。 |
load 和 loads |
前者讀檔案物件;後者讀字串。 |
dump 和 dumps |
前者寫進檔案;後者回傳字串。 |
null 和 "null" |
前者是空值,讀入是 None;後者是四個字母的字串。 |
| 數字鍵和字串鍵 | 寫成 JSON 後,鍵一律是字串;讀回來 {1: …} 變成 {"1": …}。 |
| JSON 和 JSON Lines | 前者一個檔一個值;後者一行一個值。 |
9動手:修好壞掉的訂單 Hands-on: fixing broken orders
- 下載
m8-fix-orders.ipynb和m8-orders.zip,放進python-course資料夾; - 執行
uv run jupyter lab,打開 notebook,逐格執行。
網店的訂單系統把每張訂單存成一個 JSON 檔,但有同事用文字編輯器手動改過幾個,改壞了。你要寫一個函式,逐個讀入、把壞了的檔案和錯誤位置列出來,按錯誤訊息逐個修好,最後寫出一份中文可讀、排好序的匯總 JSON。
m8-nan-eng.ipynb:感測器有時讀不到數值,程式寫進了 NaN。看看 Python 怎樣寫出和讀回 NaN、為甚麼別的程式讀不了,再改為用 null 表示缺失,並用 allow_nan=False 防止再寫出 NaN。
M9 講 YAML:它和 JSON 表示同樣的資料,但寬鬆得多。寬鬆帶來方便,也帶來陷阱:本課網站自己的設定檔 course.yml 就曾經因為一個叫 no 的鍵被讀成 False 而建置失敗。