春江暮客

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

Batch Rename Files with Python: Preview, Check Collisions, Then Apply

2026-10-18 Technology
Batch Rename Files with Python: Preview, Check Collisions, Then Apply

A download folder contains filenames with spaces, and you want underscores without replacing existing files. Calling rename() inside a loop can reveal a collision only after earlier files have already moved.

This tutorial builds a standard-library CLI that previews by default, checks the entire batch for collisions, and requires --apply to rename files. Its fixed rule replaces ordinary spaces in filenames with _, making the plan, validation, and execution steps easy to inspect.

1. Define the scope

Process regular files directly inside one directory, including hidden files. Skip subdirectories, symlinks, and special files. Contents stay unchanged, and extensions receive no separate treatment. Two spaces become two underscores; tabs and full-width spaces stay unchanged.

Use Linux and Python 3.10 or newer, with no third-party packages. The examples were tested on Linux with Python 3.11.4. Use a directory you control, and stop downloaders and other writers first. Preview and apply scan independently; a preview does not freeze the directory.

Back up important files and verify the ZIP backup. Avoid running directly on an active Hugo content directory: renaming can also affect page paths and references.

2. Save the complete script

Save the following as rename_spaces.py, outside the directory being processed.

"""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() finishes scanning before returning, so a planning collision stops the batch before any rename. A file, directory, or dangling symlink occupying a destination causes rejection, as do multiple sources mapping to one target. Operational errors return 1, argparse usage errors return 2, and successful preview or apply returns 0.

The official documentation explains that Path.lstat() inspects the link itself. The store_true action used for --apply defaults to false, keeping preview as the default behavior.

3. Method 1: Preview first

  1. Save the script in a fresh working directory, then run these commands individually. The demo directory must not exist yet. Do not repeat the file-creation commands over an existing demo.
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. The last command should print the following, while both original filenames remain:
{"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

The plan displays filenames as JSON lines, escaping special characters so embedded newlines cannot split a name across log entries. Sorting makes display order stable for the same inputs.

4. Method 2: Apply and verify

  1. Review the plan, pass --apply, then check filenames and contents:
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. The first run prints two RENAMED lines and DONE: 2 file(s). The check prints OK: names changed; contents preserved. The final run should print:
PLAN: 0 file(s)
DONE: 0 file(s)

Repeating this rule makes no further changes. For larger collections, generate a SHA-256 manifest to check contents. That manifest records paths, so compare using the approved name mapping after a rename; a path difference alone does not imply content damage.

5. Verify that collisions stop the batch

Run these commands individually in the same working directory. This deliberately returns 1; do not put it in a script that exits immediately on errors.

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'

Expected error and contents follow. The error goes to stderr, so its position relative to stdout can vary in a terminal:

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

The preflight rejects the occupied target and preserves both original files. A destination symlink pointing to a missing file must also cause a collision rather than count as an available name.

6. Troubleshooting and execution limits

Symptom Action
target collision Inspect the destination name, choose another name for the conflict, and preview again. Do not simply delete it.
Permission denied Inspect directory permissions with ls -ld rename-demo; run as a user with write and search permission on that directory.
directory changed Stop competing writers, inspect the completed RENAMED entries, and preview the remaining files again.
No such file or directory Check the path; use absolute paths in automation.
A file is missing from the plan Check that it is a regular file whose name contains an ordinary space.

The Path.rename() documentation explains that an existing destination file can be replaced on Unix. This script checks during planning and before each operation, but a time window remains between checking and renaming. It cannot guarantee protection against replacement when other processes write concurrently.

A batch is not a transaction. If the third rename fails, the first two remain renamed; there is no automatic rollback. Use the error count and RENAMED log to inspect progress. Stop writers, keep a backup, and test a small directory before applying the tool to real files.

For scheduled use, see fixing Python paths, working directories, and environment variables. Verify preview and apply with absolute paths in a terminal first, keep logs outside the processed directory, and preserve nonzero exit statuses.

7. Summary

Split batch renaming into planning, collision checks, and explicit execution, and verify the result in a demo directory first. Keep a backup and stop other writers before applying changes to real files. After a failure, inspect the completed rename log before deciding what to do with the remaining files.

标签

1024 ablang adsense agents.md ai ai-agent ai-agents ai-seo algorithm amp antibodies apparmor argparse automation background backup bash batch-processing binarycif bioinformatics biopython blockchain boltz bootstrapping boxes bubblewrap c-index cca cdn checkpoint cli cloudflare codex cofoldarena copy cpu monitoring 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 economics electron encoding esm esm2 esm3 esmc esmfold2 fable 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 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 probability 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 seo sequence-identity sha256 shell signals sklearn socket solana somaticsignatures spl sqlite ssh standardize static-site subprocess symlink sysadmin system-one tarfile tcp tensorflow tkinter tls toml trade tron tronpy troubleshooting turtle typesafe-ai ubuntu unicode usdt utf-8 uv vhhbert vite wallet webp wordcloud wordpress workflow yaml zipfile zoneinfo 迅雷解析

友情链接

其它