1. 项目背景

开发这个功能的主要原因如下:
1. 大学期间拍摄了约50G的照片,照片很多
2. 存放不规范,导致同一张照片出现在不同的文件夹内,可读性差,无法形成记忆线。
3. 重复存放过多,很多照片都有冗余备份,导致磁盘空间越来越不够用。

2. 解决思路

  1. 根据照片拍摄时间对照片文件重命名,并移动到统一文件夹内。
  2. 重复文件只移动一份,结果是除了目标文件夹内的照片以外,其他照片都是冗余照片。

注意:并非所有照片都有拍摄时间,只有数码相机与手机拍摄的才有。部分网上下载的图片也有原始拍摄时间。没有拍摄时间的照片不作处理。

3. 项目概述

3.1 项目依赖


这里的依赖都比较普通,只有一个比较特殊:metadata-extractor是用来提取照片中的拍摄时间的。joda-time用来规范日期格式。

3.2 项目结构


功能实现比较简单,根据业务分了biz/service/util/ui包。其中ui开发的比较粗糙,因为java开发基本上已经转入了后端,swing已经很少用到了,能跑起来就行。

3.3 项目流程图

1.重复文件删除

Created with Raphaël 2.1.0开始用户输入目录获取目录下所有文件对所有文件进行hash得到MultiMap根据hashcode删除重复的文件结束

2.按拍摄时间重命名照片

Created with Raphaël 2.1.0开始用户输入目录与后缀获取目录下所有后缀匹配文件通过metadata获得拍摄时间并重命名结束

3.移动文件到目标文件夹

Created with Raphaël 2.1.0开始用户输入目录与后缀获取目录下所有后缀匹配文件移动文件到目标文件夹结束

3.4 项目下载

代码地址:github地址
可执行应用地址:应用地址

关键代码

1.重复文件检测

package cuishining.bizz;

import java.io.File;
import java.io.IOException;
import java.util.List;
import java.util.Set; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import com.google.common.collect.HashMultimap;
import com.google.common.hash.Hashing;
import com.google.common.io.Files; import cuishining.util.FileUtil; /**
* Created by shining.cui on 2016/7/20.
*/
public class DuplicateFileDetector {
private static final Logger logger = LoggerFactory.getLogger(DuplicateFileDetector.class); public HashMultimap<Long, String> detect(String path, String nameSuffix) {
List<File> fileList = FileUtil.getAllFilesUnderPath(path, nameSuffix);
HashMultimap<Long, String> md5AndFilePathMultiMap = analyzeMd5OfAllFiles(fileList);
return analyzeDuplicateFiles(md5AndFilePathMultiMap);
} private HashMultimap<Long, String> analyzeMd5OfAllFiles(List<File> fileList) {
HashMultimap<Long, String> md5FileNameMultiMap = HashMultimap.create();
for (File file : fileList) {
logger.info("文件{},正在分析中……",file);
try {
long md5 = Files.hash(file, Hashing.md5()).asLong();
String path = file.getCanonicalPath();
md5FileNameMultiMap.put(md5, path);
} catch (IOException e) {
logger.error("文件hash出错,请检查文件是否可读。",e);
} }
return md5FileNameMultiMap;
} private HashMultimap<Long, String> analyzeDuplicateFiles(HashMultimap<Long, String> multimap) {
Set<Long> md5s = multimap.keySet();
HashMultimap<Long, String> duplicateFilesMap = HashMultimap.create();
for (Long md5 : md5s) {
Set<String> fileNames = multimap.get(md5);
// 如果对应md5的value多于1个,证明是重复的文件,放入新的map中返回
if (fileNames.size() > 1) {
for (String name : fileNames) {
duplicateFilesMap.put(md5, name);
}
}
}
return duplicateFilesMap;
}
}

2.重命名策略

package cuishining.service.impl;

