批量改文件名:加日期、加序号,先预览
把文件夹里的文件统一改成「前缀-日期-序号」,保留原扩展名,目标重名自动加 -2。默认只预览,确认后加 --write 才改名。
30 秒速查
- 先预览:
node resources/rename-batch/rename.mjs 图片/- 加前缀和日期:
--prefix 旅行 --date 2026-10-25- 确认后加
--write,只改当前这一层- 重名自动加
-2,已改好的会跳过
这篇教程解决什么问题
相机、手机和平台导出的文件名,常常是 IMG_001.jpg、微信图片_2026... 这种。发之前想统一成「旅行-2026-10-25-01」,手动按 F2 改十个就会烦。
这篇用一个脚本批量改:前缀、日期、两位序号,扩展名不动。默认只预览,确认后加 --write。
先看三个样本
新建 旅行/,放三个文件:
IMG_001.jpg
IMG_002.jpg
notes.txt
脚本按文件名排序,依次编号;隐藏文件(以 . 开头)会跳过。
命名规则
| 参数 | 作用 | 示例 |
|---|---|---|
--prefix |
文件名前缀 | 旅行 |
--date |
日期,默认今天 | 2026-10-25 |
| 序号 | 两位,从 01 开始 | 01、02 |
| 扩展名 | 保留原扩展名 | .jpg、.txt |
最终名字:旅行-2026-10-25-01.jpg。没有前缀时就是 2026-10-25-01.jpg。
写脚本
保存为 resources/rename-batch/rename.mjs:
import { access, readdir, rename } from "node:fs/promises";
import path from "node:path";
import { pathToFileURL } from "node:url";
// 批量改文件名:前缀 + 日期 + 两位序号,保留原扩展名。
// 默认只预览;加 --write 才真的改名。目标已存在时自动加 -2,不覆盖。
export function today() {
return new Date().toISOString().slice(0, 10);
}
export function formatName(index, extension, { prefix = "", date = today() } = {}) {
const number = String(index).padStart(2, "0");
return `${prefix ? prefix + "-" : ""}${date}-${number}${extension}`;
}
async function exists(target) {
try {
await access(target);
return true;
} catch {
return false;
}
}
async function availableName(dir, filename) {
const extension = path.extname(filename);
const base = filename.slice(0, -extension.length);
let target = path.join(dir, filename);
let suffix = 2;
while (await exists(target)) {
target = path.join(dir, `${base}-${suffix}${extension}`);
suffix += 1;
}
return target;
}
export async function plan(dir, options = {}) {
const entries = await readdir(dir, { withFileTypes: true });
const files = entries
.filter((entry) => entry.isFile() && !entry.name.startsWith("."))
.sort((a, b) => a.name.localeCompare(b.name));
const moves = [];
let index = 1;
for (const file of files) {
const extension = path.extname(file.name);
const name = formatName(index, extension, options);
const source = path.join(dir, file.name);
const candidate = path.join(dir, name);
const target = candidate === source ? candidate : await availableName(dir, name);
// 已经叫这个名字的文件不重复改。
if (target !== source) moves.push({ from: source, to: target });
index += 1;
}
return moves;
}
export async function apply(moves) {
for (const move of moves) {
await rename(move.from, move.to);
}
}
const isDirectRun =
process.argv[1] &&
pathToFileURL(path.resolve(process.argv[1])).href === import.meta.url;
if (isDirectRun) {
const args = process.argv.slice(2);
const write = args.includes("--write");
const dir = args.find((arg) => !arg.startsWith("--")) ?? ".";
const prefix = args.includes("--prefix") ? args[args.indexOf("--prefix") + 1] : "";
const date = args.includes("--date") ? args[args.indexOf("--date") + 1] : today();
if (!/^\d{4}-\d{2}-\d{2}$/.test(date)) {
console.error("日期格式应为 YYYY-MM-DD。");
process.exitCode = 1;
} else {
try {
const moves = await plan(dir, { prefix, date });
if (moves.length === 0) {
console.error("文件夹里没有可改名的文件。");
process.exitCode = 1;
} else {
for (const move of moves) {
const flag = write ? "已改名" : "预览";
console.log(`[${flag}] ${path.basename(move.from)} → ${path.basename(move.to)}`);
}
console.log(`\n共 ${moves.length} 个文件${write ? "已改名" : ";加 --write 执行"}。`);
if (write) await apply(moves);
}
} catch (error) {
if (error.code === "ENOENT") {
console.error(`打不开文件夹:${dir}`);
process.exitCode = 1;
} else {
throw error;
}
}
}
}
两个安全点:
plan先算出全部新名字,预览和真正执行用的是同一份计划。availableName发现目标已存在时加-2;已经叫目标名字的文件会跳过。

