春江暮客

春江暮客的个人学习分享网站

Python 临时目录实战:自动清理中间文件,保留成功报告

2026-10-26 技术
Python 临时目录实战:自动清理中间文件,保留成功报告

批处理脚本写完中间文件后报错,临时目录却留了下来;另一个常见问题是,把最终报告也放进临时目录,清理时一起删掉。本文用 TemporaryDirectory 给每次运行分配独立工作区,在退出时清理,并把成功结果保存在工作区之外。

面向初学者到中级读者,示例使用 Linux、Python 3.10 或更新版本,无需第三方包;已在 Python 3.11.4 下测试。目标是一个可直接运行的文本报告任务:去掉空行和首尾空白,给保留的行编号,并验证失败时旧报告不变。

1. 方法:先区分临时文件和最终产物

  1. 输入文件留在原处,任务只读取它。
  2. 在输出目录内创建随机命名的 .report-* 工作区,写入候选报告。
  3. 候选报告关闭后,用 os.replace() 放到工作区外的最终路径。
  4. 离开 with,清理本次工作区。

TemporaryDirectory 官方文档说明它可作为上下文管理器,退出时删除目录及其内容。这里用 dir= 明确位置、用 prefix= 标记用途;父目录必须已存在且可写。默认临时位置会受环境影响,不一定在 /tmp。

工作区放在最终目录内,使候选文件和目标处于同一文件系统。os.replace()成功时原子地替换路径,也会覆盖已有目标文件。它不提供多写入者互斥;本文按可信本地目录、单个目标只有一个写入者设计,也没有实现断电后的持久性保证。

2. 准备环境和输入

  1. 在同一个终端运行以下命令。重复练习时换一个目录名,避免覆盖之前的结果:
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
  1. 把下一节代码保存为当前目录的 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 把中间文件限制在本次任务的独立目录里,并在作用域结束时清理。最终产物应先完成写入,再移到工作区之外;同时验证成功和失败分支,避免把残缺文件当成结果。外部命令必须先结束,强制终止后的残留则按明确归属单独处理。

标签

1024 12306 ablang adsense agents.md ai ai-agent ai-agents ai-seo algorithm amp antibodies apparmor argparse automation backup bash batch-processing binarycif bioinformatics biopython blockchain boltz bootstrapping boxes bubblewrap c-index cca cdn chatgpt checkpoint cli cloudflare codex cofoldarena concurrency configuration copy cprofile cpu监控 cron csv cuda curl data-leakage data-processing data-quality data-validation datascience datavisualization datetime debugging decimal deployment desktop-app devtools disk-space disown dns docker dovecot download electron encoding esm esm2 esm3 esmc esmfold2 faceswap fasta fastmcp faulthandler ffmpeg file-descriptors file-io flashppi flask folium frontend game generator git github-actions google google-research grep gzip harness hls html http hugo imports indexnow javascript jev json jsonl just k-means kaggle langfuse leecode linux list litellm llm llms.txt logging logs lollipop lsof m3u8 machine-learning macos manacher matplotlib mcp memory mirror mmcif model-evaluation monitoring mp3 mp4 mpnn multiomics mutation mysql nanobert nanobodies networkx nginx normalize numpy ollama omegatherm pandas password pathlib pdb pep-723 performance phaser pillow pip postfix preprocessing print protein-alignment protein-design protein-embeddings protein-interactions protein-language-models protein-stability protein-structure proxy pydantic pyecharts pyqt python python3 r raincloud reproducibility requests reservoir-sampling rfoptimization rg ripgrep rosettafold3 roundcube rrsi rsi rsync s-tui sampling scale scikit-learn scrapy screen seaborn security selenium seo sequence-identity sha256 shell signals sklearn socket solana somaticsignatures spl sqlite ssh standardize static-site subprocess symlink sysadmin system-one tarfile tcp tempfile tensorflow tkinter tls toml tracemalloc tron tronpy troubleshooting turtle typesafe-ai ubuntu unicode urllib usdt utf-8 uv vhhbert vite webp wordcloud wordpress workflow yaml zipfile zoneinfo 后台 寓言 概率 经济 贸易 迅雷解析 钱包

友情链接

其它