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. 用java写一个用户登陆界面

    一.课堂测试源代码及其结果截图 用java的swing写一个用户登录界面,采用网格布局.源代码如下: /** * */package LiuLijia; import java.awt.CardLay ...

  2. OVS故障处理一例

    OVS下无法访问内部网站 遇到朋友求助的一个客户问题,环境是这样的,客户在自己的iaas平台(不是openstack)上使用ovs,物理交换机上配置vlan和dhcp service,计算节点的ovs ...

  3. storm从入门到放弃(二),任务分配过程-核心机密

    背景:目前就职于国内最大的IT咨询公司,恰巧又是毕业季,所在部门招了100多个应届毕业生,本人要跟部门新人进行为期一个月的大数据入职培训,特此将整理的文档分享出来. 原文和作者一起讨论:http:// ...

  4. Android中的广播

    Android中的广播 广播接受器,可以比喻成收音机.而广播则可以看成电台. Android系统内部相当于已经有一个电台 定义了好多的广播事件,比如外拨电话 短信到来 sd卡状态 电池电量变化... ...

  5. Java基础之接口与抽象类及多态、内部类

    final关键字 被其修饰的类,不能被继承. 被其修饰的方法,不能被覆盖. 被其修饰的变量,是一个常量,不能被修改,所以定义时必须初始化(和C++的const类似). 一般有final,会搭配stat ...

  6. vsftp虚拟主机

    ################################Vsftp服务器实战##########################################3 文件传输协议,基于该协议FT ...

  7. hdu--3782--找规律--xxx定律

    /* Name: hdu--3782--xxx定律 Date: 17/04/17 21:34 Description: 找规律题,又想打表了 */ /* for(int i=2;i<30;++i ...

  8. 常用类:String,StringBuffer,StringBuilder

    String String类被final修饰符修饰,所以不能将其进行继承,所有对它的改变都要重新创建一个新的地址 1.String的构造器 String() String(byte []bytes)/ ...

  9. cve-2017-8464 复现 快捷方式远程代码执行

    cve-2017-8464 2017年6月13日,微软官方发布编号为CVE-2017-8464的漏洞公告,官方介绍Windows系统在解析快捷方式时存在远程执行任意代码的高危漏洞,黑客可以通过U盘.网 ...

  10. Python使用PDFMiner解析PDF

    近期在做爬虫时有时会遇到网站只提供pdf的情况,这样就不能使用scrapy直接抓取页面内容了,只能通过解析PDF的方式处理,目前的解决方案大致只有pyPDF和PDFMiner.因为据说PDFMiner ...