import java.io.File;
import java.util.List; import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import cuishining.service.RenamePolicy;
import cuishining.util.JpgFileUtil; /**
* Created by shining.cui on 2016/7/23.
*/
public class RenameByTimePolicy implements RenamePolicy {
private static final Logger logger = LoggerFactory.getLogger(RenameByTimePolicy.class); @Override
public boolean rename(List<File> fileList) {
logger.info("接受参数fileList为:{}", fileList);
for (File file : fileList) {
String photoTimeStr = JpgFileUtil.getPhotoTimeStr(file);
if (StringUtils.isEmpty(photoTimeStr)) {
logger.error("文件{}不存在拍摄日期,无法重命名",file);
}
String path = file.getParentFile().getAbsolutePath();
if (StringUtils.isNotEmpty(photoTimeStr)) {
renameFile(file, photoTimeStr, path);
}
}
return true;
} private void renameFile(File file, String photoTimeStr, String path) {
logger.info("文件{}正在重命名中……",file);
File renamedFile = new File(path + File.separator + photoTimeStr + ".jpg");
if (renamedFile.exists()) {
logger.error("{}文件已经存在,无法重命名。", renamedFile);
} else {
boolean renameSuccess = file.renameTo(renamedFile);
if (renameSuccess) {
logger.info("{}文件命名为{}", file.getName(), renamedFile.getName());
}
}
}
}

3.文件处理工具

package cuishining.util;

import java.io.File;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Set; import com.google.common.collect.HashMultimap;
import com.google.common.io.Files;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import com.google.common.collect.Lists; /**
* 文件处理工具
* Created by shining.cui on 2016/7/12.
*/
public class FileUtil {
public static Logger logger = LoggerFactory.getLogger(FileUtil.class); /**
* 读取指定路径下的所有文件,使用队列实现
*
* @param filePath 指定的文件夹目录
* @param nameSuffix 指定后缀,若为null或者" "则匹配所有
* @return 文件夹及其子文件夹内所有文件
*/
public static List<File> getAllFilesUnderPath(String filePath, String nameSuffix) {
logger.info("接受的文件夹路径为:{},文件名匹配后缀为:{}", filePath, nameSuffix);
File basicfile = new File(filePath);
List<File> fileLis = Lists.newArrayList();
LinkedList<File> fileQueue = Lists.newLinkedList(Lists.newArrayList(basicfile));
while (!fileQueue.isEmpty()) {
File file = fileQueue.poll();
if (file.isDirectory() && file.listFiles() != null) {
fileQueue.addAll(Lists.newArrayList(file.listFiles()));
} else {
fileQueue = matchTheSuffix(file, nameSuffix, fileQueue, fileLis);
}
}
logger.info("得到的文件列表的长度为:{}", fileLis.size());
return fileLis;
} private static LinkedList<File> matchTheSuffix(File file, String nameSuffix, LinkedList<File> fileQueue,
List<File> fileList) {
String fileName = file.getName();
if (StringUtils.isNotEmpty(nameSuffix)
&& StringUtils.endsWith(fileName.toLowerCase(), nameSuffix.toLowerCase())) {
// 当有后缀名时,匹配的放入队列
fileList.add(file);
} else if (StringUtils.isEmpty(nameSuffix)) {
// 没有匹配名时,所有的都放入队列
fileList.add(file);
}
return fileQueue;
} public static String deleteFilesFromMultiMap(HashMultimap<Long, String> duplicateFileMultimap) {
Set<Long> md5s = duplicateFileMultimap.keySet();
StringBuilder sb = new StringBuilder();
int count = 0;
for (long md5 : md5s) {
ArrayList<String> filenames = Lists.newArrayList(duplicateFileMultimap.get(md5));
sb.append("以下重复文件:\n");
for (String filename : filenames) {
sb.append(filename).append("\n");
}
String firstDupFile = filenames.get(0);
File file = new File(firstDupFile);
boolean delete = file.delete();
if (delete) {
logger.info("文件{}已被删除", firstDupFile);
sb.append("文件").append(firstDupFile).append("已被删除");
count++;
} else {
logger.error("文件{}删除失败", firstDupFile);
}
}
sb.append("共删除").append(count).append("个文件");
logger.info("共删除{}个文件",count);
return sb.toString();
}
}

