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

M10 選擇格式

同一份書單,可以存成 JSON、YAML、CSV 或 TOML。四種都對,但各有擅長:給程式交換、給人寫設定、給 Excel 開、給工具讀。這個單元比較它們,再用 JSON Schema 把「資料應該長甚麼樣」寫下來,讓程式替你檢查。
約 50 分鐘練習 notebook 1 個先修:M9
學完本單元,你能夠:
  1. 說出 JSON、YAML、CSV、TOML 各自適合甚麼,按用途選擇;
  2. 用 csv 模組讀寫 CSV,知道讀入的值一律是字串,處理 Excel 的 BOM;
  3. 用 tomllib 讀 TOML,認出 pyproject.toml 就是 TOML;
  4. 寫一份 JSON Schema,用 jsonschema 驗證資料,看懂它的錯誤訊息。

1同一份資料,四種寫法 Four formats, one dataset

小書店的兩本書,分別用四種格式寫:

圖 1 同一份書單的四種寫法。JSON 和 YAML 能表示巢狀的標籤;CSV 只有行和列,標籤要擠進一格;TOML 用 [[book]] 表示一串書。
圖 1 同一份書單的四種寫法。JSON 和 YAML 能表示巢狀的標籤;CSV 只有行和列,標籤要擠進一格;TOML 用 [[book]] 表示一串書。

四種格式最大的分別,在於它們為誰而設計:

格式 為誰而設計 擅長 不擅長
JSON 程式之間交換資料 嚴格、每種語言都讀得、網頁 API 的標準 不能寫註解;人手寫容易漏逗號(M8)
YAML 人手寫設定 簡潔、可寫註解、可巢狀 會猜型別(M9);縮排錯了不一定報錯
CSV 表格、試算表 Excel 直接開;一行一筆,簡單 只有字串、不能巢狀、沒有標準的型別
TOML 工具的設定檔 型別明確、可寫註解、不猜型別 深層巢狀寫起來囉嗦;Python 內建只能讀

2CSV:表格 CSV for tables

CSV(comma-separated values)每一行是一筆記錄,用逗號分隔各欄,第一行通常是欄名。它沒有「巢狀」:書的標籤是一個 list,只能自己約定一種寫法擠進一格,例如 python;json。

標準函式庫的 csv 模組負責處理引號、逗號這些細節。DictReader 把每一行變成一個 dict:

import csv

with open("books.csv", encoding="utf-8", newline="") as f:
    rows = list(csv.DictReader(f))

rows[0]   # {'書名': '看得見的資料', '價錢': '120', '庫存': '3', '標籤': 'python;json'}

留意價錢是 '120':CSV 讀入的值一律是字串,要計算就要自己轉換(M3)。寫出時用 DictWriter;值裏有逗號,它會自動加上引號:"Python, 入門",120。

開 CSV 時寫明 newline=""

csv 模組自己處理換行,open() 要加 newline=""。不加的話,在 Windows 上寫出的 CSV 每行之間會多一行空行。

Excel 的 BOM

Excel 存成「CSV UTF-8」的檔案,開頭有三個看不見的位元組,叫 BOM。用 encoding="utf-8" 讀,第一個欄名會變成 '\ufeff書名',用 row["書名"] 就找不到。讀 Excel 存的 CSV,用 encoding="utf-8-sig",它會把 BOM 拿走。反過來,要讓 Excel 正確顯示中文,寫出時也用 utf-8-sig。

M11 會用 pandas 處理 CSV,屆時型別轉換會方便得多;但理解「CSV 只有字串」仍然重要。

3TOML:設定檔 TOML for configuration

TOML 是另一種給人寫的設定格式。你其實已經見過它:M1 的 pyproject.toml 就是 TOML。

title = "小書店目錄"
updated = 2026-10-04          # 日期是 TOML 本身的型別

[[book]]                      # [[ ]]:一串同類的表,讀入是 list of dict
title = "看得見的資料"
price = 120
tags = ["python", "json"]

TOML 和 YAML 最大的分別:字串一定要加引號,所以它不用猜。M9 那些陷阱,在 TOML 裏多數會直接報錯,而不是靜靜地猜錯:

寫的 YAML(PyYAML) TOML(tomllib)
country = NO False 報錯:Invalid value
phone = 0123456 42798 報錯
open = 9:30 570 報錯
version = 1.10 1.1 1.1(一樣要加引號)

報錯比猜錯好:你立即知道要改,而不是等到挪威的客人下不到單。

Python 3.11 起,標準函式庫有 tomllib,只能讀,不能寫:

import tomllib

with open("catalog.toml", "rb") as f:     # 注意:用二進制模式 "rb" 開
    data = tomllib.load(f)

要寫出 TOML,要另外安裝套件(例如 tomli-w)。這也說明了 TOML 的定位:給人寫、給工具讀的設定檔,而不是程式之間交換資料。

4怎樣選 How to choose

flowchart TD
    Q1{"主要是誰寫?"} -->|"程式寫、程式讀"| J["JSON"]
    Q1 -->|"人手寫"| Q2{"是表格嗎?<br/>(一行一筆、欄位固定)"}
    Q2 -->|"是,而且要用 Excel 開"| C["CSV"]
    Q2 -->|"不是,是設定"| Q3{"有工具指定格式嗎?"}
    Q3 -->|"有(pyproject.toml、<br/>GitHub Actions…)"| U["用它指定的"]
    Q3 -->|"沒有"| Q4{"層次深、<br/>要寫很多註解?"}
    Q4 -->|"是"| Y["YAML(記得加引號)"]
    Q4 -->|"否"| T["TOML"]
    classDef q fill:#ffffff,stroke:#d3d1cb,color:#37352f
    classDef a fill:#f1f1ef,stroke:#d3d1cb,color:#37352f
    class Q1,Q2,Q3,Q4 q
    class J,C,U,Y,T a
