跳到正文
跟我做
自媒体自动化5 分钟Node.js

批量改文件名:加日期、加序号,先预览

把文件夹里的文件统一改成「前缀-日期-序号」,保留原扩展名,目标重名自动加 -2。默认只预览,确认后加 --write 才改名。

约 5 分钟
本页目录
  1. 这篇教程解决什么问题
  2. 先看三个样本
  3. 命名规则
  4. 写脚本
  5. 先预览
  6. 执行
  7. 改完怎么检查
  8. 什么时候不适合批量改
  9. 常见坑
  10. 1. 忘了先预览
  11. 2. 序号顺序不是想要的
  12. 3. 把扩展名改丢了
  13. 4. 子文件夹没动
  14. 下一步读什么

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;已经叫目标名字的文件会跳过。
撞名预览:IMG_001.jpg 改成 旅行-2026-10-25-01-2.jpg,已存在的 旅行-2026-10-25-01.jpg 被改成 02
① 目标名已存在 ② 加 -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 执行。
终端预览 7 个文件,IMG_001.jpg 到 IMG_007.jpg 分别改成 旅行-2026-10-25-01.jpg 到 07.jpg
① 原文件名 ② 新名字(本机真实预览,一个文件都没改)

执行

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 个文件已改名。
旅行文件夹里的 7 个文件,文件名从 旅行-2026-10-25-01.jpg 到 07.jpg,序号连续
① 日期在前 ② 序号连续到 07(真跑 --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说「退订」就删掉。详见隐私政策。

同方向继续

全部教程