4.照片拍摄时间提取工具

package cuishining.util;

import java.io.File;
import java.io.IOException;
import java.util.Date; import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import com.drew.imaging.ImageMetadataReader;
import com.drew.imaging.ImageProcessingException;
import com.drew.metadata.Directory;
import com.drew.metadata.Metadata;
import com.drew.metadata.exif.ExifDirectoryBase; /**
* Created by shining.cui on 2016/7/23.
*/
public class JpgFileUtil {
private static final Logger logger = LoggerFactory.getLogger(JpgFileUtil.class); public static String getPhotoTimeStr(File file) {
Date date = null;
try {
Metadata metadata = ImageMetadataReader.readMetadata(file);
for (Directory dr : metadata.getDirectories()) {
if (dr.containsTag(ExifDirectoryBase.TAG_DATETIME_ORIGINAL)) {
date = dr.getDate(ExifDirectoryBase.TAG_DATETIME_ORIGINAL);
}
if (date != null) {
return TimeUtil.parseDateFromJpgFileDate(date);
}
}
} catch (ImageProcessingException e) {
logger.error("jpg文件读取错误", e);
} catch (IOException e) {
logger.error("发生io错误", e);
}
return null;
}
}
5.时间工具 package cuishining.util; import org.joda.time.DateTime;
import org.joda.time.DateTimeZone; import java.util.Date; /**
* Created by shining.cui on 2016/7/25.
*/
public class TimeUtil {
private static final String timeFormatStr = "yyyy-MM-dd HH-mm-ss";
private static final String timeFormatStr1 = "yyyy-MM-dd HH:mm:ss"; public static String parseDateFromSystemDate(Date date) {
return new DateTime(date).toString(timeFormatStr1);
} public static String parseDateFromJpgFileDate(Date date) {
return new DateTime(date, DateTimeZone.UTC).toString(timeFormatStr);
}
}

总结

项目总体思想是根据md5删除重复照片,然后根据拍摄时间重命名之后移动到统一文件夹内。可以在同一个文件夹内按照拍摄时间浏览照片,比较有历史感,容易唤起回忆。

FileDetector-基于java开发的照片整理工具的更多相关文章

  1. 纯 Java 开发 WebService 调用测试工具(wsCaller.jar)

    注:本文来自hacpai.com:Tanken的<纯 Java 开发 WebService 调用测试工具(wsCaller.jar)>的文章 基于 Java 开发的 WebService ...

  2. 基于java开发的在线题库系统tamguo

    简介 探果网(简称tamguo)是基于java开发的在线题库系统,包括 在线访问 后台运营 会员中心 书籍中心 管理员账号:system 密码:123456 因为线上数据和测试数据没有做到隔离,作者已 ...

  3. 免费开源数字货币交易所——基于Java开发的比特币交易所 | BTC交易所 | ETH交易所 | 数字货币交易所

    本项目是基于Java开发的比特币交易所 | BTC交易所 | ETH交易所 | 数字货币交易所 | 交易平台 | 撮合交易引擎.本项目基于SpringCloud微服务开发,可用来搭建和二次开发数字货币 ...

  4. Java 开发环境配置--eclipse工具进行java开发

    Java 开发环境配置 在本章节中我们将为大家介绍如何搭建Java开发环境. Windows 上安装开发环境 Linux 上安装开发环境 安装 Eclipse 运行 Java Cloud Studio ...

  5. 基于Java的简易表达式解析工具(一)

    最近需要用到相关表达式解析的工具,然后去网上搜索,找到了一个用C#写的表达式解析工具,仔细看了功能后发现,这正是我需要的,如果我能将它改造成基于Java语言的方式,岂不是更好吗,所以花了一段时间,把网 ...

  6. 来认识一下venus-init——一个让你仅需一个命令开始Java开发的命令行工具

    源代码地址: Github仓库地址 个人网站:个人网站地址 前言 不知道你是否有过这样的经历.不管你是什么岗位,前端也好,后端也罢,想去了解一下Java开发到底是什么样的,它是不是真的跟传说中的一样. ...

  7. Java开发常用的在线工具

    原文出处: hollischuang(@Hollis_Chuang) 作为一个Java开发人员,经常要和各种各样的工具打交道,除了我们常用的IDE工具以外,其实还有很多工具是我们在日常开发及学习过程中 ...

  8. [开发工具]Java开发常用的在线工具

    注明: 本文转自http://www.hollischuang.com/archives/1459.作为一个Java开发人员,经常要和各种各样的工具打交道,除了我们常用的IDE工具以外,其实还有很多工 ...

  9. 基于Java的简易表达式解析工具(二)

    之前简单的介绍了这个基于Java表达式解析工具,现在把代码分享给大家,希望帮助到有需要的人们,这个分享代码中依赖了一些其他的类,这些类大家可以根据自己的情况进行导入,无非就是写字符串处理工具类,日期处 ...

