快轉到主要內容
  1. 教學文章/

Python tempfile 實戰:安全建立暫存檔案、目錄與測試資料

·9 分鐘· loading · loading · ·
Python Tempfile Filesystem Testing Standard-Library Developer-Tools
每日拍拍
作者
每日拍拍
科學家 X 科技宅宅
目錄
Python 學習 - 本文屬於一個選集。
§ 61: 本文

一. 前言:暫存檔不是隨便丟到 /tmp 就好
#

寫 Python 的時候,我們常常需要一個「先放一下」的地方。 例如把 API 回來的大檔案先落地。 例如把使用者上傳的 CSV 轉成乾淨版本。 例如測試時需要建立一組假的資料夾。 例如 CLI 工具產生中間報表,最後才輸出成正式檔案。 很多人第一反應是這樣:

path = "/tmp/report.csv"

拍拍君先皺眉一下。 這段看起來很直覺,但實務上會遇到不少麻煩。

  • 檔名可能跟別的程序撞在一起

  • 權限可能不是你以為的那樣

  • 程式中途失敗後檔案可能永遠留著

  • Windows、macOS、Linux 的暫存路徑規則不完全一樣

  • 測試跑平行時可能互相污染 所以今天要介紹 Python 標準庫裡很務實的模組:tempfile。 它不是華麗框架。 它也不會讓程式看起來很潮。 但它會幫你安全地建立暫存檔案、暫存目錄,並且在適當時機清理掉。 這種工具平常不起眼,直到某天你發現伺服器的 /tmp 被幾十 GB 的殘檔塞爆。 那時候你就會開始尊敬它了。 今天這篇會聚焦幾個實戰場景:

  • TemporaryDirectory 建立會自動清理的工作目錄

  • NamedTemporaryFile 產生有檔名的暫存檔

  • mkstemp 處理需要手動控制生命週期的情境

  • SpooledTemporaryFile 處理小檔留在記憶體、大檔才落地

  • 在測試、CLI、資料處理裡安全使用暫存空間

  • 避免常見的清理、權限、跨平台陷阱 如果你之前看過 pathlib 文章,可以把今天這篇想成補上「路徑背後的暫時空間管理」。 pathlib 解決的是路徑操作。 tempfile 解決的是暫存資源的建立與清理。 兩個搭起來,檔案處理才比較不會變成一團亂。

二. 安裝:標準庫內建,直接 import
#

好消息,tempfile 是 Python 標準庫。 不需要 pip。 不需要 uv add。 不需要跟系統套件搏鬥。 直接 import:

import tempfile

通常你也會搭配 pathlib.Path

from pathlib import Path
import tempfile

確認 Python 版本:

python --version

今天的範例使用 Python 3.11 以上的寫法。 大多數概念在 Python 3.8 之後都可以用。 如果你還在維護更舊的版本,要特別確認 TemporaryDirectory 和部分參數的行為。 先看一個最短範例:

from pathlib import Path
import tempfile

with tempfile.TemporaryDirectory() as tmp:
    tmp_path = Path(tmp)
    file_path = tmp_path / "hello.txt"
    file_path.write_text("hello, 拍拍君\n", encoding="utf-8")
    print(file_path.read_text(encoding="utf-8"))

print("暫存目錄已經被清理")

這段程式做了幾件事。 第一,建立一個系統認可的暫存目錄。 第二,在目錄裡寫入檔案。 第三,離開 with 區塊時自動刪掉整個目錄。 這就是 tempfile 最重要的精神: 暫存資源應該有清楚的生命週期。 不要靠「我晚點會記得刪」這種不可靠的信仰。

三. TemporaryDirectory:最推薦的預設選擇
#

如果你需要一個暫時工作區,拍拍君通常會先選 TemporaryDirectory。 它比單一暫存檔更有彈性。 因為很多實務流程不是只產生一個檔案,而是一整組中間檔。 例如:

  • 下載一個 zip
  • 解壓縮
  • 讀取裡面的 CSV
  • 產生清理後的 JSON
  • 最後只把正式結果輸出到指定位置 中間那些東西不應該永久留在專案目錄。 範例:
from pathlib import Path
import tempfile

def build_report(rows: list[dict[str, str]]) -> str:
    with tempfile.TemporaryDirectory(prefix="pypy-report-") as tmp:
        workdir = Path(tmp)
        raw_path = workdir / "raw.txt"
        out_path = workdir / "report.txt"

        raw_path.write_text(
            "\n".join(row["name"] for row in rows),
            encoding="utf-8",
        )

        names = raw_path.read_text(encoding="utf-8").splitlines()
        out_path.write_text(
            f"count={len(names)}\nfirst={names[0]}\n",
            encoding="utf-8",
        )

        return out_path.read_text(encoding="utf-8")

