春江暮客

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

用 Python 批量重命名文件:默认预览、冲突检查与显式执行

2026-10-18 技术
用 Python 批量重命名文件:默认预览、冲突检查与显式执行

下载了一批文件,文件名里全是空格,想统一改成下划线,又担心覆盖已有文件。把 rename() 直接放进循环,可能改到一半才发现重名。

本文写一个只用 Python 标准库的命令行工具:默认只打印计划,先检查整批冲突,加上 --apply 才执行。规则固定为把文件名中的普通空格替换成 _,方便先学会预览、校验、执行的完整流程。

1. 确定处理范围

只处理指定目录第一层的普通文件,包括隐藏文件;跳过子目录、符号链接和其他特殊文件。文件内容不变,扩展名也不会被单独修改;连续两个空格会变成两个下划线,制表符和全角空格保持原样。

使用 Linux、Python 3.10 或更新版本,无第三方依赖。示例已在 Linux、Python 3.11.4 下验证。目录必须由你控制,运行期间先停止下载器和其他写入程序;预览与执行是两次扫描,不是冻结的快照。

重要文件先备份,并参考检查 ZIP 备份是否完整验证副本。不要对正在使用的 Hugo 内容目录直接运行;改名还可能影响页面路径和其他引用。

2. 保存完整脚本

把下面代码保存为 rename_spaces.py,放在待处理目录之外。

"""Preview or apply space-to-underscore renames. Python 3.10+, Linux."""
import argparse
import json
from pathlib import Path
import stat
import sys


def occupied(path):
    try:
        path.lstat()  # A dangling symlink also occupies a name.
    except FileNotFoundError:
        return False
    return True


def make_plan(directory):
    if not directory.is_dir():
        raise ValueError(f"not a directory: {directory}")
    plan = []
    targets = set()
    for source in sorted(directory.iterdir()):
        if not stat.S_ISREG(source.lstat().st_mode):
            continue
        name = source.name.replace(" ", "_")
        if name == source.name:
            continue
        target = source.with_name(name)
        if target in targets or occupied(target):
            raise ValueError(f"target collision: {target.name!r}")
        targets.add(target)
        plan.append((source, target))
    return plan


def main():
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("directory", type=Path)
    parser.add_argument("--apply", action="store_true", help="perform the renames")
    args = parser.parse_args()
    completed = 0
    try:
        plan = make_plan(args.directory.resolve(strict=True))
        for source, target in plan:
            print(json.dumps({"from": source.name, "to": target.name}), flush=True)
        print(f"PLAN: {len(plan)} file(s)", flush=True)
        if not args.apply:
            print("DRY RUN: no files renamed")
            return 0
        for source, target in plan:
            if not stat.S_ISREG(source.lstat().st_mode) or occupied(target):
                raise ValueError("directory changed; stop and inspect")
            source.rename(target)
            completed += 1
            print(f"RENAMED: {json.dumps(target.name)}", flush=True)
    except (OSError, ValueError) as exc:
        print(f"ERROR after {completed} rename(s): {exc}", file=sys.stderr)
        return 1
    print(f"DONE: {completed} file(s)")
    return 0


if __name__ == "__main__":
    raise SystemExit(main())

make_plan() 完成全部扫描后才返回,因此发现冲突时还没有执行任何重命名。目标名称被文件、目录或失效符号链接占用都会拒绝;多个源文件映射到同一目标也会拒绝。错误统一返回退出码 1,参数用法错误由 argparse 返回 2,成功预览或执行返回 0。

Python 官方文档说明,Path.lstat()检查链接本身,而 --apply 使用的 store_true在没有传入开关时默认为假。这样可以把“只看计划”作为默认行为。

3. 方法一:先预览

  1. 在一个新的工作目录保存脚本,再逐条执行以下命令。演示目录必须尚不存在;不要重复执行创建文件的命令,以免覆盖演示数据。
python3 --version
python3 rename_spaces.py --help
mkdir rename-demo
printf 'alpha\n' > 'rename-demo/report one.txt'
printf 'beta\n' > 'rename-demo/report two.txt'
python3 rename_spaces.py rename-demo
  1. 最后一条命令的预期输出如下,两个原始文件名都还在:
{"from": "report one.txt", "to": "report_one.txt"}
{"from": "report two.txt", "to": "report_two.txt"}
PLAN: 2 file(s)
DRY RUN: no files renamed

