Node.js 批量修改文件名脚本

发布于:2025-03-28 ⋅ 阅读:(25) ⋅ 点赞:(0)
#!/usr/bin/env node
const fs = require('fs');
const path = require('path');
const readline = require('readline');
const { promisify } = require('util');

// 创建 Promise 版本的 readline.question
const question = promisify(readline.createInterface({
  input: process.stdin,
  output: process.stdout
}).question).bind(readline.createInterface({
  input: process.stdin,
  output: process.stdout
}));

// 日志记录器
class Logger {
  constructor() {
    this.logs = [];
    this.operationId = Date.now();
  }

  addLog(message, type = 'info') {
    const logEntry = {
      timestamp: new Date().toISOString(),
      type,
      message
    };
    this.logs.push(logEntry);
    console[type](message);
  }

  async saveLogs() {
    const logFileName = `rename_log_${this.operationId}.json`;
    try {
      await fs.promises.writeFile(logFileName, JSON.stringify(this.logs, null, 2));
      this.addLog(`操作日志已保存到: ${logFileName}`);
    } catch (error) {
      this.addLog(`无法保存日志文件: ${error}`, 'error');
    }
  }
}

/**
 * 批量重命名文件
 * @param {string} directory - 目标目录
 * @param {Object} options - 配置选项
 * @param {Logger} logger - 日志记录器
 */
async function batchRenameFiles(directory, options, logger) {
  try {
    // 验证目录是否存在
    await fs.promises.access(directory, fs.constants.R_OK | fs.constants.W_OK);
    
    const files = await fs.promises.readdir(directory);
    let renameCount = 0;
    let skipCount = 0;
    let errorCount = 0;

    // 处理扩展名过滤
    const extensions = options.extensions 
      ? options.extensions.split(',').map(ext => ext.trim().toLowerCase())
      : null;

    for (const file of files) {
      try {
        const filePath = path.join(directory, file);
        const stats = await fs.promises.stat(filePath);

        // 跳过目录
        if (!stats.isFile()) {
          skipCount++;
          continue;
        }

        // 解析文件名
        const parsed = path.parse(file);
        
        // 检查扩展名过滤
        if (extensions && !extensions.includes(parsed.ext.toLowerCase().replace('.', ''))) {
          skipCount++;
          continue;
        }

        // 执行替换操作
        let newName = parsed.name;
        if (options.replaceFrom) {
          const regex = options.useRegex 
            ? new RegExp(options.replaceFrom, 'g') 
            : options.replaceFrom;
          newName = newName.replace(regex, options.replaceTo || '');
        }

        // 添加前缀和后缀
        newName = `${options.prefix || ''}${newName}${options.suffix || ''}${parsed.ext}`;

        // 如果名称没变则跳过
        if (file === newName) {
          skipCount++;
          continue;
        }

        // 构建新路径并检查是否已存在
        const newPath = path.join(directory, newName);
        try {
          await fs.promises.access(newPath);
          logger.addLog(`文件已存在,跳过: ${newPath}`, 'warn');
          skipCount++;
          continue;
        } catch (e) {
          // 文件不存在,可以继续
        }

        // 执行重命名
        await fs.promises.rename(filePath, newPath);
        renameCount++;
        logger.addLog(`重命名成功: ${file} → ${newName}`);
      } catch (error) {
        errorCount++;
        logger.addLog(`处理文件 ${file} 时出错: ${error}`, 'error');
      }
    }

    logger.addLog(`\n操作完成:
      - 成功重命名: ${renameCount} 个文件
      - 跳过: ${skipCount} 个文件
      - 错误: ${errorCount} 个文件`);
  } catch (error) {
    logger.addLog(`无法访问目录 ${directory}: ${error}`, 'error');
  }
}

/**
 * 交互式模式
 */
async function interactiveMode() {
  const logger = new Logger();
  logger.addLog('=== 文件批量重命名工具 ===');

  try {
    const directory = await question('请输入目录路径: ');
    const prefix = await question('要添加的前缀(直接回车跳过): ');
    const suffix = await question('要添加的后缀(直接回车跳过): ');
    const replaceFrom = await question('要替换的文本(直接回车跳过): ');
    const replaceTo = await question('替换为的文本(直接回车跳过): ');
    const useRegex = (await question('使用正则表达式? (y/n, 默认n): ')).toLowerCase() === 'y';
    const extensions = await question('要处理的文件扩展名(多个用逗号分隔,直接回车处理所有): ');

    logger.addLog('\n即将执行以下操作:');
    logger.addLog(`目录: ${directory}`);
    logger.addLog(`前缀: "${prefix}"`);
    logger.addLog(`后缀: "${suffix}"`);
    logger.addLog(`替换: "${replaceFrom}" → "${replaceTo}"`);
    logger.addLog(`使用正则: ${useRegex}`);
    logger.addLog(`文件类型: ${extensions || '所有'}`);
    
    const confirm = await question('\n确认执行? (y/n): ');
    if (confirm.toLowerCase() === 'y') {
      await batchRenameFiles(directory, {
        prefix,
        suffix,
        replaceFrom,
        replaceTo,
        useRegex,
        extensions: extensions || undefined
      }, logger);
    } else {
      logger.addLog('操作已取消');
    }
  } catch (error) {
    logger.addLog(`交互过程中出错: ${error}`, 'error');
  } finally {
    await logger.saveLogs();
    process.exit(0);
  }
}