print(build_report([
    {"name": "拍拍君"},
    {"name": "拍拍醬"},
]))

這裡的 prefix 很有用。 它讓你在除錯時可以看出這個暫存目錄大概屬於哪個功能。 但它仍然會加上隨機字串,避免撞名。 你可能會看到類似:

/var/folders/.../pypy-report-a8m9x2kq

不要自己拼:

# 不推薦
workdir = Path("/tmp/pypy-report")
workdir.mkdir(exist_ok=True)

這種寫法在本機小腳本可能沒事。 但在 CI、伺服器、多程序、平行測試裡,撞名只是早晚問題。 TemporaryDirectory 的另一個好處是清理範圍很明確。 只要把中間檔都放在 workdir 裡,離開 with 時整包帶走。 這比到處記得呼叫 unlink() 穩很多。

四. NamedTemporaryFile:需要「一個檔名」時使用
#

有些 API 不是吃 file object,而是吃檔案路徑。 這時候你需要一個真的存在於檔案系統上的暫存檔。 NamedTemporaryFile 就是這種情境的工具。 範例:

import tempfile

with tempfile.NamedTemporaryFile(
    mode="w+",
    encoding="utf-8",
    prefix="pypy-",
    suffix=".txt",
) as f:
    f.write("hello\n")
    f.write("temporary file\n")
    f.seek(0)

    print("path:", f.name)
    print(f.read())

print("離開 with 後,檔案通常已被刪除")

幾個參數要看懂。 mode=“w+" 代表可寫也可讀。 encoding=“utf-8” 是文字檔應該養成的習慣。 prefixsuffix 方便辨識,也能讓外部工具看到副檔名。 例如有些程式會根據 .csv.json 判斷格式。 你可以這樣做:

import csv
import tempfile

rows = [
    {"name": "拍拍君", "score": 95},
    {"name": "chatPTT", "score": 88},
]

with tempfile.NamedTemporaryFile(
    mode="w+",
    encoding="utf-8",
    newline="",
    suffix=".csv",
) as f:
    writer = csv.DictWriter(f, fieldnames=["name", "score"])
    writer.writeheader()
    writer.writerows(rows)
    f.seek(0)
    print(f.read())

這裡的 newline=”"csv 模組常見建議。 不加也不一定立刻爆炸。 但跨平台時換行可能變得奇怪。

Windows 上的小提醒
#

NamedTemporaryFile 在 Windows 上有一個常見坑。 當檔案還開著時,另一個程序可能不能重新打開同一路徑。 也就是說,這種寫法在某些外部工具情境會失敗:

with tempfile.NamedTemporaryFile(suffix=".txt") as f:
    call_external_tool(f.name)

如果外部工具需要自己打開檔案,你可以改用 TemporaryDirectory,在裡面建立一般檔案。 這通常比較直覺,也比較跨平台。

from pathlib import Path
import tempfile

with tempfile.TemporaryDirectory() as tmp:
    path = Path(tmp) / "input.txt"
    path.write_text("hello\n", encoding="utf-8")

    # 外部工具可以用 path 重新開啟檔案
    print(path)

拍拍君自己的偏好是: 需要一整段流程,就用 TemporaryDirectory。 真的只需要單一暫存檔,才用 NamedTemporaryFile

五. mkstemp:需要手動控制時才拿出來
#

mkstemp 是比較底層的工具。 它會安全建立一個暫存檔,並回傳兩個東西:

  • 檔案描述符 file descriptor
  • 檔案路徑 範例:
import os
import tempfile

fd, path = tempfile.mkstemp(prefix="pypy-", suffix=".txt")

try:
    with os.fdopen(fd, "w", encoding="utf-8") as f:
        f.write("manual cleanup\n")

    with open(path, "r", encoding="utf-8") as f:
        print(f.read())
finally:
    os.remove(path)

這段比較囉嗦。 但它給你更完整的控制權。 例如你需要把檔案關掉後,再交給另一個函式使用。 或者你需要在比較底層的系統整合裡處理 file descriptor。 不過請注意: 使用 mkstemp 時,清理責任就回到你身上。 你一定要搭配 try/finally。 不然例外一發生,暫存檔就可能留在磁碟上。 拍拍君通常不會把 mkstemp 當預設選擇。 它適合少數需要手動生命週期的場景。 大多數應用程式其實用 TemporaryDirectory 就夠了。

六. SpooledTemporaryFile:小檔留記憶體,大檔才落地
#

有些資料可能很小,也可能突然變大。 例如使用者上傳檔案。 例如把 API 回應轉成中間格式。 例如產生報表前先累積內容。 如果永遠用記憶體,遇到大檔會危險。 如果永遠寫磁碟,小檔又有點浪費。 SpooledTemporaryFile 的想法是: 先放在記憶體。 超過指定大小後,再自動轉成真正的暫存檔。 範例:

