第 34 課 · 進階 · 19 分鐘

Python CSV 檔案

讀寫表格文字,不必自己按行和逗號拆分。

CSVDictReadercsv.writer引號

會計要一份 CSV,可「拿鐵,燕麥」這個名字會撐破所有天真的 split。csv 模組懂引號。DictReader 對第一行做了什麼假設?

CSV 看起來簡單,直到某個值裡出現逗號、引號或換行。csv 模組會處理這些細節,還能把每一行表示成以表頭為鍵的字典。小例子可以用 StringIO,避免留下臨時檔案。 你還會處理列數不齊、嚴格寫入和位元組順序標記。

把行讀成字典

預設情況下,DictReader 把第一行當作欄位名。之後每一行都會變成字典。相比記住列的位置,這種方式更易讀,列很多時尤其明顯。

import csv
from io import StringIO

raw = "name,score\nAda,92\nGrace,88\n"
rows = list(csv.DictReader(StringIO(raw)))
print(rows[0]["name"])

表頭名稱變成鍵

用 writer 寫入行

csv.writer 一次寫入一行,並自動加上行結束符。傳入欄位列表或元組即可。小型記憶體範例可以寫入 StringIO,再用 getvalue() 查看結果。

import csv
from io import StringIO

buffer = StringIO()
writer = csv.writer(buffer)
writer.writerow(["name", "score"])
writer.writerow(["Ada", 92])
print(buffer.getvalue().strip())

在記憶體中建構 CSV 文字

給需要的欄位加引號

欄位中的逗號必須用引號保護。QUOTE_ALL 會給每個欄位都加引號,適合另一個工具要求這種格式的場景。讀取時 csv 會去掉引號,還原原始值。

import csv
from io import StringIO

buffer = StringIO()
writer = csv.writer(buffer, quoting=csv.QUOTE_ALL)
writer.writerow(["a,b", "c"])
print(buffer.getvalue().strip())

欄位內部的逗號

DictReader:缺失列和多餘列

DictReader 把表頭當鍵。短行缺少的欄位會得到 None;多出來的欄位會放進 restkey 指定的鍵。邊界處要認真檢查這兩種情況,不能假設每一行都正好是預期形狀。

import csv
import io

text = "name,price\nlatte,4.5\ntea\nmocha,5.0,note\n"
reader = csv.DictReader(io.StringIO(text), restkey="extra")
for row in reader:
    print(row["name"], row.get("price"), row.get("extra"))

有表頭不代表每行都完整

DictWriter:固定列序,嚴格拒絕多餘欄位

fieldnames 固定列順序。extrasaction='raise' 會把意外欄位變成錯誤,而不是悄悄丟掉。固定的 lineterminator 讓輸出可復現,也能避免 Windows 上出現雙換行。

import csv
import io

out = io.StringIO(newline="")
fields = ["name", "price"]
writer = csv.DictWriter(
    out, fieldnames=fields, extrasaction="raise", lineterminator="\n"
)
writer.writeheader()
writer.writerow({"name": "latte", "price": 4.5})
try:
    writer.writerow({"name": "tea", "price": 3.0, "note": "hot"})
except ValueError as error:
    print(type(error).__name__, error)
print(repr(out.getvalue()))

多一列就明確報錯

在邊界處理 BOM 和換行

表格軟體導出的 CSV 經常帶 UTF-8 位元組順序標記。用 utf-8-sig 解碼,第一個表頭才不會變成 \ufeffname。文字流用 newline='' 打開,讓 csv 模組而不是作業系統決定行尾。

import csv
import io

raw = b"\xef\xbb\xbfname,price\r\nlatte,4.5\r\n"
text = raw.decode("utf-8-sig")
reader = csv.DictReader(io.StringIO(text, newline=""))
row = next(reader)
print(row["name"], row["price"])

out = io.StringIO(newline="")
writer = csv.writer(out, lineterminator="\n")
writer.writerow(["tea", 3.0])
print(repr(out.getvalue()))

先解碼 BOM,再解析

本課要點

  • 使用 csv.reader 或 csv.DictReader,不要自己按逗號拆分。
  • DictReader 預設把第一行當作表頭。
  • writerow 寫入一行並添加行結束符。
  • 引號可以保護欄位內的逗號、引號和換行。
  • 小型記憶體範例可以使用 StringIO。
  • 檢查行列形狀;寫入時拒絕多餘欄位;用 utf-8-sig 和 newline='' 控制邊界。

常見錯誤

  • 欄位包含逗號時仍然使用 line.split(",")。
  • 忘記 DictReader 讀出的值預設是字串,需要時得轉換。
  • 在會轉換行結束符的平臺上寫 CSV 檔案時,忘記 newline=""。
  • 打開 CSV 文字時不用 newline='',然後把空行怪到 csv 頭上。
輪到你

用 DictReader 解析 'name,score\nAda,92\n',再列印 Ada 的分數。

動手試試

改改變數、先預測輸出,再執行程式碼。第一次點擊「執行程式碼」時會下載 Python 執行時,可能需要幾秒鐘。

瀏覽器內 Python本機執行 · 無需帳號
首次執行會從 CDN 下載 Pyodide。修改後點擊「執行程式碼」。

練習本課

12 道題,作答後立即看到解釋。

查看 12 道題答案開始練習
← 列舉日誌 →