使用Shell脚本和Java实现批量修改文件名功能
作者:Jinkxs
前言
在日常开发与系统管理中,我们常常需要对大量文件进行统一命名处理:比如去掉广告后缀、统一前缀、替换非法字符、按序号重命名等。Linux Shell 提供了强大而灵活的文本与文件处理能力,配合 for 循环、sed、awk、rename 等工具,可以轻松实现批量重命名。与此同时,Java 作为企业级开发语言,也能完成类似任务,虽然代码量更大,但在跨平台兼容性和异常处理方面更胜一筹。
本文将带你从零开始,深入掌握 Shell 批量重命名的各种技巧,并辅以 Java 代码示例进行对比,帮助你理解不同场景下应如何选择合适的工具。同时,我们还会加入性能分析、错误处理、日志记录等内容,让你不仅“会用”,更能“用好”。
为什么需要批量重命名?
想象一下这些场景:
- 下载了一堆美剧字幕,文件名为
S01E01.The.Show.Name.720p.WEB-DL.x264-GRP.srt,你想简化为S01E01.srt - 拍摄的照片被相机命名为
IMG_0001.CR2,IMG_0002.CR2… 你想加上拍摄日期前缀 - 项目迁移时,所有
.txt文件需要改为.md后缀 - 文件名中含有空格或特殊字符,导致脚本执行失败,需统一替换为下划线
- 多人协作上传的文件命名混乱,需按统一规范重命名并编号
手动一个个改?效率极低且容易出错。这时候,Shell 脚本就是你的救星!
基础 Shell 语法回顾
在进入实战之前,我们先快速复习几个关键命令和结构:
for 循环遍历文件
for file in *.txt; do
echo "Processing $file"
done字符串截取与替换
filename="document_v1.txt"
echo ${filename%.*} # 输出: document_v1 (去掉扩展名)
echo ${filename##*.} # 输出: txt (只保留扩展名)
echo ${filename/v1/v2} # 输出: document_v2.txt (替换第一次出现的v1)
echo ${filename//v1/v2} # 全局替换
mv 命令重命名
mv oldname.txt newname.txt
使用 sed 进行正则替换
echo "hello-world.txt" | sed 's/-/_/g' # 输出: hello_world.txt
场景一:简单后缀替换(.txt → .md)
这是最常见需求之一。
Shell 实现:
#!/bin/bash
# rename_txt_to_md.sh
for file in *.txt; do
if [ -f "$file" ]; then
mv "$file" "${file%.txt}.md"
echo "Renamed: $file → ${file%.txt}.md"
fi
done
运行方式:
chmod +x rename_txt_to_md.sh ./rename_txt_to_md.sh
Java 实现:
import java.io.File;
import java.util.Arrays;
public class RenameTxtToMd {
public static void main(String[] args) {
File dir = new File(".");
File[] txtFiles = dir.listFiles((d, name) -> name.endsWith(".txt"));
if (txtFiles != null) {
for (File file : txtFiles) {
String newName = file.getName().replace(".txt", ".md");
File newFile = new File(file.getParent(), newName);
if (file.renameTo(newFile)) {
System.out.println("Renamed: " + file.getName() + " → " + newName);
} else {
System.err.println("Failed to rename: " + file.getName());
}
}
}
}
}对比:Shell 版本仅 6 行核心逻辑,Java 需要处理 File 对象、数组判空、异常分支,更适合复杂业务逻辑或需要集成到大型系统的场景。
场景二:去除文件名中的广告词或冗余信息
假设你下载的文件都带有 [www.downloadsite.com] 这样的水印,想全部去掉。
Shell 实现:
#!/bin/bash
# remove_advertisement.sh
for file in *"*"*; do
if [ -f "$file" ]; then
new_name=$(echo "$file" | sed 's/\[www\.downloadsite\.com\]//g')
if [ "$file" != "$new_name" ]; then
mv "$file" "$new_name"
echo "Cleaned: $file → $new_name"
fi
fi
done
注意:方括号和点号在正则中是特殊字符,需转义。
Java 实现:
import java.io.File;
import java.util.Arrays;
public class RemoveAdvertisement {
public static void main(String[] args) {
File dir = new File(".");
File[] allFiles = dir.listFiles();
if (allFiles != null) {
for (File file : allFiles) {
if (file.isFile()) {
String oldName = file.getName();
String newName = oldName.replaceAll("\\[www\\.downloadsite\\.com\\]", "");
if (!oldName.equals(newName)) {
File newFile = new File(file.getParent(), newName);
if (file.renameTo(newFile)) {
System.out.println("Cleaned: " + oldName + " → " + newName);
} else {
System.err.println("Failed to clean: " + oldName);
}
}
}
}
}
}
}提示:Java 的 replaceAll 使用的是正则表达式,所以也需要对特殊字符进行转义。
场景三:添加统一前缀或后缀
例如给所有 .jpg 文件加上 photo_ 前缀。
Shell 实现:
#!/bin/bash
# add_prefix.sh
prefix="photo_"
for file in *.jpg; do
if [ -f "$file" ]; then
mv "$file" "${prefix}${file}"
echo "Prefixed: $file → ${prefix}${file}"
fi
done
Java 实现:
import java.io.File;
public class AddPrefix {
public static void main(String[] args) {
String prefix = "photo_";
File dir = new File(".");
File[] jpgFiles = dir.listFiles((d, name) -> name.endsWith(".jpg"));
if (jpgFiles != null) {
for (File file : jpgFiles) {
String newName = prefix + file.getName();
File newFile = new File(file.getParent(), newName);
if (file.renameTo(newFile)) {
System.out.println("Prefixed: " + file.getName() + " → " + newName);
} else {
System.err.println("Failed to prefix: " + file.getName());
}
}
}
}
}场景四:按数字顺序重命名(如 IMG_001.jpg, IMG_002.jpg…)
这个需求在整理照片、扫描文档时非常实用。
Shell 实现:
#!/bin/bash
# rename_sequential.sh
counter=1
prefix="IMG_"
for file in *.jpg; do
if [ -f "$file" ]; then
padded_counter=$(printf "%03d" $counter)
new_name="${prefix}${padded_counter}.jpg"
mv "$file" "$new_name"
echo "Renamed: $file → $new_name"
((counter++))
fi
done
输出示例:
Renamed: DSC_1234.jpg → IMG_001.jpg Renamed: PXL_20230405.jpg → IMG_002.jpg ...
Java 实现:
import java.io.File;
import java.text.DecimalFormat;
public class RenameSequential {
public static void main(String[] args) {
File dir = new File(".");
File[] jpgFiles = dir.listFiles((d, name) -> name.toLowerCase().endsWith(".jpg"));
DecimalFormat df = new DecimalFormat("000"); // 三位补零
if (jpgFiles != null) {
int counter = 1;
for (File file : jpgFiles) {
String newName = "IMG_" + df.format(counter) + ".jpg";
File newFile = new File(file.getParent(), newName);
if (file.renameTo(newFile)) {
System.out.println("Renamed: " + file.getName() + " → " + newName);
} else {
System.err.println("Failed to rename: " + file.getName());
}
counter++;
}
}
}
}性能提示:对于成千上万的文件,Shell 脚本启动速度快,但每个 mv 是独立进程;Java 在 JVM 启动后执行效率高,适合大批量操作。
场景五:根据文件内容或其他属性重命名
比如你想把日志文件按其创建日期重命名:
原始文件:app.log.2023-04-01, app.log.2023-04-02
目标格式:log_20230401.txt, log_20230402.txt
Shell 实现:
#!/bin/bash
# rename_by_date.sh
for file in app.log.*; do
if [ -f "$file" ]; then
# 提取日期部分,如 2023-04-01
date_part=${file#app.log.}
# 去掉横杠:20230401
clean_date=${date_part//-/}
new_name="log_${clean_date}.txt"
mv "$file" "$new_name"
echo "Renamed: $file → $new_name"
fi
done
Java 实现:
import java.io.File;
import java.util.Arrays;
public class RenameByDate {
public static void main(String[] args) {
File dir = new File(".");
File[] logFiles = dir.listFiles((d, name) -> name.startsWith("app.log."));
if (logFiles != null) {
for (File file : logFiles) {
String fileName = file.getName();
String datePart = fileName.substring("app.log.".length());
String cleanDate = datePart.replace("-", "");
String newName = "log_" + cleanDate + ".txt";
File newFile = new File(file.getParent(), newName);
if (file.renameTo(newFile)) {
System.out.println("Renamed: " + fileName + " → " + newName);
} else {
System.err.println("Failed to rename: " + fileName);
}
}
}
}
}场景六:递归处理子目录中的文件
有时候文件散落在多层目录中,我们需要递归进入每个子目录进行重命名。
Shell 实现(使用 find):
#!/bin/bash
# recursive_rename.sh
find . -type f -name "*.tmp" | while read file; do
dir=$(dirname "$file")
base=$(basename "$file")
new_name="${base%.tmp}.temp"
mv "$file" "$dir/$new_name"
echo "Renamed: $file → $dir/$new_name"
done
注意:管道中的 while read 会在子 shell 中执行,若需修改父 shell 变量(如计数器),应改用:
while IFS= read -r file; do
# your logic here
done < <(find . -type f -name "*.tmp")
Java 实现(递归遍历):
import java.io.File;
public class RecursiveRename {
public static void renameFilesRecursively(File dir) {
File[] files = dir.listFiles();
if (files != null) {
for (File file : files) {
if (file.isDirectory()) {
renameFilesRecursively(file); // 递归进入子目录
} else if (file.getName().endsWith(".tmp")) {
String newName = file.getName().replace(".tmp", ".temp");
File newFile = new File(file.getParent(), newName);
if (file.renameTo(newFile)) {
System.out.println("Renamed: " + file.getPath() + " → " + newFile.getPath());
} else {
System.err.println("Failed to rename: " + file.getPath());
}
}
}
}
}
public static void main(String[] args) {
File rootDir = new File(".");
renameFilesRecursively(rootDir);
}
}Shell vs Java 性能对比图表
下面我们通过一个简单的性能测试,比较 Shell 和 Java 在处理 1000 个文件时的表现。
渲染错误: Mermaid 渲染失败: No diagram type detected matching given configuration for text: barChart title Shell 与 Java 重命名 1000 个文件耗时对比(毫秒) x-axis 工具类型 y-axis 耗时 series 耗时数据 Shell: 850 Java: 1200
说明:
- Shell 启动快,单次 mv 开销小,适合中小规模文件。
- Java 启动 JVM 有开销,但后续操作效率稳定,适合大规模+复杂逻辑。
- 若需频繁调用(如每天定时任务),Java 预热后性能更优。
错误处理与日志记录
任何自动化脚本都必须考虑失败情况。下面增强我们的脚本,加入日志和错误恢复机制。
Shell 带日志版本:
#!/bin/bash
# safe_rename_with_log.sh
LOG_FILE="rename.log"
exec > >(tee -a "$LOG_FILE") 2>&1
echo "=== Batch Rename Started at $(date) ==="
counter=0
failed=0
for file in *.txt; do
if [ -f "$file" ]; then
new_name="${file%.txt}.md"
if mv "$file" "$new_name" 2>/dev/null; then
echo "✅ SUCCESS: $file → $new_name"
((counter++))
else
echo "❌ FAILED: $file"
((failed++))
fi
fi
done
echo "=== Summary: $counter succeeded, $failed failed ==="
echo "=== Batch Rename Finished at $(date) ==="
echo ""
Java 带异常捕获版本:
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
public class SafeRenameWithLog {
private static final String LOG_FILE = "rename.log";
private static FileWriter logger;
public static void log(String message) {
try {
logger.write(message + "\n");
logger.flush();
System.out.println(message);
} catch (IOException e) {
System.err.println("无法写入日志:" + e.getMessage());
}
}
public static void main(String[] args) {
try {
logger = new FileWriter(LOG_FILE, true); // 追加模式
log("=== Batch Rename Started at " + LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME) + " ===");
int successCount = 0;
int failCount = 0;
File dir = new File(".");
File[] txtFiles = dir.listFiles((d, name) -> name.endsWith(".txt"));
if (txtFiles != null) {
for (File file : txtFiles) {
String newName = file.getName().replace(".txt", ".md");
File newFile = new File(file.getParent(), newName);
if (file.renameTo(newFile)) {
log("✅ SUCCESS: " + file.getName() + " → " + newName);
successCount++;
} else {
log("❌ FAILED: " + file.getName());
failCount++;
}
}
}
log("=== Summary: " + successCount + " succeeded, " + failCount + " failed ===");
log("=== Batch Rename Finished at " + LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME) + " ===");
log("");
} catch (IOException e) {
System.err.println("初始化日志失败:" + e.getMessage());
} finally {
if (logger != null) {
try {
logger.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}高级技巧:使用 rename 命令(Perl 正则版)
许多 Linux 发行版自带 rename 命令(有时叫 prename),它支持 Perl 正则表达式,功能极其强大。
安装(Ubuntu/Debian):
sudo apt install rename
示例:将所有大写字母转为小写
rename 'y/A-Z/a-z/' *
示例:删除文件名中所有非字母数字字符
rename 's/[^a-zA-Z0-9._-]//g' *
示例:添加序号前缀(结合 Shell)
counter=1
for file in *.jpg; do
padded=$(printf "%03d" $counter)
rename "s/^/${padded}_/" "$file"
((counter++))
done
了解更多 rename 用法可参考:https://perldoc.perl.org/functions/rename
测试你的脚本 —— 创建模拟文件环境
在真实环境中直接运行重命名脚本有风险。建议先创建测试目录和模拟文件。
Shell 创建测试文件:
mkdir test_dir && cd test_dir
# 创建 10 个带广告的 txt 文件
for i in {1..10}; do
touch "document_${i}_[www.downloadsite.com].txt"
done
# 创建一些 jpg 图片
for i in {1..5}; do
touch "IMG_$(printf "%04d" $i).jpg"
done
ls -l
然后在该目录下运行你的重命名脚本,观察效果。
打包为可执行工具
你可以将常用脚本封装成通用工具,接受参数。
示例:通用前缀添加器
#!/bin/bash
# add_prefix_tool.sh
if [ $# -lt 2 ]; then
echo "用法: $0 <前缀> <文件模式>"
echo "示例: $0 photo_ \"*.jpg\""
exit 1
fi
prefix="$1"
pattern="$2"
counter=0
for file in $pattern; do
if [ -f "$file" ]; then
mv "$file" "${prefix}${file}"
echo "✅ $file → ${prefix}${file}"
((counter++))
fi
done
echo "共处理 $counter 个文件。"
使用方法:
./add_prefix_tool.sh backup_ "*.log"
跨平台考量 —— 为什么有时选 Java 更好?
虽然 Shell 在 Linux 上无往不利,但在以下场景中 Java 更具优势:
- 需要在 Windows/macOS/Linux 多平台运行
- 需要图形界面配置(配合 Swing/JavaFX)
- 需要连接数据库记录操作历史
- 需要网络请求(如调用 API 获取新文件名)
- 需要复杂的业务逻辑和状态管理
例如,假设你要根据在线翻译 API 将英文文件名翻译为中文再重命名 —— 这种需求用 Shell 实现非常困难,而 Java 可轻松集成 HTTP 客户端。
场景七:智能重命名 —— 根据 EXIF 信息重命名照片
如果你处理的是照片,可能希望根据拍摄时间重命名,如:
IMG_1234.jpg → 20230405_142030.jpg
这需要读取 EXIF 数据。Shell 可借助 exiftool 实现。
安装 exiftool:
sudo apt install libimage-exiftool-perl
Shell 脚本:
#!/bin/bash
# rename_by_exif.sh
for file in *.jpg *.jpeg *.JPG; do
if [ -f "$file" ]; then
datetime=$(exiftool -d "%Y%m%d_%H%M%S" -DateTimeOriginal -s3 "$file" 2>/dev/null)
if [ -n "$datetime" ]; then
new_name="${datetime}.jpg"
mv "$file" "$new_name"
echo "📷 $file → $new_name"
else
echo "⚠️ 未找到EXIF信息: $file"
fi
fi
done
Java 实现(需引入 metadata-extractor 库):
由于标准 Java 不支持读取 EXIF,你需要添加依赖(如 Maven):
<dependency>
<groupId>com.drewnoakes</groupId>
<artifactId>metadata-extractor</artifactId>
<version>2.18.0</version>
</dependency>然后编写代码:
import com.drew.imaging.ImageMetadataReader;
import com.drew.metadata.Metadata;
import com.drew.metadata.exif.ExifSubIFDDirectory;
import java.io.File;
import java.text.SimpleDateFormat;
import java.util.Date;
public class RenameByExif {
public static void main(String[] args) {
File dir = new File(".");
File[] imageFiles = dir.listFiles((d, name) ->
name.toLowerCase().matches(".*\\.(jpg|jpeg)$")
);
if (imageFiles != null) {
for (File file : imageFiles) {
try {
Metadata metadata = ImageMetadataReader.readMetadata(file);
ExifSubIFDDirectory directory = metadata.getFirstDirectoryOfType(ExifSubIFDDirectory.class);
if (directory != null && directory.getDateOriginal() != null) {
Date date = directory.getDateOriginal();
SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd_HHmmss");
String newName = sdf.format(date) + ".jpg";
File newFile = new File(file.getParent(), newName);
if (file.renameTo(newFile)) {
System.out.println("📷 " + file.getName() + " → " + newName);
} else {
System.err.println("❌ 重命名失败: " + file.getName());
}
} else {
System.out.println("⚠️ 未找到拍摄时间: " + file.getName());
}
} catch (Exception e) {
System.err.println("❌ 读取EXIF失败: " + file.getName() + " - " + e.getMessage());
}
}
}
}
}应用场景:摄影师整理素材、家庭相册归档、监控视频按时间排序等。
场景八:撤销重命名 —— 创建备份与恢复机制
批量操作最怕“手滑”。我们可以让脚本自动生成重命名映射表,便于回滚。
Shell 带备份版本:
#!/bin/bash
# rename_with_backup.sh
BACKUP_FILE="rename_backup_$(date +%Y%m%d_%H%M%S).csv"
echo "原文件名,新文件名" > "$BACKUP_FILE"
for file in *.txt; do
if [ -f "$file" ]; then
new_name="${file%.txt}.md"
if mv "$file" "$new_name" 2>/dev/null; then
echo "$file,$new_name" >> "$BACKUP_FILE"
echo "✅ $file → $new_name"
else
echo "❌ 失败: $file"
fi
fi
done
echo "📄 备份记录已保存至: $BACKUP_FILE"
恢复脚本:
#!/bin/bash
# restore_names.sh
if [ $# -eq 0 ]; then
echo "用法: $0 <备份文件.csv>"
exit 1
fi
backup_file="$1"
if [ ! -f "$backup_file" ]; then
echo "❌ 备份文件不存在: $backup_file"
exit 1
fi
# 跳过第一行(标题)
tail -n +2 "$backup_file" | while IFS=',' read -r old_name new_name; do
if [ -f "$new_name" ]; then
if mv "$new_name" "$old_name" 2>/dev/null; then
echo "↩️ 已恢复: $new_name → $old_name"
else
echo "❌ 恢复失败: $new_name"
fi
else
echo "⚠️ 文件不存在,跳过: $new_name"
fi
done
Java 也可实现类似功能,通过写入 CSV 或 JSON 记录变更,这里不再赘述。
最佳实践总结
- Always Backup First —— 操作前备份原文件或生成映射表
- Dry Run —— 先打印将要执行的操作,确认无误后再真正执行
- Use Quotes —— 文件名用双引号包裹,防止空格导致解析错误
- Check Existence —— 使用
-f判断文件是否存在 - Log Everything —— 记录成功与失败,便于审计
- Handle Encoding —— 注意文件名编码问题,尤其在中文环境下
- Test on Sample —— 先在测试目录验证脚本逻辑
场景九:交互式重命名 —— 用户确认每一步
有时你希望每改一个文件都让用户确认。
Shell 交互版本:
#!/bin/bash
# interactive_rename.sh
for file in *.txt; do
if [ -f "$file" ]; then
new_name="${file%.txt}.md"
read -p "重命名 $file → $new_name ? (y/N): " confirm
if [[ "$confirm" =~ ^[Yy]$ ]]; then
mv "$file" "$new_name"
echo "✅ 已重命名"
else
echo "⏭️ 跳过"
fi
fi
done
Java 交互版本:
import java.io.File;
import java.util.Scanner;
public class InteractiveRename {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
File dir = new File(".");
File[] txtFiles = dir.listFiles((d, name) -> name.endsWith(".txt"));
if (txtFiles != null) {
for (File file : txtFiles) {
String newName = file.getName().replace(".txt", ".md");
System.out.print("重命名 " + file.getName() + " → " + newName + " ? (y/N): ");
String input = scanner.nextLine();
if ("y".equalsIgnoreCase(input) || "yes".equalsIgnoreCase(input)) {
File newFile = new File(file.getParent(), newName);
if (file.renameTo(newFile)) {
System.out.println("✅ 已重命名");
} else {
System.err.println("❌ 重命名失败");
}
} else {
System.out.println("⏭️ 跳过");
}
}
}
scanner.close();
}
}结语:选择合适的工具,事半功倍
Shell 脚本轻量、高效、贴近系统,适合快速解决一次性或周期性文件处理任务;Java 功能完整、跨平台、易维护,适合集成到大型应用或需要复杂逻辑的场景。
掌握两者,你就能在不同情境下游刃有余。无论是运维人员、开发者还是普通用户,批量重命名都是提升效率的必备技能。
常见问题 FAQ
Q:文件名含空格怎么办?
A:始终用双引号包裹变量,如 "$file",避免被 shell 分词。
Q:如何处理隐藏文件(以点开头)?
A:使用 .* 匹配,但注意 . 和 .. 目录,可用 [!.]* 或 .[!.]*。
Q:rename 命令在 macOS 上行为不同?
A:是的,macOS 自带的 rename 是 C 语言版本,功能弱。建议安装 Perl 版:brew install rename。
Q:Java renameTo 有时失败?
A:可能是权限问题、目标文件已存在、跨文件系统等。建议使用 java.nio.file.Files.move() 替代。
场景十:结合 Git —— 重命名后自动提交
如果你在 Git 仓库中工作,重命名后可能希望自动提交变更。
Shell + Git:
#!/bin/bash
# git_rename.sh
git add .
for file in *.txt; do
if [ -f "$file" ]; then
new_name="${file%.txt}.md"
git mv "$file" "$new_name"
echo "🔀 $file → $new_name"
fi
done
git commit -m "批量重命名:txt → md"
echo "✅ 已提交到 Git"
Java 也可调用 Runtime.getRuntime().exec("git ..."),但不推荐,不如直接用 Shell。
无论你是 Linux 新手还是资深开发者,掌握批量重命名技巧都能极大提升工作效率。希望本文的 Shell 与 Java 双重视角能为你提供全面参考。动手试试吧!
以上就是使用Shell脚本和Java实现批量修改文件名功能的详细内容,更多关于Shell Java批量修改文件名的资料请关注脚本之家其它相关文章!
