大量檔案(作業照片、下載資料、爬到的檔)名稱亂成一團?
今天要做的這支工具可一次整理好:前綴、後綴、大小寫、字串取代、空白轉符號、改副檔名、加連號
檔名:rename_tool.py
# rename_tool.py — Day 14:批次檔名改名器(支援 Dry-run 與 CSV 匯出)
from __future__ import annotations
import argparse, csv, re, shutil
from pathlib import Path
from typing import List, Tuple, Dict
INVALID_WIN = r'<>:"/\\|?*' # Windows 不允許的字元
def normalize_spaces(name: str, fill: str | None) -> str:
if not fill:
return name
# 所有空白(含連續空白、tab)轉成 fill,並收斂重複
s = re.sub(r"\s+", fill, name)
s = re.sub(rf"{re.escape(fill)}+", fill, s)
return s
def apply_case(stem: str, mode: str | None) -> str:
if not mode: return stem
if mode == "lower": return stem.lower()
if mode == "upper": return stem.upper()
if mode == "title": return stem.title()
return stem
def apply_replacements(stem: str, pairs: List[Tuple[str,str]]) -> str:
for a, b in pairs:
stem = stem.replace(a, b)
return stem
def sanitize(name: str) -> str:
# 移除 Windows 不合法字元,避免改名失敗
for ch in INVALID_WIN:
name = name.replace(ch, "")
# 避免結尾為空白或點
return name.strip(" .")
def plan_new_name(p: Path, args, seq: int) -> str:
stem, ext = p.stem, p.suffix
# 大小寫、取代、空白
stem = apply_case(stem, args.case)
stem = apply_replacements(stem, args.replace or [])
stem = normalize_spaces(stem, args.spaces)
# 前綴/後綴
new_stem = f"{args.prefix or ''}{stem}{args.suffix or ''}"
# 連號
if args.number:
num = f"{seq:0{args.num_digits}d}"
if args.num_pos == "front":
new_stem = f"{num}_{new_stem}"
else:
new_stem = f"{new_stem}_{num}"
# 改副檔名
out_ext = args.to_ext if args.to_ext else ext
if out_ext and not out_ext.startswith("."):
out_ext = "." + out_ext
new_name = sanitize(new_stem + out_ext)
return new_name
def collect_files(src: Path, recursive: bool, match: List[str] | None) -> List[Path]:
pats = match or ["*"]
files: List[Path] = []
for pat in pats:
if recursive:
files.extend([p for p in src.rglob(pat) if p.is_file()])
else:
files.extend([p for p in src.glob(pat) if p.is_file()])
# 去重(不同 pattern 可能抓到同檔)
uniq = []
seen = set()
for p in files:
if p.resolve() not in seen:
uniq.append(p)
seen.add(p.resolve())
return uniq
def write_csv(plan: List[Dict], out: Path):
out.parent.mkdir(parents=True, exist_ok=True)
cols = ["src", "dst", "status", "reason"]
with out.open("w", encoding="utf-8", newline="") as f:
w = csv.DictWriter(f, fieldnames=cols)
w.writeheader()
for row in plan:
w.writerow({k: row.get(k, "") for k in cols})
def main():
ap = argparse.ArgumentParser(
description="批次檔名改名器:前綴/後綴/大小寫/取代/空白/副檔名/連號。預設為 dry-run。")
ap.add_argument("--src", type=Path, required=True, help="來源資料夾")
ap.add_argument("--recursive", action="store_true", help="包含子資料夾")
ap.add_argument("--match", nargs="*", help="檔名過濾 pattern,例如 *.jpg *.png *.pdf(可多個)")
# 規則
ap.add_argument("--prefix", default="", help="加在檔名前面的前綴")
ap.add_argument("--suffix", default="", help="加在檔名後面的後綴")
ap.add_argument("--spaces", default=None, help="把所有空白換成此字元,例如 _ 或 -")
ap.add_argument("--case", choices=["lower","upper","title"], help="大小寫模式")
ap.add_argument("--replace", nargs="*", metavar="A=B",
help="字串取代,可多個,格式 A=B(從檔名中把 A 換成 B)")
ap.add_argument("--to-ext", help="改副檔名,例如 jpg / png / pdf(不含點),不填則保留原始")
# 連號
ap.add_argument("--number", action="store_true", help="加入連號")
ap.add_argument("--num-start", type=int, default=1, help="連號起始(預設 1)")
ap.add_argument("--num-digits", type=int, default=3, help="連號位數(預設 3)")
ap.add_argument("--num-pos", choices=["front","end"], default="front", help="連號位置(前/後)")
# 執行
ap.add_argument("--apply", action="store_true", help="真的改名(預設不改,只試跑)")
ap.add_argument("--out", type=Path, default=Path("exports/rename_preview.csv"), help="輸出 CSV")
args = ap.parse_args()
src = args.src
if not src.exists():
print("❌ 來源不存在")
return
# 解析 replace 參數
pairs: List[Tuple[str,str]] = []
if args.replace:
for token in args.replace:
if "=" not in token:
print(f"⚠️ 忽略不合法 replace:{token}(需 A=B)")
continue
a, b = token.split("=", 1)
pairs.append((a, b))
args.replace = pairs
files = collect_files(src, args.recursive, args.match)
if not files:
print("⚠️ 找不到檔案(檢查 --match pattern 或資料夾路徑)")
return
plan: List[Dict] = []
seq = args.num_start
dst_used = set()
for p in files:
new_name = plan_new_name(p, args, seq if args.number else 0)
if args.number:
seq += 1
dst = p.with_name(new_name)
status = "ok"
reason = ""
# 目標已存在或重複
key = (dst.parent.resolve(), dst.name)
if dst.exists() or key in dst_used:
status = "conflict"
reason = "目標檔名已存在或重複"
# 追加 _1, _2… 解決衝突
base = dst.stem
ext = dst.suffix
k = 1
while True:
alt = dst.with_name(f"{base}_{k}{ext}")
key2 = (alt.parent.resolve(), alt.name)
if not alt.exists() and key2 not in dst_used:
dst = alt
status = "ok"
reason = "自動加序號避免衝突"
key = key2
break
k += 1
dst_used.add(key)
plan.append({"src": str(p), "dst": str(dst), "status": status, "reason": reason})
# 輸出預覽 CSV
write_csv(plan, args.out)
print(f"📝 已輸出預覽:{args.out}")
# 列出前 10 筆預覽
print("\n預覽(前 10 筆):")
for row in plan[:10]:
print(f"- {Path(row['src']).name} -> {Path(row['dst']).name} [{row['status']}]")
if not args.apply:
print("\n(Dry-run)預設不會真的改名,加上 --apply 參數才會執行。")
return
# 真的執行改名
done_ok = done_fail = 0
for row in plan:
if row["status"] != "ok":
done_fail += 1
continue
src_p = Path(row["src"])
dst_p = Path(row["dst"])
try:
if src_p == dst_p:
continue
shutil.move(str(src_p), str(dst_p))
done_ok += 1
except Exception as e:
row["status"] = "error"
row["reason"] = str(e)
done_fail += 1
# 重新輸出執行後的結果
final_csv = args.out.with_name(args.out.stem + "_applied.csv") if args.apply else args.out
write_csv(plan, final_csv)
print(f"\n✅ 改名完成:成功 {done_ok},失敗/跳過 {done_fail}")
print(f"📄 變更明細:{final_csv}")
if __name__ == "__main__":
main()
使用方式
先在專案資料夾開終端機(VS Code 內建 Terminal),路徑要在有檔案的資料夾。
1)最基本:把所有空白改成底線,轉小寫
python rename_tool.py --src . --spaces _ --case lower
輸出 exports/rename_preview.csv,不會動到檔案。
2)加入前綴 & 後綴、限定副檔名、加連號,真的改名
python rename_tool.py --src . ^
--match *.jpg *.png ^
--prefix Trip_ --suffix _Taipei ^
--number --num-start 1 --num-digits 3 --num-pos front ^
--apply
3)字串取代(把「_副本」→「」;把「空白」→「-」)
python rename_tool.py --src D:\photos ^
--recursive --match *.jpg ^
--replace _副本= " =-" ^
--apply
4)改副檔名(把 .jpeg 全部轉成 .jpg),並輸出到自訂 CSV
python rename_tool.py --src . --match *.jpeg --to-ext jpg --out exports/rename_jpeg.csv
Windows 開啟輸出資料夾小技巧:ii .\exports
實作:
功能重點
Dry-run 預設:不會誤動檔案;先看 CSV 預覽再決定
自動避免衝突:若目標檔已存在,會加 _1、_2…
大小寫/空白/取代:常見整理規則一次到位
通配過濾:--match 接多個 *.jpg *.png *.pdf
子資料夾:--recursive 會一路處理
小提醒
預覽和實際不同?
改名前後都會各輸出一次 CSV( _applied.csv ),可比對。
權限不足 / 檔案被占用
關閉正在使用該檔案的程式,或用管理員開終端機再試。
改副檔名 ≠ 轉檔
這只是改檔名的副檔名,不會轉格式
今日小結
完成一支零相依的批次改名工具:可先試跑、輸出 CSV、再一次套用
規則彈性:前綴/後綴/大小寫/取代/空白/連號/副檔名