圖 2 按用途選擇格式。大部分情況,問兩個問題就夠了:誰寫、誰讀。

實際上,很多時候格式不是你選的:網頁 API 給你 JSON、會計部給你 Excel 匯出的 CSV、pyproject.toml 一定是 TOML、GitHub Actions 一定是 YAML。所以四種都要讀得懂,而自己能選的時候,按上圖選。

5JSON Schema:把「應該長甚麼樣」寫下來 JSON Schema

M9 寫過一個 check_types(),用一個 dict 寫下每個設定應有的型別。這個想法有一個標準的寫法,叫 JSON Schema:用 JSON 本身描述「一份資料應該長甚麼樣」。

book_schema = {
    "type": "object",
    "properties": {
        "title": {"type": "string"},
        "price": {"type": "number", "minimum": 0},
        "stock": {"type": "integer", "minimum": 0},
        "tags":  {"type": "array", "items": {"type": "string"}},
    },
    "required": ["title", "price", "stock"],
}

先在專案裏加入驗證用的套件:uv add jsonschema。然後:

import jsonschema

jsonschema.validate(book, book_schema)     # 合格:甚麼都不發生

不合格時,它報 ValidationError,訊息說明是哪一項、為甚麼:

'120' is not of type 'number'              ← price 是字串(例如從 CSV 讀入)
'stock' is a required property             ← 缺了 stock
-1 is less than the minimum of 0           ← 庫存是負數
2 is not of type 'string'                  ← tags 的第二項不是字串

錯誤物件的 absolute_path 指出出事的位置。它不是普通的 list,用 list(err.absolute_path) 轉換後再看,例如 ['tags', 1] 即「tags 的第 1 項(第二項)」,和 M4 的路徑同一個讀法。

圖 3 JSON Schema 和它檢查的資料。左邊每一條規則,對應右邊資料的一個位置;紅色是不合格的地方和報出的錯誤。
圖 3 JSON Schema 和它檢查的資料。左邊每一條規則,對應右邊資料的一個位置;紅色是不合格的地方和報出的錯誤。

JSON Schema 不只檢查 JSON:YAML、TOML、CSV 讀入 Python 之後都是 dict 和 list,同一份 schema 照樣可以檢查。

Schema 只檢查,不轉換

'120' is not of type 'number' 的意思是「這不合格」,不是「我幫你轉成數字」。從 CSV 讀入的資料,先自己轉換型別,再用 schema 檢查轉換的結果。

6Schema 常用的規則 Common schema keywords

規則 意思 例子
type 型別:object、array、string、number、integer、boolean、null {"type": "integer"}
properties 物件裏每個鍵的規則 見上
required 一定要有的鍵 ["title", "price"]
items 陣列裏每一項的規則 {"type": "string"}
minimum、maximum 數字的範圍 {"minimum": 0}
enum 只准這幾個值 {"enum": ["HK", "TW", "NO"]}
pattern 字串要符合的樣式 {"pattern": "^[A-Z]{2}$"}:兩個大寫字母
additionalProperties 是否准許沒有列出的鍵 false:打錯了鍵名也會被抓到

"additionalProperties": false 特別有用:設定檔裏把 price 打成 prcie,沒有它,schema 只會說「缺了 price」;有了它,還會說「多了一個 prcie」,一眼看出是打錯字。

7容易混淆 Easily confused

這兩樣 分別
JSON 和 JSON Schema 前者是資料;後者是描述資料應該長甚麼樣的規則,本身也用 JSON 寫。
CSV 的 "120" 和 JSON 的 120 CSV 讀入一律是字串;JSON 有數字型別。
utf-8 和 utf-8-sig 後者會處理開頭的 BOM;讀寫 Excel 的 CSV 用它。
tomllib 讀和寫 標準函式庫只能讀;寫要另外安裝套件。
number 和 integer JSON Schema 裏,integer 不接受 2.5;number 兩者都接受。
驗證和轉換 Schema 只說合不合格,不會改你的資料。

8動手:一份書單,四種格式 Hands-on: one list, four formats

  1. 下載 m10-four-formats.ipynb 和 m10-booklist.zip,放進 python-course 資料夾;
  2. 在終端機執行 uv add jsonschema;
  3. 執行 uv run jupyter lab,打開 notebook,逐格執行。

zip 裏是同一份書單的四個版本,其中 CSV 是會計部用 Excel 存的。你要把四份讀入、轉成同一個樣子、比較它們是否真的相同,再寫一份 JSON Schema 檢查全部書,找出混在裏面的壞資料。

工程線(選做)

m10-sensor-schema-eng.ipynb,需要 M4 的 sensors.json:為感測器資料寫一份 JSON Schema(量程用 enum、單位、讀數的範圍),用它檢查幾份故意弄壞的資料,並用 iter_errors 一次列出所有錯誤。

下一步

第三部分完結。M11 開始應用:用 pandas 把一層層的 JSON 攤平成表格,按月統計訂單、畫圖。