M7 檔案與路徑
- 分清絕對路徑和相對路徑,說出相對路徑是相對於哪裏;
- 用
pathlib組合路徑、取檔名、列出一個資料夾裏的檔案; - 讀寫文字檔時一律寫明
encoding="utf-8",看懂UnicodeDecodeError; - 分清
"w"和"a",不會意外覆寫檔案。
1程式怎樣找到檔案 How a program finds a file
M0 起,我們一直這樣讀檔:
Path("book.json").read_text(encoding="utf-8")
只寫了檔名,Python 怎知道去哪裏找?答案是:它在目前的工作目錄(working directory)裏找。檔案不在那裏,就會報:
FileNotFoundError: [Errno 2] No such file or directory: 'book.json'
這是讀檔最常見的錯誤。通常不是檔案不見了,而是程式在另一個資料夾裏找。用 Path.cwd() 可以查出目前的工作目錄。
2路徑:絕對與相對 Absolute and relative paths
路徑(path)是檔案的地址,有兩種寫法:
- 絕對路徑:由磁碟的最頂層寫起,在哪裏用都指向同一個檔案。例如
/Users/you/python-course/orders/2026-10-03 門市.yaml(Mac)或C:\Users\you\python-course\orders\2026-10-03 門市.yaml(Windows)。 - 相對路徑:由工作目錄起計,例如
orders/2026-10-03 門市.yaml。工作目錄不同,它指向的檔案就不同。
練習和課程資料一律用相對路徑:整個 python-course 資料夾搬到別的電腦,相對路徑照樣有效;絕對路徑裏有你的用戶名,換一部電腦就不對了。
3pathlib:把路徑當作物件 pathlib
標準函式庫的 pathlib 把路徑變成一個 Path 物件,用 / 把各段接起來,Mac 和 Windows 都寫法一樣:
from pathlib import Path
p = Path("data") / "orders" / "2026-10-01 門市.yaml"
p.name # '2026-10-01 門市.yaml':檔名
p.stem # '2026-10-01 門市':不含副檔名
p.suffix # '.yaml':副檔名
p.parent # Path('data/orders'):所在的資料夾
p.exists() # 檔案存在嗎?
列出一個資料夾裏的某類檔案,用 glob();它的次序不固定,所以通常再用 sorted() 排好:
for f in sorted(Path("data/orders").glob("*.yaml")):
print(f.name)
"data" + "/" + "orders" 能用(Windows 的 open() 也接受 /),但很容易漏寫或多寫一個 /:"data" + "orders" 變成 dataorders,"data/" + "/orders" 變成 data//orders;幾個人寫的路徑還可能混用 / 和 \。用 Path 和 / 接起來,分隔符交給 Python 處理;得到的又是 Path 物件,可以直接用 .name、.parent、.exists()。檔名有中文和空格也沒有問題。
4讀和寫 Reading and writing
整個檔案一次讀入或寫出,用 read_text() 和 write_text():
text = Path("book.yaml").read_text(encoding="utf-8")
Path("note.txt").write_text("今天賣出 7 本書\n", encoding="utf-8")
要一行一行處理(例如很大的記錄檔),用 with open(...)。with 保證用完之後檔案一定會關上,即使中途出錯:
with open("log.txt", encoding="utf-8") as f:
for line in f:
print(line.strip()) # strip() 去掉行尾的換行字元
open() 的第二個參數是模式:
| 模式 | 意思 | 檔案已存在時 |
|---|---|---|
"r"(預設) |
讀 | 照讀 |
"w" |
寫 | 先清空,再寫 |
"a" |
加在後面 | 保留原有內容,在尾加上 |
with open("log.txt", "w", encoding="utf-8") as f: f.write("第一行\n")
with open("log.txt", "w", encoding="utf-8") as f: f.write("第二行\n")
# log.txt 現在只有「第二行」
write_text() 也一樣會覆寫。要保留舊內容,用 "a";寫入重要檔案前,先用 exists() 檢查。
把 Python 物件寫成 JSON 檔時,記得 ensure_ascii=False,否則中文會變成 \u770b\u5f97… 這樣的轉義碼(M8 會詳細講):
Path("report.json").write_text(json.dumps(report, ensure_ascii=False, indent=2), encoding="utf-8")
5文字編碼 Text encoding
檔案裏存的其實不是文字,而是位元組(bytes)。把文字變成位元組的規則叫做編碼(encoding)。同一個「看」字,用不同的編碼存,位元組就不同:
"看".encode("utf-8") # b'\xe7\x9c\x8b':3 個位元組
"看".encode("big5") # b'\xac\xdd':2 個位元組
flowchart LR
T1["文字<br/>看得見的資料"] -->|"編碼 encode<br/>(寫檔)"| B["位元組<br/>b'\xac\xdd…'"]
B -->|"解碼 decode<br/>(讀檔)"| T2["文字"]
B -.->|"用錯編碼解碼"| E["UnicodeDecodeError<br/>或亂碼"]
classDef v fill:#ffffff,stroke:#d3d1cb,color:#37352f
classDef b fill:#f1f1ef,stroke:#d3d1cb,color:#37352f
classDef err fill:#fdebec,stroke:#c4423d,color:#5d1715
class T1,T2 v
class B b
class E err
如今幾乎所有新檔案都用 UTF-8,它能表示世界上所有文字。但舊系統(例如一些舊的收銀機、會計軟件)匯出的繁體中文檔案,常常用 Big5。用 UTF-8 去讀 Big5 檔,就會報:
UnicodeDecodeError: 'utf-8' codec can't decode byte 0xac in position 0: invalid start byte
意思是:第 0 個位元組 0xac 不是合法的 UTF-8。知道檔案是 Big5,就這樣讀:
Path("legacy.txt").read_text(encoding="big5")
沒有寫 encoding 時,Python 用的是作業系統的預設編碼。Mac 和 Linux 是 UTF-8,但繁體中文版 Windows 通常是 cp950(Big5 的一種)。同一段程式,在 Mac 上正常,在同學的 Windows 上讀中文檔案就出錯。所以本課每一次讀寫文字檔,都寫明 encoding="utf-8"。
6notebook 的工作目錄 A notebook's working directory
在 JupyterLab 和 VS Code 裏打開 notebook,工作目錄就是 notebook 所在的資料夾。所以本課一直叫你把資料檔和 notebook 放在一起。
.py 檔則不同。在終端機執行 uv run python tools/report.py,工作目錄是終端機當時所在的資料夾,不是 report.py 所在的 tools/。腳本裏寫 Path("orders.yaml"),就會在錯的地方找。要找「和腳本放在一起」的檔案,用腳本自己的位置:
HERE = Path(__file__).parent # 這個 .py 檔所在的資料夾
orders = HERE / "orders.yaml"
(__file__ 只在 .py 檔裏有;notebook 裏沒有,也不需要。)
7容易混淆 Easily confused
| 這兩樣 | 分別 |
|---|---|
| 絕對路徑和相對路徑 | 前者由磁碟頂層寫起,在哪裏用都一樣;後者由工作目錄起計,搬資料夾後仍有效。 |
| 工作目錄和檔案所在的資料夾 | notebook 的兩者通常相同;.py 腳本的工作目錄是終端機所在的地方。 |
"w" 和 "a" |
前者先清空再寫;後者在原有內容後面加。 |
read_text() 和 with open(...) |
前者一次讀入整個檔;後者可以一行一行處理大檔案。 |
| 文字和位元組 | 檔案存的是位元組;讀寫時按編碼在兩者之間轉換。 |
UnicodeDecodeError 和亂碼 |
前者是解碼時發現不合法;後者是解碼「成功」但用錯了編碼。兩者原因相同:編碼不對。 |
8動手:三間分店的訂單 Hands-on: orders from three branches
- 下載
m7-branch-orders.ipynb和m7-orders.zip,放進python-course資料夾(zip 不用自己解壓,notebook 第一格會做); - 執行
uv run jupyter lab,打開 notebook,逐格執行。
小書店多了兩間分店,每日把訂單各自存成一個 YAML 檔,檔名有日期和分店名;舊門市的收銀機還匯出了一份 Big5 編碼的銷售記錄。你要列出所有訂單檔、逐個讀入合併、處理那份 Big5 檔,最後把匯總寫成 JSON 和一份文字報告,而且不覆寫已有的報告。
m7-sensor-logs-eng.ipynb 和 m7-logs.zip:一個資料夾裏有按日期命名的感測器記錄檔(每行一筆 JSON),按日期次序逐個讀入、合併、找出缺了哪一日,再寫成一個合併檔。
M8 回到 JSON 本身:完整的語法、與 Python 型別的對應、load 和 loads 的分別,以及怎樣看懂 JSON 的錯誤訊息。