计划使用 JSON 行展示文件名,特殊字符会被转义,避免文件名中的换行混进输出。目录排序保证同一批输入的显示顺序稳定。

4. 方法二:确认后执行并验证

  1. 核对计划后,传入 --apply,再检查文件名和内容:
python3 rename_spaces.py rename-demo --apply
python3 - <<'PYCODE'
from pathlib import Path
root = Path("rename-demo")
assert sorted(p.name for p in root.iterdir()) == ["report_one.txt", "report_two.txt"]
assert (root / "report_one.txt").read_text() == "alpha\n"
assert (root / "report_two.txt").read_text() == "beta\n"
print("OK: names changed; contents preserved")
PYCODE
python3 rename_spaces.py rename-demo --apply
  1. 第一次执行会输出两条 RENAMED 和 DONE: 2 file(s);验证代码应输出 OK: names changed; contents preserved。最后一次执行应输出:
PLAN: 0 file(s)
DONE: 0 file(s)

这条规则再次运行不会继续改名。想核对大量文件的内容,可参考生成 SHA-256 清单;清单按路径记录,重命名后需要按已确认的名称映射对照,不能直接把路径差异当作内容损坏。

5. 验证重名时整批停止

在同一个工作目录中逐条运行。这里故意触发退出码 1,不要放进遇错立即退出的脚本。

mkdir rename-conflict
printf 'original\n' > 'rename-conflict/a b.txt'
printf 'keep\n' > 'rename-conflict/a_b.txt'
python3 rename_spaces.py rename-conflict --apply
printf 'exit=%s\n' "$?"
cat 'rename-conflict/a b.txt' 'rename-conflict/a_b.txt'

预期错误和内容如下;错误写到 stderr,终端上与 stdout 的交错顺序可能不同:

ERROR after 0 rename(s): target collision: 'a_b.txt'
exit=1
original
keep

这证明预检拒绝了目标重名,原来的两个文件仍在。若目标是一个指向不存在文件的符号链接,也应报告冲突,而不会把它当成空闲名称。

6. 排错与执行边界

现象 具体处理
target collision 检查输出的目标名;为冲突文件另选名字,重新预览。不要直接删除它。
Permission denied 用 ls -ld rename-demo 检查目录权限,确认以拥有目录写入和搜索权限的用户运行。
directory changed 停止其他写入程序,检查已输出的 RENAMED,重新预览剩余文件。
No such file or directory 核对路径;自动化中使用绝对路径。
文件没有出现在计划里 检查是否为普通文件,以及文件名是否真的包含普通空格。

Python 的 Path.rename() 文档说明,在 Unix 上目标文件已存在时可能被覆盖。脚本会在规划阶段和每次操作前检查目标,但检查与改名之间仍有时间窗口,不能保证并发写入场景下绝不覆盖。

整批改名也不是事务。 如果第三个文件操作失败,前两个已经改好,不会自动回滚;错误中的计数和 RENAMED 日志可用于核对。暂停写入、先备份、在小目录测试,是执行这类批处理的前提。

需要定时执行时,参考固定 Python 路径、工作目录和环境变量。先在终端验证绝对路径下的预览与执行,日志放在处理目录外,并保留非零退出状态。

7. 总结

把批量改名拆成生成计划、检查冲突和显式执行,先用演示目录验证结果。正式执行前保存副本并暂停其他写入程序;执行失败时先检查已完成的日志,再决定如何处理剩余文件。

标签

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 copy cpu监控 cron csv cuda curl data-leakage data-processing data-quality data-validation datascience datavisualization datetime debugging deployment desktop-app devtools disk-space disown 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 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 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 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 tcp tensorflow tkinter tls toml tron tronpy troubleshooting turtle typesafe-ai ubuntu usdt uv vhhbert vite webp wordcloud wordpress workflow yaml zipfile zoneinfo 后台 寓言 概率 经济 贸易 迅雷解析 钱包

友情链接

其它