浏览 AI使用教程

批量重命名文件脚本:先预览映射、试跑 10 个再全量执行

批量重命名前先生成新旧映射,检查重名,再用少量文件试跑。真正可靠的脚本不仅能改名,还能说明改了什么以及怎样恢复。

按实际操作顺序推进

每完成一段就检查当前输出,确认结果符合预期后再继续。

  1. 01准备动手前,把材料和不能改的部分定下来
  2. 02执行有意限制范围的完整改名脚本
  3. 03检查为什么“多人同时修改目录造成中途失败”还不能交付
  4. 04完成新旧名称映射、试跑和回滚结果通过哪些检查才算完成
从手头这一份材料开始

600 个文件一次改名,最怕的不是失败,而是静默覆盖

两个文件可能被规则映射成同一个名称。临时文件名只能缓解先后占位,不能让同一个最终文件名容纳两份内容。

本例不处理 EXIF 日期或 600 张真实图片。目标是演示冻结映射、拒绝碰撞和反向恢复;只在副本运行。

  • 多对一直接拒绝,不用临时名掩盖
  • 执行前逐项核对原始哈希
  • 成功日志与源副本保留
本例材料

动手前,把材料和不能改的部分定下来

需要确认本例内容
现有材料Node.js 20+;练习副本 photos-copy 中只有 img_A.jpg、img_B.jpg,可用任意公开图片副本。脚本与图片目录同级。
不能越过的边界先只生成映射,不改文件;检查冲突、非法字符和缺失;复制小样本试跑;保留可逆映射。
要交付的结果新旧名称映射、试跑和回滚结果
实际操作

批量重命名文件脚本,从“建立副本”开始做

建立副本

把两张公开图片复制为 img_A.jpg、img_B.jpg,原目录不参与脚本。

只生成计划

运行 plan,逐行检查旧名、新名和哈希;已有计划不会覆盖。

识别不同碰撞

多对一必须改变最终命名规则;A→B、B→A 属占位循环,可另用临时唯一名两阶段处理,本脚本选择拒绝。

执行并核对

apply 预检所有行后才开始逐项改名;执行期间不要让其它程序修改该目录。

反向恢复

全部成功后运行 rollback;若中断则先按日志判断当前状态,不能盲目执行完整回滚。

Node.js / 只操作副本

有意限制范围的完整改名脚本

rename.mjs

冻结计划、验证、执行和恢复

import fs from 'node:fs';
import path from 'node:path';
import crypto from 'node:crypto';
const [mode, dirArg = './photos-copy'] = process.argv.slice(2);
const root = fs.realpathSync(dirArg);
const manifest = path.join(root, 'rename-plan.json');
const hash = file => crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex');
const safe = name => typeof name === 'string' && /^[a-zA-Z0-9_.-]+$/.test(name) && !['.', '..'].includes(name);
if (mode === 'plan') {
  if (fs.existsSync(manifest)) throw new Error('已有计划,请先检查,不覆盖');
  const rows = fs.readdirSync(root, { withFileTypes: true })
    .filter(e => e.isFile() && /^img_[a-z0-9_-]+\.jpg$/i.test(e.name))
    .map(e => ({ old: e.name, new: e.name.slice(4).toLowerCase(), sha256: hash(path.join(root, e.name)) }));
  if (!rows.length) throw new Error('没有 img_*.jpg');
  fs.writeFileSync(manifest, JSON.stringify({ root, rows }, null, 2), { flag: 'wx' });
  console.log('仅生成计划,未改名', rows);
} else {
  if (!['apply', 'rollback'].includes(mode)) throw new Error('用法: node rename.mjs plan|apply|rollback 目录');
  const plan = JSON.parse(fs.readFileSync(manifest));
  if (plan.root !== root) throw new Error('计划属于另一个目录');
  const rows = plan.rows.map(r => ({ ...r, from: mode === 'apply' ? r.old : r.new, to: mode === 'apply' ? r.new : r.old }));
  const targets = new Set();
  for (const r of rows) {
    if (!safe(r.from) || !safe(r.to)) throw new Error('名称越界');
    const key = r.to.toLowerCase();
    if (targets.has(key)) throw new Error('多个源指向同一目标,重新设计命名规则');
    targets.add(key);
    const from = path.join(root, r.from), to = path.join(root, r.to);
    if (!fs.lstatSync(from).isFile() || hash(from) !== r.sha256) throw new Error('源文件变更或不是普通文件');
    if (fs.existsSync(to)) throw new Error('目标已存在;本脚本不处理链式/循环改名');
  }
  const journal = path.join(root, 'rename-' + mode + '-' + Date.now() + '.jsonl');
  for (const r of rows) {
    fs.renameSync(path.join(root, r.from), path.join(root, r.to));
    fs.appendFileSync(journal, JSON.stringify(r) + '\n');
  }
  console.log(mode + ' 完成 ' + rows.length + ' 项;核对日志 ' + journal);
}
终端

按顺序执行并逐次核对

node rename.mjs plan ./photos-copy
node rename.mjs apply ./photos-copy
node rename.mjs rollback ./photos-copy

不要把第三条当作自动补救:仅在前一次全部成功、文件未被后续修改时运行。脚本检测内容变化、符号链接和目标占位;并发修改、磁盘故障仍需人工按日志恢复。

三种冲突不能用同一个答案

映射问题处理
img_A.jpg→a.jpg;img_a.jpg→a.jpg多对一改变最终命名,例如加入稳定序号;两份内容不能覆盖
A→B;B→A循环占位另写两阶段程序和完整日志;本例拒绝
A→B;B 已存在且不在映射外部目标占位停止,确认归属,不覆盖
最终输出

完成后的新旧名称映射、试跑和回滚结果

预期计划为 img_A.jpg→a.jpg、img_B.jpg→b.jpg。apply 后核对哈希,rollback 后恢复原名。多个源到同一目标、目标占位或循环改名均在任何写入前拒绝。

常见失败

为什么“多人同时修改目录造成中途失败”还不能交付

多人同时修改目录造成中途失败

原因
预检与逐项执行之间并非文件系统事务
怎么改
停止并按已有日志逐项核对,不能直接重跑完整反向计划
验收方式

新旧名称映射、试跑和回滚结果通过哪些检查才算完成

依据与范围

本页实际使用的资料