当Metadata失效时:我是如何从2000张手机自拍照片里“考古“出真实拍摄时间的?


在这里插入图片描述

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

在这里插入图片描述

对于一张图片,可以查看到它的拍摄信息如下:这里最重要的就是根据这个拍摄日期来进行重命名排序,但是有个需要注意的是拍摄日期只识别到了分钟,所以比如同一天同一个小时同一分钟的照片,直接重命名就会覆盖,我们可以在这个基础上进行递增即可,类似于我们的自增主键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实例后,可以迭代或查询从图像中读取的各种标签值。

编写程序代码处理

实现思路

  1. EXIF信息读取:使用metadata-extractor库提取图片的DateTimeOriginal字段
  2. 时间格式化:将拍摄时间转换为yyyyMMddHHmm格式(精确到分钟)
  3. 序号生成:通过ConcurrentHashMap记录每分钟的拍摄次数,实现递增序号
  4. 线程安全:使用AtomicInteger保证多文件处理的原子性操作
  5. 文件操作:优先采用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 个文件。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Apple_Web

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值