随机推荐

  1. 【HTML】dl dt dd

    摘要 看到没怎么使用过的html 标签,记录下 定义 dl 类似于 ul ,无任何样式,自定义列表容器, ul 为无序列表容器,ol 为有序列表容器 dt dd 类似于 li ,无任何样式,为帮助实现 ...

  2. centos6.8 静默安装 oracle 11.2.0.4

    安装环境及系统要求    (下文具体参数值与路径根据自己的环境调整)操作系统:Red Hat Enterprise Linux 6 (x86) 或者CentOS 6 (x64) 数据库:Oracle ...

  3. year:2017 month:7 day:27

    2017-07-27 JAVA 1:java分为三类:javase(桌面开发应用) javaee(企业级开发应用) javame(手机嵌入式开发应用) 2:jdk(java开发工具包),jre(jav ...

  4. 42. leetcode 70. Climbing Stairs

    70. Climbing Stairs You are climbing a stair case. It takes n steps to reach to the top. Each time y ...

  5. Selenium+java操作浏览器cookies

    描述:登录CSDN,将登录信息cookies保存到文件,再次打开网页时,直接利用文件中的数据登录. 1. 获取cookies并保存到文件 步骤: ① 打开CSDN的登录界面: ② 填写用户名和密码: ...

  6. swift3.0 屏幕截图并且保存到本地相册

    所要截取的对象 var bg_view: UIView! 截取并且保存的代码如下 UIGraphicsBeginImageContextWithOptions(bg_view.frame.size, ...

  7. .net操作IIS,新建网站,新建应用程序池,设置应用程序池版本,设置网站和应用程序池的关联

    ServerManager类用来操作IIS,提供了很多操作IIS的API.使用ServerManager必须引用Microsoft.Web.Administration.dll,具体路径为:%wind ...

  8. Bat脚本自动卸载软件-静默执行

    通过Bat脚本卸载软件,原理是得到某软件的ProductCode,然后通过MsiExec.exe命令卸载软件,下面是卸载一个产品的基本代码示例: set ML4.0HF4Name=Product4.0 ...

  9. 今天出现了一个问题,Tomcat 进入localhost:8080正常,进入项目内别的页面都是空白页

    经仔细检查发现代码没有任何的问题,经仔细检查找到了原因. 问题原因:拦截器(过滤器)把我的访问请求全都拦下了,我在拦截器里把//chain.doFilter(request, response);这行 ...

  10. 53. Maximum Subarray【leetcode】

    53. Maximum Subarray[leetcode] Find the contiguous subarray within an array (containing at least one ...