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

M9 YAML

YAML 和 JSON 表示同樣的資料,卻寬鬆得多:不用引號、可以寫註解、縮排就是結構。寬鬆是為了讓人好寫,代價是 YAML 會替你「猜」每個值的型別,而且常常猜錯。這個單元教你看穿它的猜測。
約 50 分鐘練習 notebook 1 個先修:M8
學完本單元,你能夠:
  1. 讀寫映射、序列、純量,用縮排表示結構,寫註解和多行字串;
  2. 認出 YAML 會猜錯型別的值(NO、on、010、9:30、1.10、1e-3),並用引號避開;
  3. 只用 safe_load,說出為甚麼不用 load;
  4. 看懂 YAML 的錯誤訊息,處理多文件和重複的鍵。

1三種東西 Three kinds of thing

YAML 只有三種東西,和 JSON 一一對應:

YAML 寫法 JSON 讀入 Python 後
映射 mapping title: 看得見的資料 物件 { } dict
序列 sequence - python(每項一行,以 - 開頭) 陣列 [ ] list
純量 scalar 120、看得見的資料、true 字串、數字、布林、null str、int、float、bool、None

M0 那本書就是一個映射,裏面的 tags 是一個序列,author 是另一個映射。YAML 也接受 JSON 式的寫法:tags: [python, json]、author: {name: 陳大文}。事實上,合法的 JSON 幾乎都是合法的 YAML。

2縮排就是結構 Indentation is structure

JSON 用括號表示「屬於誰」,YAML 用縮排。同樣的三行,縮排不同,意思就不同:

圖 1 同樣三行,c 的縮排不同,結構就不同:左邊 c 屬於 a,右邊 c 和 a 同級。兩份都是合法的 YAML,不會報錯。
圖 1 同樣三行,c 的縮排不同,結構就不同:左邊 c 屬於 a,右邊 c 和 a 同級。兩份都是合法的 YAML,不會報錯。

規則:

a:
  b: 1
 c: 2

讀入時報的錯是:

ParserError: while parsing a block mapping
  in "<unicode string>", line 1, column 1:
    a:
    ^
expected <block end>, but found '<block mapping start>'
  in "<unicode string>", line 3, column 2:
     c: 2
     ^

讀 YAML 的錯誤訊息,看最後的 line … column … 和它下面的 ^,它指着出事的位置(第一個 ^ 只是說「問題出在從這裏開始的那一段」):第 3 行的 c 縮排了 1 格,既不屬於 a(要 2 格),也不和 a 同級(要 0 格)。

縮排錯了不一定會報錯

圖 1 兩份 YAML 都合法,只是意思不同。這是 YAML 最危險的地方:結構錯了,程式照樣執行,只是讀到的資料不對。讀入後印出來看一眼,或者檢查一下 keys()。

3註解與多行字串 Comments and multi-line strings

# 之後到行尾是註解,讀入時會被丟掉(M0 工程線見過)。這是 YAML 適合寫設定檔的主要原因之一:可以在設定旁邊寫明為甚麼這樣設。

長的文字可以分行寫:

description: |        # | 保留換行
  第一行
  第二行
summary: >            # > 把換行變成空格
  這兩行讀入後
  會接成一行

| 讀入後是 '第一行\n第二行\n';> 讀入後是 '這兩行讀入後 會接成一行\n'。

4YAML 替你猜型別 YAML guesses the type

YAML 的值大多不用引號,那它怎知 120 是數字、看得見的資料 是字串?答案是:它按樣子猜。大多數時候猜對,但以下這些會猜錯。全部用本課的環境(Python 3.12、PyYAML 6)實測:

你寫的 你想要的 YAML 讀到的
country: NO(挪威) 'NO' False
switch: on 'on' True
zip: 010 '010' 8(當作八進制)
phone: 0123456 '0123456' 42798(八進制)
phone: 0912345678 '0912345678' '0912345678'(有 8、9,不是八進制,所以是字串)
open: 9:30 '9:30' 570(當作六十進制:9×60+30)
open: 09:30 '09:30' '09:30'(以 0 開頭,不當六十進制,所以是字串)
version: 1.10 '1.10' 1.1
lr: 1e-3 0.001 '1e-3'(字串!要寫 1.0e-3)
date: 2026-10-03 視乎用途 datetime.date(2026, 10, 3)
圖 2 同一欄看起來差不多的值,YAML 猜出的型別各不相同。藍色是猜對的,紅色是猜錯的。
圖 2 同一欄看起來差不多的值,YAML 猜出的型別各不相同。藍色是猜對的,紅色是猜錯的。

留意兩組:同樣是電話號碼,一個變成數字、一個保持字串,只因為裏面有沒有 8 和 9;同樣是開店時間,9:30 變成數字,09:30 卻是字串。這類錯誤最難發現,因為大部分資料看起來都沒事。

本課網站自己就中過招

這個課程網站的設定檔 course.yml 裏,本來有一個叫 no 的鍵(「編號」的意思)。建置時程式找 x["no"],卻報了 KeyError: 'no':因為 YAML 把鍵 no 讀成了 False,dict 裏的鍵是 False,不是 'no'。最後把鍵改名為 label 才解決。

5怎樣避開:加引號 The fix: quote it

規則很簡單:不是用來計算的值,就加引號。