import tempfile

with tempfile.SpooledTemporaryFile(
    max_size=1024,
    mode="w+t",
    encoding="utf-8",
) as f:
    for i in range(100):
        f.write(f"line {i}\n")

    f.seek(0)
    print(f.readline().strip())

max_size=1024 代表超過約 1 KB 後會 rollover 到磁碟。 如果你想手動強制落地,可以呼叫:

f.rollover()

這個工具在 web app 裡很常見。 小型上傳檔不需要馬上寫磁碟。 大型上傳檔又不能全塞記憶體。 不過要記得,這不是資料庫,也不是長期儲存。 它仍然是暫存資源。 處理完就該結束生命週期。

七. 實戰一:轉檔流程的安全工作區
#

假設我們有一個簡單需求: 讀取一批文字資料,整理成 JSONL,最後把結果寫到正式輸出路徑。 中間檔只用來檢查和轉換,不需要永久保存。 可以這樣寫:

from pathlib import Path
import json
import tempfile

def export_jsonl(names: list[str], output_path: Path) -> None:
    with tempfile.TemporaryDirectory(prefix="pypy-export-") as tmp:
        workdir = Path(tmp)
        staging_path = workdir / "staging.jsonl"

        with staging_path.open("w", encoding="utf-8") as f:
            for index, name in enumerate(names, start=1):
                record = {"id": index, "name": name}
                f.write(json.dumps(record, ensure_ascii=False) + "\n")

        content = staging_path.read_text(encoding="utf-8")

        if not content.strip():
            raise ValueError("empty export")

        output_path.write_text(content, encoding="utf-8")

export_jsonl(
    ["拍拍君", "拍拍醬", "chatPTT"],
    Path("names.jsonl"),
)

這裡有個重要設計。 正式輸出檔 names.jsonl 不放在暫存目錄裡。 因為 TemporaryDirectory 會在離開區塊後刪掉。 暫存目錄只負責中間工作。 正式成果要明確寫到外面。 這種分界很重要。 它讓你一眼看出哪些檔案是可丟棄的,哪些檔案是產物。

八. 實戰二:測試需要檔案時,不要污染專案目錄
#

測試常常需要建立檔案。 例如測試設定載入。 例如測試匯入資料。 例如測試 CLI 會不會產生輸出。 如果你直接寫到專案目錄,測試跑完就容易留下殘檔。 用 tempfile 可以把測試資料包起來:

from pathlib import Path
import tempfile

def load_names(path: Path) -> list[str]:
    return [
        line.strip()
        for line in path.read_text(encoding="utf-8").splitlines()
        if line.strip()
    ]

def test_load_names() -> None:
    with tempfile.TemporaryDirectory() as tmp:
        path = Path(tmp) / "names.txt"
        path.write_text("拍拍君\n拍拍醬\n", encoding="utf-8")

        assert load_names(path) == ["拍拍君", "拍拍醬"]

如果你用 pytest,它其實有內建的 tmp_path fixture。 那會更方便:

from pathlib import Path

def load_names(path: Path) -> list[str]:
    return path.read_text(encoding="utf-8").splitlines()

def test_load_names(tmp_path: Path) -> None:
    path = tmp_path / "names.txt"
    path.write_text("拍拍君\n拍拍醬\n", encoding="utf-8")

    assert load_names(path) == ["拍拍君", "拍拍醬"]

那為什麼今天還要學 tempfile? 因為 pytest fixture 是測試框架提供的便利包裝。 tempfile 是標準庫能力。 你在一般程式、CLI、背景工作、web app 裡都會用到。 想更完整理解 pytest 的 fixture,可以回去看 pytest 文章。 今天這篇的主角是暫存資源本身。

九. 實戰三:CLI 工具先寫暫存檔,再原子替換
#

有些 CLI 會產生正式輸出檔。 如果你直接寫目標檔,程式跑到一半失敗時,使用者可能拿到半個檔案。 比較穩的做法是: 先寫到同一個目錄裡的暫存檔。 確認成功後,再用 replace 替換正式檔。 範例:

from pathlib import Path
import tempfile

def write_report_atomic(output_path: Path, content: str) -> None:
    output_path.parent.mkdir(parents=True, exist_ok=True)

    with tempfile.NamedTemporaryFile(
        mode="w",
        encoding="utf-8",
        dir=output_path.parent,
        prefix=f".{output_path.name}.",
        suffix=".tmp",
        delete=False,
    ) as f:
        tmp_path = Path(f.name)
        f.write(content)

    try:
        tmp_path.replace(output_path)
    except Exception:
        tmp_path.unlink(missing_ok=True)
        raise