/**
 * 命令行参数模式
 */
async function cliMode() {
  const logger = new Logger();
  const args = process.argv.slice(2);

  if (args.length === 0 || args.includes('-h') || args.includes('--help')) {
    logger.addLog(`
文件批量重命名工具
用法:
  node rename.js [目录路径] [选项]

选项:
  -p, --prefix [前缀]       添加文件名前缀
  -s, --suffix [后缀]       添加文件名后缀
  -f, --from [文本]         要替换的文本
  -t, --to [文本]           替换为的文本
  -e, --ext [扩展名]        要处理的文件扩展名(逗号分隔)
  -r, --regex               使用正则表达式替换
  -i, --interactive         进入交互模式
  -h, --help                显示帮助信息

示例:
  node rename.js ./photos -p "vacation_" -s "_2023" -f "DSC" -t "Photo" -e "jpg,png"
    `);
    process.exit(0);
  }

  if (args.includes('-i') || args.includes('--interactive')) {
    return interactiveMode();
  }

  try {
    // 解析命令行参数
    const directory = args[0];
    const options = {
      prefix: getArgValue(args, ['-p', '--prefix']),
      suffix: getArgValue(args, ['-s', '--suffix']),
      replaceFrom: getArgValue(args, ['-f', '--from']),
      replaceTo: getArgValue(args, ['-t', '--to']),
      extensions: getArgValue(args, ['-e', '--ext']),
      useRegex: args.includes('-r') || args.includes('--regex')
    };

    logger.addLog('开始批量重命名...');
    logger.addLog(`目录: ${directory}`);
    logger.addLog(`选项: ${JSON.stringify(options, null, 2)}`);

    await batchRenameFiles(directory, options, logger);
  } catch (error) {
    logger.addLog(`命令行模式出错: ${error}`, 'error');
  } finally {
    await logger.saveLogs();
    process.exit(0);
  }
}

// 辅助函数:从参数中获取值
function getArgValue(args, keys) {
  for (const key of keys) {
    const index = args.indexOf(key);
    if (index !== -1 && index + 1 < args.length) {
      return args[index + 1];
    }
  }
  return '';
}

// 启动程序
if (require.main === module) {
  cliMode();
}

// 导出函数以便作为模块使用
module.exports = {
  batchRenameFiles,
  Logger
};

1. 安装

将上述代码保存为 rename.js 文件,然后添加可执行权限:

bash

复制

chmod +x rename.js

2. 交互式模式运行

bash

复制

./rename.js -i
# 或
node rename.js --interactive

3. 命令行模式运行

bash

复制

# 基本用法
./rename.js /path/to/directory -p "prefix_" -s "_suffix" -f "old" -t "new" -e "jpg,png"

# 使用正则表达式替换
./rename.js /path/to/directory -f "\\d+" -t "NUM" -r

# 查看帮助
./rename.js -h

4. 作为模块使用

javascript

复制

const { batchRenameFiles, Logger } = require('./rename');

const logger = new Logger();
batchRenameFiles('./my-files', {
  prefix: 'archive_',
  replaceFrom: 'draft',
  replaceTo: 'final',
  extensions: 'docx,txt'
}, logger);

  1. Node.js 环境

    • 需要已安装 Node.js(建议版本 12+)

    • 检查是否已安装:

      bash

      复制

      node -v
    • 如果未安装,请从 Node.js 官网 下载安装

  2. 脚本文件

    • 将前面提供的完整代码保存为 rename.js

使用方法

1. 直接运行(作为脚本)

bash

复制

# 添加执行权限(Linux/Mac)
chmod +x rename.js

# 运行
./rename.js -i  # 交互模式
./rename.js /path/to/folder -p "new_"  # 命令行模式

2. 通过 Node 运行

bash

复制

node rename.js -i  # 交互模式
node rename.js /path/to/folder -s "_backup"  # 命令行模式

3. 作为模块使用

javascript

复制

// 在你的项目中
const { batchRenameFiles } = require('./rename.js');

batchRenameFiles('/path/to/files', {
  prefix: '2023_',
  replaceFrom: 'old',
  replaceTo: 'new'
});