最近换了新手机,想把旧手机里的照片整理到电脑里再上传网盘。结果发现从手机拷到电脑的照片,修改时间全变成一样的了!看着文件列表里密密麻麻的相同时间戳,我这个强迫症患者简直不能忍。作为程序员,职业病马上犯了——既然系统时间不靠谱,为什么不直接读取照片的拍摄信息来重命名呢?

对于一张图片,可以查看到它的拍摄信息如下:这里最重要的就是根据这个拍摄日期来进行重命名排序,但是有个需要注意的是拍摄日期只识别到了分钟,所以比如同一天同一个小时同一分钟的照片,直接重命名就会覆盖,我们可以在这个基础上进行递增即可,类似于我们的自增主键id!202303191429_001、202303191429_002

metadata-extractor介绍
而对于如何识别图片的拍摄信息呢?这里有一个Java库:metadata-extractor,它用于从媒体文件中提取元数据。它支持多种格式的元数据,包括EXIF、IPTC、XMP、ICC以及其他可能在单个图像中存在的元数据格式。这个库可以处理多种类型的文件,包括JPEG、TIFF、WebP、WAV、PSD、PNG、BMP、GIF、HEIF (HEIC & AVIF)、ICO、PCX、QuickTime、MP4、Camera Raw等。
使用
依赖引入:需添加metadata-extractor依赖(Maven配置):
<dependency>
<groupId>com.drewnoakes</groupId>
<artifactId>metadata-extractor</artifactId>
<version>2.18.0</version>
</dependency>
使用metadata-extractor库,可以读取图像文件的元数据,如下所示:
Metadata metadata = ImageMetadataReader.readMetadata(imagePath);
有了Metadata实例后,可以迭代或查询从图像中读取的各种标签值。
编写程序代码处理
实现思路
- EXIF信息读取:使用
metadata-extractor库提取图片的DateTimeOriginal字段 - 时间格式化:将拍摄时间转换为
yyyyMMddHHmm格式(精确到分钟) - 序号生成:通过
ConcurrentHashMap记录每分钟的拍摄次数,实现递增序号 - 线程安全:使用
AtomicInteger保证多文件处理的原子性操作 - 文件操作:优先采用NIO的
Files.move()方法
public class PhotoRenamer {
// 线程安全的时间戳计数器(用于生成唯一序号)
private static final Map<String, AtomicInteger> TIME_COUNTER = new ConcurrentHashMap<>();
// 日期格式化器(用于生成时间戳前缀)
private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyyMMddHHmm");
/**
* 主入口方法
* @param args 命令行参数(未使用)
*/
public static void main(String[] args) {
File dir = new File("D:\\test"); // 要处理的目录路径
processDirectory(dir);
}
/**
* 处理指定目录下的图片文件
* @param dir 要处理的目录对象
*/
private static void processDirectory(File dir) {
// 获取目录下所有图片文件(并行流处理)
File[] files = dir.listFiles(file ->
file.isFile() && file.getName().toLowerCase().matches(".*\\.(jpg|jpeg|png|heic)$")
);
if (files == null) return;
Arrays.stream(files).parallel().forEach(file -> {
try {
// 获取照片拍摄时间
Date shootDate = getShootDate(file);
if (shootDate == null) {
System.out.println("跳过无EXIF信息的文件: " + file.getName());
return;
}
// 生成时间戳并获取顺序号
String timeKey = DATE_FORMAT.format(shootDate);
int sequence = TIME_COUNTER
.computeIfAbsent(timeKey, k -> new AtomicInteger(0))
.incrementAndGet();
// 构建新文件名(格式:时间戳_序号.扩展名)
String newName = String.format("%s_%03d%s",
timeKey,
sequence,
getFileExtension(file.getName())
);
// 执行文件重命名操作
File newFile = new File(file.getParent(), newName);
Files.move(
file.toPath(),
newFile.toPath(),
StandardCopyOption.REPLACE_EXISTING
);
System.out.printf("重命名成功: %s -> %s%n", file.getName(), newName);
} catch (Exception e) {
System.err.println("处理文件失败: " + file.getName() + " - " + e.getMessage());
}
});
}
/**
* 从文件元数据中获取拍摄时间
* @param file 图片文件对象
* @return 拍摄日期对象(可能为null)
* @throws Exception 读取元数据失败时抛出异常
*/
private static Date getShootDate(File file) throws Exception {
Metadata metadata = ImageMetadataReader.readMetadata(file);
ExifSubIFDDirectory directory = metadata.getFirstDirectoryOfType(ExifSubIFDDirectory.class);
return directory != null ?
directory.getDate(ExifSubIFDDirectory.TAG_DATETIME_ORIGINAL) :
null;
}
/**
* 获取文件扩展名(小写)
* @param fileName 原始文件名
* @return 包含点的扩展名(如.jpg)
*/
private static String getFileExtension(String fileName) {
int dotIndex = fileName.lastIndexOf('.');
return (dotIndex == -1) ? "" : fileName.substring(dotIndex);
}
}
异常处理:
- 跳过非图片文件(已过滤常见图片格式)
- 无EXIF信息的文件会输出警告
- 文件冲突时会强制覆盖(
StandardCopyOption.REPLACE_EXISTING)
发现问题
运行以后发现,会有部分照片没有EXIF信息!
跳过无EXIF信息的文件: MYXJ_20210101144459632_fast.jpg
跳过无EXIF信息的文件: MYXJ_20221003213437883_fast.jpg
跳过无EXIF信息的文件: MYXJ_20221003213232856_fast.jpg
重命名成功: IMG_20230312_134030.jpg -> 202309041836_003.jpg
重命名成功: IMG_20230312_133956.jpg -> 202309041836_001.jpg
重命名成功: IMG_20221004_103623.jpg -> 202210041836_001.jpg
重命名成功: IMG_20240915_150501.jpg -> 202409010332_001.jpg
重命名成功: IMG_20241001_152208.jpg -> 202410041836_001.jpg
重命名成功: IMG_20230312_134112.jpg -> 202309041836_002.jpg
重命名成功: IMG_20240930_193232.jpg -> 202410010332_001.jpg
重命名成功: IMG_20221004_101808.jpg -> 202209041836_001.jpg
重命名成功: IMG_20240930_193229.jpg -> 202409041836_001.jpg
重命名成功: IMG_20230312_134201.jpg -> 202303122142_001.jpg
重命名成功: IMG_20221004_103641.jpg -> 202210040332_001.jpg
重命名成功: IMG_20230312_134158.jpg -> 202309041836_004.jpg
重命名成功: IMG_20240928_141322.jpg -> 202409282213_001.jpg
重命名成功: IMG_20240915_150913.jpg -> 202409152309_001.jpg
重命名成功: IMG_20221004_082639.jpg -> 202210041626_001.jpg
重命名成功: IMG_20240915_150340.jpg -> 202409152303_001.jpg
重命名成功: IMG_20221004_103754.jpg -> 202210041837_001.jpg
重命名成功: IMG20250405144111.jpg -> 202504052241_001.jpg
重命名成功: IMG20250405144116.jpg -> 202504052241_002.jpg
重命名成功: AGC9.9_R3_20250405_144833822.jpg -> 202504052248_001.jpg
我仔细查看了下这些照片都是自拍的照片