write_report_atomic(
    Path("reports/daily.txt"),
    "Daily Pypy report\nstatus=ok\n",
)

這裡有幾個細節。 dir=output_path.parent 很重要。 暫存檔和正式檔在同一個目錄時,替換通常比較可靠。 delete=False 代表離開 with 後不要自動刪掉。 因為我們要在檔案關閉後執行 replace。 但這也代表如果替換失敗,我們要自己清理。 所以有 try/except 裡的 unlink。 這種寫法很適合報表、設定檔、cache metadata。 它不能解決所有併發問題。 但比直接覆寫正式檔好多了。

十. 常見陷阱:拍拍君幫你先踩一遍
#

1. 不要手刻可預測檔名
#

不要這樣:

path = Path("/tmp") / "upload.csv"

這會有撞名和安全問題。 用 tempfile

with tempfile.NamedTemporaryFile(suffix=".csv") as f:
    print(f.name)

2. 不要把暫存檔當永久儲存
#

暫存目錄可能被系統清理。 container 重啟後可能消失。 serverless 環境甚至不保證長期存在。 如果資料重要,處理完就移到正式儲存位置。

3. 不要忘記關閉檔案
#

with。 真的需要手動控制,也要用 try/finally。 這句話很無聊,但很值錢。

4. 不要假設暫存路徑一定是 /tmp
#

請用 tempfile.gettempdir() 查:

import tempfile

print(tempfile.gettempdir())

macOS 可能是 /var/folders/…。 Windows 可能是使用者 profile 下的 temp。 Linux 常見是 /tmp。 不要把平台細節寫死。

5. 不要把敏感資料留在暫存檔
#

暫存不代表安全。 如果你處理 token、API key、個資、上傳文件,就要更謹慎。 至少做到:

  • 用最短生命週期
  • 不把路徑印到公開 log
  • 不把暫存檔 commit 進 repo
  • 不在例外處理裡把內容 dump 出來 tempfile 幫你安全建立檔案。 但它不會替你設計資料治理。

十一. 小抄:什麼情境用哪個 API?
#

簡單整理一下。

情境 建議 API 原因
一段流程需要多個中間檔 TemporaryDirectory 清理範圍清楚,最推薦
只需要一個有名字的暫存檔 NamedTemporaryFile 可取得路徑和副檔名
外部工具需要重新開啟檔案 TemporaryDirectory + 一般檔案 跨平台比較穩
需要手動控制 file descriptor mkstemp 底層控制較完整
小檔留記憶體,大檔才落地 SpooledTemporaryFile 適合上傳或中間 buffer
pytest 測試資料 tmp_pathTemporaryDirectory 測試內用 tmp_path,程式內用 tempfile
拍拍君自己的排序通常是: 先想 TemporaryDirectory。 不夠再想 NamedTemporaryFile。 真的需要底層控制,才拿 mkstemp。 這樣寫出來的程式通常最容易維護。

十二. 結語:好的暫存檔,存在感應該很低
#

tempfile 的目標不是讓你寫出很炫的程式。 它的目標是讓暫存檔安靜地出現、完成任務、然後消失。 這聽起來很普通。 但工程裡很多穩定性,就是靠這種普通的小習慣撐起來的。 不要手刻 /tmp/something.txt。 不要相信自己晚點會記得刪。 不要讓測試把專案目錄弄髒。 不要把暫存檔誤當永久資料。 下次你需要中間工作區時,先寫:

with tempfile.TemporaryDirectory() as tmp:
    workdir = Path(tmp)

然後讓程式自己收尾。 拍拍君覺得,會自己收拾殘局的程式,通常比較值得信任。

延伸閱讀
#

Python 學習 - 本文屬於一個選集。
§ 61: 本文

相關文章

Python hypothesis 實戰:Property-Based Testing 與自動化找 bug 完全攻略
·7 分鐘· loading · loading
Python Hypothesis Testing Pytest Developer-Tools
Python watchdog 實戰:檔案變更監控與自動化完全攻略
·8 分鐘· loading · loading
Python Watchdog Automation Filesystem Developer-Tools
Python zoneinfo 實戰:時區、DST 與排程時間處理完全攻略
·8 分鐘· loading · loading
Python Zoneinfo Datetime Timezone DST Standard-Library
Python Typer 進階:巢狀 subcommands、callback 與 CLI 架構
·9 分鐘· loading · loading
Python Typer Cli Command-Line Developer-Tools Testing
Python difflib 實戰:文字差異比對、相似度比較與 patch 輸出完全攻略
·10 分鐘· loading · loading
Python Difflib Text-Processing Developer-Tools Cli
Python prompt_toolkit 實戰:打造互動式 CLI、Auto-Completion 與 REPL 完全攻略
·10 分鐘· loading · loading
Python Prompt_toolkit Cli REPL Developer-Tools