flowchart TD
    Q1{"這個值要拿來<br/>計算嗎?"} -->|要| N["不加引號<br/>(數字要有小數點或整數)"]
    Q1 -->|不要| Q2{"它是編號、代碼、電話、<br/>版本、時間、國家代碼?"}
    Q2 -->|是| Y["加引號"]
    Q2 -->|不是| Q3{"它像 yes、no、on、off、<br/>true、數字或日期嗎?"}
    Q3 -->|像| Y
    Q3 -->|不像| P["可以不加"]
    classDef q fill:#ffffff,stroke:#d3d1cb,color:#37352f
    classDef a fill:#f1f1ef,stroke:#d3d1cb,color:#37352f
    class Q1,Q2,Q3 q
    class N,Y,P a
圖 3 寫 YAML 時,一個值要不要加引號。拿不準就加,加了引號一定是字串。
country: "NO"
version: "1.10"
phone: "0123456"
open_at: "9:30"
learning_rate: 1.0e-3      # 要計算的,就寫成 YAML 認得的數字

還有兩種情況一定要加引號,否則會直接報錯:值裏面有「冒號加空格」(title: Python: 入門 會報 mapping values are not allowed here),或者值以 # 開頭(會被當成註解,讀到 None)。寫這個單元時,本課的術語表就因為一句「鍵: 值」中了第一種。

反方向,用 Python 寫出 YAML 時,yaml.safe_dump 會自動替 PyYAML 自己會讀錯的字串加引號:

yaml.safe_dump({"country": "NO", "version": "1.10"})
# country: 'NO'
# version: '1.10'

但它只按 PyYAML 的規則(YAML 1.1)判斷。"1e-3"、"0912345678" 在 PyYAML 讀來本來就是字串,所以寫出時不加引號,變成 1e-3、0912345678;換了跟 YAML 1.2 的工具,就會讀成數字(見後面「YAML 1.1 與 1.2」一節)。寫出的檔案要給別的工具讀,這類值就要自己檢查、加上引號。

寫中文時加 allow_unicode=True,否則中文會變成 "\u770B" 這樣的轉義碼(和 M8 的 ensure_ascii 同一回事)。

6safe_load,不要 load Always safe_load

本課一直用 yaml.safe_load()。PyYAML 也有 yaml.load(),配合某些設定時,它能按 YAML 檔裏的指示建立任何 Python 物件,甚至執行命令。打開一份從網上下載的 YAML,就可能被它執行了程式。

safe_load 只會建立 dict、list、字串、數字這些普通的值;遇到這類指示就報錯,不執行:

ConstructorError: could not determine a constructor for the tag 'tag:yaml.org,2002:python/object/apply:os.system'
只用 safe_load

讀設定檔、讀資料,一律用 yaml.safe_load;寫出用 yaml.safe_dump。看到網上的例子寫 yaml.load(f),自己用時改成 safe_load。

7多文件與重複的鍵 Multiple documents and duplicate keys

一個 YAML 檔可以用 --- 分成幾份文件。safe_load 只讀一份,遇到第二份會報 expected a single document in the stream;要全部讀,用 safe_load_all:

list(yaml.safe_load_all("a: 1\n---\na: 2\n"))     # [{'a': 1}, {'a': 2}]

另一個陷阱:同一個映射裏寫了兩次同一個鍵,PyYAML 不會報錯,只保留最後那個:

yaml.safe_load("price: 120\nprice: 150")        # {'price': 150}

設定檔很長時,很容易在下面重複寫了一個上面已有的設定,然後奇怪為甚麼改上面那個沒有作用。

8YAML 1.1 與 1.2 YAML 1.1 and 1.2

上面 NO、on、010、9:30 這些猜測,大多來自舊版規格 YAML 1.1。2009 年的 YAML 1.2 取消了它們:布林只剩 true/false,八進制要寫 0o10,也沒有六十進制。

問題是 PyYAML 至今仍然跟 1.1,而其他工具各有各的版本。同一份 YAML,交給不同的程式,可能讀出不同的結果。所以最可靠的做法不是記住哪個工具跟哪個版本,而是上一節的規則:不是用來計算的值,就加引號。

9容易混淆 Easily confused

這兩樣 分別
YAML 和 JSON 表示同樣的資料;YAML 用縮排、可寫註解、值多數不用引號,但會猜型別;JSON 嚴格,不猜。
safe_load 和 load 前者只建立普通的值;後者可能按檔案的指示建立任何物件。一律用前者。
NO 和 "NO" 前者在 PyYAML 讀到 False;後者是字串。
1e-3 和 1.0e-3 前者在 PyYAML 是字串;後者是 0.001。
` 和>`
縮排錯誤和結構錯誤 前者報錯;後者合法,只是讀到的結構和你想的不同。

10動手:App 的設定檔 Hands-on: an app's settings file

  1. 下載 m9-app-config.ipynb 和 shop-config.yaml,放進 python-course 資料夾;
  2. 執行 uv run jupyter lab,打開 notebook,逐格執行。

小書店的網上商店有一份設定檔,列出送貨的國家、營業時間、聯絡電話、版本號等。同事說「挪威的客人一直下不到單」。你要讀入設定檔,逐個檢查每個值的型別,找出所有被 YAML 猜錯的值,加上引號修好,再用一個函式自動檢查型別,以後改設定檔時就不會再犯。

工程線(選做)

m9-experiment-config-eng.ipynb 和 experiment.yaml:一份實驗參數設定檔。學習率寫成 1e-3 讀到的是字串、韌體版本 1.10 變成 1.1、取樣開始時間 14:00 變成 840、感測器編號 007 變成 7。找出它們,修好,再寫一個用 assert 驗證設定檔的函式。

下一步

M10 比較 JSON、YAML、CSV、TOML 各自適合甚麼,並用 JSON Schema 把「這份資料應該長甚麼樣」寫下來,讓程式替你檢查。