那么这部分照片我也想要按照时间排序又如何解决呢,后面仔细观察一下,这些手机拍摄的照片命名都是有规则的,都是英文前缀+YYYYMMDD_HHMMSS 或 YYYYMMDDHHMMSS类似的格式,那么我们就可以把问题简单化,采用正则表达式来匹配名称进行重命名!
AGC9.9_R3_20250405_141029233.jpg
IMG20250405152829.jpg
MYXJ_20210101144459632_fast.jpg
使用正则表达式解决问题
/**
* 智能文件重命名工具类
* 功能:自动识别媒体文件中的日期时间信息,并按日期+序列号的格式进行重命名
* 支持格式:YYYYMMDD_HHMMSS 或 YYYYMMDDHHMMSS 等组合形式
*/
public class ImprovedFileRenamer {
/**
* 主程序入口
* @param args 命令行参数(本程序未使用)
*/
public static void main(String[] args) {
// 设置要处理的文件夹路径
String folderPath = "D:\\test";
// 检查文件夹有效性
File folder = new File(folderPath);
if (!folder.exists() || !folder.isDirectory()) {
System.out.println("指定的文件夹不存在或不是一个目录!");
return;
}
// 获取文件夹中的文件列表
File[] files = folder.listFiles();
if (files == null || files.length == 0) {
System.out.println("文件夹中没有文件!");
return;
}
// 改进版正则表达式,匹配多种日期时间格式:
// 1. 20开头年份(20xx)
// 2. 匹配月份(01-12)
// 3. 匹配日期(01-31)
// 4. 可选分隔符 _
// 5. 匹配小时(00-23)
// 6. 匹配分钟(00-59)
// 7. 可选秒数(00-59)
Pattern pattern = Pattern.compile(".*?(20\\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\\d|3[01]))[_]?([01]\\d|2[0-3])([0-5]\\d)([0-5]\\d)?.*");
// 使用Map记录每个日期对应的文件计数
Map<String, Integer> dateCountMap = new HashMap<>();
int renamedCount = 0; // 成功重命名计数器
// 遍历处理每个文件
for (File file : files) {
String oldName = file.getName();
// 跳过目录处理
if (file.isDirectory()) {
continue;
}
// 获取并验证文件扩展名
String extension = getFileExtension(oldName).toLowerCase();
if (!isMediaFile(extension)) {
System.out.println("跳过非媒体文件: " + oldName);
continue;
}
// 使用正则匹配日期时间信息
Matcher matcher = pattern.matcher(oldName);
if (matcher.find()) {
// 提取日期部分(YYYYMMDD格式)
String datePart = matcher.group(1);
// 更新并获取当前日期的文件计数(从1开始递增)
int count = dateCountMap.getOrDefault(datePart, 0) + 1;
dateCountMap.put(datePart, count);
// 构建新文件名格式:日期_序号.扩展名(序号3位补零)
String newName = String.format("%s_%03d.%s", datePart, count, extension);
// 执行重命名操作
File newFile = new File(file.getParent(), newName);
if (file.renameTo(newFile)) {
System.out.println("重命名成功: " + oldName + " -> " + newName);
renamedCount++;
} else {
System.out.println("重命名失败: " + oldName);
}
} else {
System.out.println("未找到符合规则的日期时间部分: " + oldName);
}
}
System.out.println("处理完成!共重命名了 " + renamedCount + " 个文件。");
}
/**
* 获取文件扩展名
* @param filename 原始文件名
* @return 文件扩展名(小写),无扩展名时返回空字符串
*/
private static String getFileExtension(String filename) {
int dotIndex = filename.lastIndexOf('.');
return (dotIndex == -1) ? "" : filename.substring(dotIndex + 1);
}
/**
* 判断是否为支持的媒体文件类型
* @param extension 文件扩展名
* @return true表示是媒体文件,false表示不支持
*/
private static boolean isMediaFile(String extension) {
// 支持的图片格式
String[] imageExtensions = {"jpg", "jpeg", "png", "gif", "bmp", "webp"};
// 支持的视频格式
String[] videoExtensions = {"mp4", "mov", "avi", "mkv", "flv", "wmv"};
// 检查图片类型
for (String ext : imageExtensions) {
if (ext.equals(extension)) {
return true;
}
}
// 检查视频类型
for (String ext : videoExtensions) {
if (ext.equals(extension)) {
return true;
}
}
return false;
}
}
这里核心就是采用正则表达式来匹配查找到我们的文件名,然后进行重命名,并且对于同一分钟的多个文件需要递增排序命名
Pattern.compile(".*?(20\\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\\d|3[01]))[_]?([01]\\d|2[0-3])([0-5]\\d)([0-5]\\d)?.*")
日期匹配与计数
- 提取文件名中的 YYYYMMDD 日期部分
- 使用 HashMap 记录每个日期的文件出现次数
- 自动生成3位序列号(如 20230415_001.jpg)
最终执行发现可以满足我们的目的了:
重命名成功: AGC9.9_R3_20250405_144833822.jpg -> 20250405_001.jpg
重命名成功: IMG20250405144111.jpg -> 20250405_002.jpg
重命名成功: IMG20250405144116.jpg -> 20250405_003.jpg
重命名成功: IMG_20221004_082639.jpg -> 20221004_001.jpg
重命名成功: IMG_20221004_101808.jpg -> 20221004_002.jpg
重命名成功: IMG_20221004_103623.jpg -> 20221004_003.jpg
重命名成功: IMG_20221004_103641.jpg -> 20221004_004.jpg
重命名成功: IMG_20221004_103754.jpg -> 20221004_005.jpg
重命名成功: IMG_20230312_133956.jpg -> 20230312_001.jpg
重命名成功: IMG_20230312_134030.jpg -> 20230312_002.jpg
重命名成功: IMG_20230312_134112.jpg -> 20230312_003.jpg
重命名成功: IMG_20230312_134158.jpg -> 20230312_004.jpg
重命名成功: IMG_20230312_134201.jpg -> 20230312_005.jpg
重命名成功: IMG_20240915_150340.jpg -> 20240915_001.jpg
重命名成功: IMG_20240915_150501.jpg -> 20240915_002.jpg
重命名成功: IMG_20240915_150913.jpg -> 20240915_003.jpg
重命名成功: IMG_20240928_141322.jpg -> 20240928_001.jpg
重命名成功: IMG_20240930_193229.jpg -> 20240930_001.jpg
重命名成功: IMG_20240930_193232.jpg -> 20240930_002.jpg
重命名成功: IMG_20241001_152208.jpg -> 20241001_001.jpg
重命名成功: MYXJ_20210101144459632_fast.jpg -> 20210101_001.jpg
重命名成功: MYXJ_20221003213232856_fast.jpg -> 20221003_001.jpg
重命名成功: MYXJ_20221003213437883_fast.jpg -> 20221003_002.jpg
重命名成功: VID_20240915_145014.mp4 -> 20240915_004.mp4
重命名成功: VID_20240915_150600.mp4 -> 20240915_005.mp4
处理完成!共重命名了 25 个文件。

5万+

被折叠的 条评论
为什么被折叠?



