Python 临时目录实战:自动清理中间文件,保留成功报告
批处理脚本写完中间文件后报错,临时目录却留了下来;另一个常见问题是,把最终报告也放进临时目录,清理时一起删掉。本文用 TemporaryDirectory 给每次运行分配独立工作区,在退出时清理,并把成功结果保存在工作区之外。
面向初学者到中级读者,示例使用 Linux、Python 3.10 或更新版本,无需第三方包;已在 Python 3.11.4 下测试。目标是一个可直接运行的文本报告任务:去掉空行和首尾空白,给保留的行编号,并验证失败时旧报告不变。
1. 方法:先区分临时文件和最终产物
- 输入文件留在原处,任务只读取它。
- 在输出目录内创建随机命名的
.report-*工作区,写入候选报告。 - 候选报告关闭后,用
os.replace()放到工作区外的最终路径。 - 离开
with,清理本次工作区。
TemporaryDirectory 官方文档说明它可作为上下文管理器,退出时删除目录及其内容。这里用 dir= 明确位置、用 prefix= 标记用途;父目录必须已存在且可写。默认临时位置会受环境影响,不一定在 /tmp。
工作区放在最终目录内,使候选文件和目标处于同一文件系统。os.replace()成功时原子地替换路径,也会覆盖已有目标文件。它不提供多写入者互斥;本文按可信本地目录、单个目标只有一个写入者设计,也没有实现断电后的持久性保证。
2. 准备环境和输入
- 在同一个终端运行以下命令。重复练习时换一个目录名,避免覆盖之前的结果:
python3 --version
mkdir tempfile-report-demo
cd tempfile-report-demo
mkdir output
python3 - <<'PY_DATA'
from pathlib import Path
Path("notes.txt").write_text(" first task \n\n第二个任务\n", encoding="utf-8")
PY_DATA
- 把下一节代码保存为当前目录的
task_report.py。后续命令都在tempfile-report-demo内执行。
3. 完整实现
"""Build a numbered UTF-8 report in a private temporary workspace."""
import argparse
import os
from pathlib import Path
import sys
import tempfile
def build_report(source, output, fail=False):
source = source.resolve(strict=True)
if not source.is_file():
raise ValueError("input must be a regular file")
output = output.parent.resolve(strict=True) / output.name
if output.is_symlink():
raise ValueError("output must not be a symlink")
if output.exists() and output.samefile(source):
raise ValueError("input and output must be different files")
with tempfile.TemporaryDirectory(prefix=".report-", dir=output.parent) as name:
work = Path(name)
print(f"WORK={work}", file=sys.stderr, flush=True)
staged = work / "report.txt"
count = 0
with source.open(encoding="utf-8") as incoming:
with staged.open("x", encoding="utf-8", newline="\n") as outgoing:
for line in incoming:
text = line.strip()
if text:
count += 1
outgoing.write(f"{count}\t{text}\n")
if fail:
raise RuntimeError("simulated failure before replacement")
os.replace(staged, output)
return count
def main():
parser = argparse.ArgumentParser(description=__doc__, allow_abbrev=False)
parser.add_argument("source", type=Path, help="existing UTF-8 text file")
parser.add_argument("output", type=Path, help="report path; replaces an existing file")
parser.add_argument("--fail", action="store_true", help="fail before replacement")
args = parser.parse_args()
try:
count = build_report(args.source, args.output, args.fail)
except (OSError, UnicodeError, ValueError, RuntimeError) as exc:
print(f"ERROR: {exc}", file=sys.stderr)
return 1
print(f"OK: {count} nonempty lines")
return 0
if __name__ == "__main__":
raise SystemExit(main())
文件的 with 先结束,才替换候选报告,然后再退出临时目录的 with。--fail 在替换前主动抛出异常,用来检查失败清理;UTF-8 解码失败也走同一条异常处理路径。输入与输出指向同一文件时会拒绝执行,输出符号链接也会被拒绝。
成功运行会替换指定输出文件。 输入为空或全是空白时,成功产物就是空文件。脚本逐行读取,因此不把整个输入装入内存,但单个超长行仍会占用内存。
4. 运行并验证成功结果
python3 task_report.py --help
python3 task_report.py notes.txt output/report.txt
cat output/report.txt
标准错误会显示一条 WORK=.../output/.report-随机字符,便于定位本次工作区。标准输出中的成功信息和报告内容为:
OK: 2 nonempty lines
1 first task
2 第二个任务
结束后 output/report.txt 保留,.report-* 目录应消失。它被移出了工作区,退出清理时不会再删除它。退出码约定为:成功 0,读取、写入或模拟失败 1,命令参数错误 2。
把下面代码保存为 verify_report.py,验证正常结束、替换前失败、坏编码和并行运行使用独立目录:
"""Verify cleanup and output preservation without leaving test data."""
from concurrent.futures import ThreadPoolExecutor
from pathlib import Path
import re
import subprocess
import sys
import tempfile
script = Path("task_report.py").resolve(strict=True)
with tempfile.TemporaryDirectory(prefix="check-", dir=Path.cwd()) as name:
root = Path(name)
source = root / "notes.txt"
output = root / "report.txt"
source.write_text(" first task \n\n第二个任务\n", encoding="utf-8")
def run(target, *options):
result = subprocess.run(
[sys.executable, str(script), str(source), str(target), *options],
capture_output=True, text=True, encoding="utf-8", timeout=10,
)
match = re.search(r"^WORK=(.+)$", result.stderr, re.MULTILINE)
if match:
assert not Path(match[1]).exists(), result.stderr
return result
result = run(output)
assert result.returncode == 0, result.stderr
assert output.read_text(encoding="utf-8") == "1\tfirst task\n2\t第二个任务\n"
previous = output.read_bytes()
result = run(output, "--fail")
assert result.returncode == 1 and not result.stdout
assert output.read_bytes() == previous
result = run(root / "never-created.txt", "--fail")
assert result.returncode == 1
assert not (root / "never-created.txt").exists()
source.write_bytes(b"valid\n\xffinvalid\n")
result = run(output)
assert result.returncode == 1 and output.read_bytes() == previous
source.write_text("ready\n", encoding="utf-8")
assert run(source).returncode == 1
assert run(output, "--fai").returncode == 2
with ThreadPoolExecutor(max_workers=4) as pool:
results = list(pool.map(run, [root / f"report-{i}.txt" for i in range(4)]))
assert all(result.returncode == 0 for result in results)
paths = [re.search(r"^WORK=(.+)$", r.stderr, re.MULTILINE)[1] for r in results]
assert len(set(paths)) == 4
assert not list(root.glob(".report-*"))
print("PASS: cleanup, failure preservation, UTF-8 errors, isolated workspaces")
运行:
python3 verify_report.py
预期输出 PASS: cleanup, failure preservation, UTF-8 errors, isolated workspaces。测试会检查旧报告字节不变、新报告未创建,以及临时目录已删除;测试自己的输入和报告也会在结束后清理。并行测试使用不同输出文件,临时目录不重名不等于同一输出文件具备并发保护。
5. 外部命令要在工作区关闭前结束
需要把工作区交给外部工具时,可用同步等待的 subprocess.run()。下面的独立演示会等待子进程写完文件,再检查文件,最后清理目录:
python3 - <<'PY_CHILD'
from pathlib import Path
import subprocess
import sys
import tempfile
with tempfile.TemporaryDirectory(prefix="child-", dir=Path.cwd()) as work:
subprocess.run(
[sys.executable, "-c",
"from pathlib import Path; Path('intermediate.txt').write_text('done', encoding='utf-8')"],
cwd=work, check=True, timeout=10,
)
assert (Path(work) / "intermediate.txt").read_text(encoding="utf-8") == "done"
assert not Path(work).exists()
print("PASS: child finished before cleanup")
PY_CHILD
不要启动后台进程后立即离开 with,否则它还在使用的路径可能已经被删除。真实命令的超时和日志处理,可接着看用 Python 执行外部命令。
6. 常见问题与边界
| 现象 | 原因与处理 |
|---|---|
| 提示父目录不存在 | 先执行 mkdir -p output;dir= 不负责创建父目录。 |
| 失败后旧报告不变 | 替换前失败的预期行为;先修复输入或任务错误,再重新运行。 |
| 退出码为 1,但报告已更新 | 替换之后清理也可能失败;查看 stderr 的错误及 WORK 路径,分别核对产物与残留目录。 |
| Permission denied | 核对输出目录的写权限;关闭文件句柄,不要用 ignore_cleanup_errors=True 隐藏清理错误。 |
| No space left on device | 临时文件也占空间;运行前做磁盘空间预检,把新旧报告同时存在的峰值算进去。 |
| 杀进程后目录仍在 | 强制终止、默认 SIGTERM 或断电可能绕过 Python 清理;先确认任务和子进程已停止,只处理这次记录的目录。 |
SIGKILL 不能被捕获、阻塞或忽略,所以不能承诺所有退出方式都会触发 with 清理。不要对整个 /tmp 或所有 .report-* 批量删除;其他任务可能仍在使用自己的目录。
本例保证的是替换前失败不会改写旧报告;它没有执行 fsync(),不等于断电安全保存。需要保存重要 JSON 状态时,可以参考Python 原子写入 JSON中的刷盘顺序和适用边界。
7. 总结
用 TemporaryDirectory 把中间文件限制在本次任务的独立目录里,并在作用域结束时清理。最终产物应先完成写入,再移到工作区之外;同时验证成功和失败分支,避免把残缺文件当成结果。外部命令必须先结束,强制终止后的残留则按明确归属单独处理。
- 原文作者:春江暮客
- 原文链接:https://www.bobobk.com/python-tempfile-workspace-cleanup.html
- 版权声明:本作品采用 知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议 进行许可,非商业转载请注明出处(作者,原文链接),商业转载请联系作者获得授权。