先预览
node resources/rename-batch/rename.mjs 旅行 --prefix 旅行 --date 2026-10-25
真实输出:
[预览] IMG_001.jpg → 旅行-2026-10-25-01.jpg
[预览] IMG_002.jpg → 旅行-2026-10-25-02.jpg
[预览] notes.txt → 旅行-2026-10-25-03.txt
共 3 个文件;加 --write 执行。

执行
node resources/rename-batch/rename.mjs 旅行 --prefix 旅行 --date 2026-10-25 --write
[已改名] IMG_001.jpg → 旅行-2026-10-25-01.jpg
[已改名] IMG_002.jpg → 旅行-2026-10-25-02.jpg
[已改名] notes.txt → 旅行-2026-10-25-03.txt
共 3 个文件已改名。

--write 之后的文件名,和预览一一对应)再跑一次同样的命令,会提示「文件夹里没有可改名的文件」,不会把名字改成 -01-2。
改完怎么检查
- 数量没变:改名前数一次文件,改完再数一次。
- 序号连续:01 到 0N,没有跳号。
- 能打开:抽一张图或一个文本文件,确认扩展名没丢。
什么时候不适合批量改
- 文件名里有客户编号或版本号,重排会丢信息:先复制一份。
- 要按拍摄时间排序:脚本现在按文件名排,改
plan里的排序规则。 - 文件夹里有程序正在写入:等写完再改。
- 中途报错先停手,记下哪些已改、哪些没改,不要直接重跑。
常见坑
1. 忘了先预览
改名是不可逆的(至少脚本不记录)。先跑一遍不加 --write,确认顺序和前缀;重要的文件夹先复制一份。
2. 序号顺序不是想要的
脚本按文件名排序,不是按拍摄时间。要按时间改,先把文件按时间重命名,或修改 plan 里的排序规则。
3. 把扩展名改丢了
formatName 从原文件名取 path.extname,无扩展名的文件序号后什么都不加。特殊后缀(.tar.gz)只会保留 .gz,需要时单独处理。
4. 子文件夹没动
只处理当前层。子目录要单独跑,或在脚本里递归调用。
下一步读什么
改完名,接着整理文件夹:装好 Node,跑通第一个自动整理文件的脚本 会按扩展名分类;两篇合起来,下载文件夹就能保持整齐。
这条路线收在从 0 开始:第一台自动化工作台。
常见问题
会覆盖同名文件吗?
不会。目标名已存在时自动加 -2、-3;已经是目标格式的文件会跳过,重复执行不会一直改名。
能只改图片吗?
脚本默认处理文件夹里所有可见文件。只想改图片,先把其他文件移到别处,或按扩展名加一层过滤。
改完还能撤回吗?
脚本不记录撤销日志。第一次先跑预览,确认映射没问题再加 --write;重要的文件夹先复制一份。
子文件夹会改吗?
不会,只处理当前这一层。需要递归时,在脚本里对子目录再调用一次 plan。
每周一张复习表
每周分享一张实用复习表,不收钱不打扰,想走随时走
邮箱存在 Cloudflare KV 里,只用来发每周更新;任何时候写信到hello@genwozuo.com说「退订」就删掉。详见隐私政策。