Python 資料全景課 / 第三部分 資料格式 · M8

M8 JSON

M0 以來我們一直在讀 JSON,這個單元把它講完整:它只有六種值,語法非常嚴格。嚴格是好事,壞了一定會報錯;這個單元教你看懂錯誤訊息,很快找到壞在哪裏。
約 45 分鐘練習 notebook 1 個先修:M7
學完本單元,你能夠:
  1. 說出 JSON 的六種值,以及它們讀入 Python 後變成甚麼型別;
  2. 認出 JSON 和 Python dict 寫法上的分別,寫出合法的 JSON;
  3. 分清 load、loads、dump、dumps;
  4. 從 JSONDecodeError 的行號和欄號,找出 JSON 壞在哪裏。

1JSON 的六種值 The six kinds of value

JSON 的全部語法,就是六種值:

圖 1 一份 JSON 裏的六種值。物件和陣列可以互相包含,形成 M4 講過的巢狀結構。
圖 1 一份 JSON 裏的六種值。物件和陣列可以互相包含,形成 M4 講過的巢狀結構。
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,但規則嚴格得多:

圖 2 同一本書,左邊是 Python 的寫法,右邊是 JSON 的寫法。紅色標出 JSON 不接受的地方。
圖 2 同一本書,左邊是 Python 的寫法,右邊是 JSON 的寫法。紅色標出 JSON 不接受的地方。
規則 正確 錯誤
字串和鍵一律用雙引號 {"title": "Python"} {'title': 'Python'}
鍵一定要有引號 {"price": 120} {price: 120}
最後一項後面不可以有逗號 [1, 2, 3] [1, 2, 3,]
true、false、null 全部小寫 true True、None
不可以寫註解 — // 書名、# 書名
Python 會寫出不合規格的 NaN

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
圖 3 四個函式的分別。有 s 的處理字串;沒有 s 的直接處理檔案。load 是讀入(文字 → 物件),dump 是寫出(物件 → 文字)。
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)

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

  1. 下載 m8-fix-orders.ipynb 和 m8-orders.zip,放進 python-course 資料夾;
  2. 執行 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 而建置失敗。