最近用迅雷下载了很多文件,很多都是点默认内容直接下载的。屯了很多垃圾广告,一想到有这么多重复文件在里面,就感觉不舒服。
于是乎,直接整了个去重的程序:
程序逻辑是,先比较文件大小,如果文件大小相同,再比较md5和Hash值,如果都相同,就认为是重复文件。当然md5和Hash都是弱相关,有极小概率出错(大概16的72次方分之一),但为了我宝贵的资源,我还是不直接删除重复文件,而是把文件放到指定目录下,让我人工删除。
软件里做了日志,方便查验。
实测1.61TB视频,用时10分钟。16000张图片,用时30秒。
第一个文本框是路径,会遍历路径下的所有子文件夹。第二个框是文件类型,输入空则不限定类型,可限定多种类型。
整个软件工程:
链接:/s/1HiaSbglK48ri_QWwTNFfWA?pwd=cnmd
提取码:cnmd
完整程序:
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace 文件去重
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
textBox1.Text = AppDomain.CurrentDomain.BaseDirectory;
}
class Features
{
public long size { get; set; }
public string md5 { get; set; }
public string Hash { get; set; }
public string path { get; set; }
}
private void button1_Click(object sender, EventArgs e)
{
try
{
button1.Enabled = false;
button1.Text = "进行中";
string FilePath = textBox1.Text;
string Format = textBox2.Text;
log("开始:" + FilePath + " 格式:" + Format);
string RepeatFilePath = AppDomain.CurrentDomain.BaseDirectory + @"RepeatFilePath\" + DateTime.Now.ToString("HH-mm-ss");
if (!Directory.Exists(RepeatFilePath))
{
Directory.CreateDirectory(RepeatFilePath);
}
FileGet.getFile(FilePath, Format);
List features = new List();
//进度条
progressBar1.Value = 0;
progressBar1.Maximum = FileGet.lst.Count;
label1.Text = progressBar1.Value + "/" + FileGet.lst.Count;
log("共扫描到" + FileGet.lst.Count + "个文件");
int RepeatNum = 0;
int NoRepeatNum = 0;
int ErrorNum = 0;
Task.Factory.StartNew(() => {
foreach (FileInfo file in FileGet.lst)
{
try
{
string path = file.DirectoryName + "\\" + file.Name;
var index = features.FindAll(o => o.size == file.Length);//先比较文件大小
if (index.Count == 0)
{
Features features1 = new Features();
features1.size = file.Length;
features1.path = path;
features.Add(features1);
NoRepeatNum++;
}
else//如果文件大小相同,再计算哈希
{
//当前文件的md5和hash
string md5 = GetMD5HashFromFile(path);
string Hash = GetHash(path);
//log(path + " md5:" + md5);
//log(path + " Hash:" + Hash);
bool BeRepeat = false;
foreach (var RepeatPossible in index)
{
if (RepeatPossible.md5 == null)
{
RepeatPossible.md5 = GetMD5HashFromFile(RepeatPossible.path);
}
if (RepeatPossible.Hash == null)
{
RepeatPossible.Hash = GetHash(RepeatPossible.path);
}
if (md5 == RepeatPossible.md5 && Hash == RepeatPossible.Hash)
{
//确定为同一文件
string oldpath = file.DirectoryName + "\\" + file.Name;
log(oldpath + " 与该文件相同: " + RepeatPossible.path);
int i = 0;//重复文件重命名编号
//当前文件夹下文件列表
DirectoryInfo fdir = new DirectoryInfo(RepeatFilePath);
FileInfo[] CurrentFile = fdir.GetFiles();
List names = new List();
foreach (FileInfo currentfile in CurrentFile)
{
names.Add(currentfile.Name);
}
if (names.FindIndex(o => o == file.Name) != -1)
{
//已有重名文件
i = 0;
while (names.FindIndex(o => o == "(" + i + ")" + file.Name) != -1)
{
i++;
}
file.MoveTo(RepeatFilePath + "\\(" + i + ")" + file.Name);
log(oldpath + " 已移动到: " + file.DirectoryName + "\\" + file.Name);
}
else
{
file.MoveTo(RepeatFilePath + "\\" + file.Name);
log(oldpath + " 已移动到: " + file.DirectoryName + "\\" + file.Name);
}
RepeatNum++;
BeRepeat = true;
break;
}
}
if(!BeRepeat)
{
NoRepeatNum++;
}
}
progressBar1.Value++;
label1.Text = progressBar1.Value + "/" + FileGet.lst.Count + " 重复文件:" + RepeatNum + " 不重复文件:" + NoRepeatNum + " 错误文件:" + ErrorNum;
}
catch (Exception ex)
{
progressBar1.Value++;
ErrorNum++;
log("错误:" + ex);
}
}
log("结束");
button1.Text = "开始";
DeleteNullFile(AppDomain.CurrentDomain.BaseDirectory + @"RepeatFilePath\");
});
}
catch (Exception ex)
{
log("button1错误:" + ex.Message);
button1.Text = "开始";
MessageBox.Show(ex.Message, "错误");
}
button1.Enabled = true;
}
public static bool isValidFileContent(string filePath1, string filePath2)
{
//创建一个哈希算法对象
using (HashAlgorithm hash = HashAlgorithm.Create())
{
using (FileStream file1 = new FileStream(filePath1, FileMode.Open), file2 = new FileStream(filePath2, FileMode.Open))
{
byte[] hashByte1 = hash.ComputeHash(file1);//哈希算法根据文本得到哈希码的字节数组
byte[] hashByte2 = hash.ComputeHash(file2);
string str1 = BitConverter.ToString(hashByte1);//将字节数组装换为字符串
string str2 = BitConverter.ToString(hashByte2);
return (str1 == str2);//比较哈希码
}
}
}
public string GetHash(string filePath)
{
//创建一个哈希算法对象
using (HashAlgorithm hash = HashAlgorithm.Create())
{
using (FileStream file = new FileStream(filePath, FileMode.Open))
{
byte[] hashByte = hash.ComputeHash(file);//哈希算法根据文本得到哈希码的字节数组
string str = BitConverter.ToString(hashByte);//将字节数组装换为字符串
return str;//返回哈希码
}
}
}
///
/// 获取md5值
///
/// 文件路径
///
public string GetMD5HashFromFile(string fileName)
{
try
{
FileStream file = new FileStream(fileName, System.IO.FileMode.Open);
MD5 md5 = new MD5CryptoServiceProvider();
byte[] retVal = md5.ComputeHash(file);
file.Close();
StringBuilder sb = new StringBuilder();
for (int i = 0; i < retVal.Length; i++)
{
sb.Append(retVal[i].ToString("x2"));
}
return sb.ToString();
}
catch (Exception ex)
{
throw new Exception("GetMD5HashFromFile() fail,error:" + ex.Message);
}
}
///
/// 日志
///
/// 日志内容
///
public static void log(string content)
{
string path = AppDomain.CurrentDomain.BaseDirectory + "log";
if (!Directory.Exists(path))
{
Directory.CreateDirectory(path);
}
path = path + "\\" + DateTime.Now.ToString("yyyyMMdd") + ".txt";
if (!File.Exists(path))
{
FileStream fs = File.Create(path);
fs.Close();
}
if (File.Exists(path))
{
StreamWriter sw = new StreamWriter(path, true, System.Text.Encoding.Default);
sw.WriteLine(DateTime.Now.ToString("HH:mm:ss.fff ") + content);
sw.Close();
}
}
public partial class FileGet
{
public static List lst = new List();
///
/// 获得目录下所有文件或指定文件类型文件(包含所有子文件夹)
///
/// 文件夹路径
/// 扩展名可以多个 例如 .mp3.wma.rm
/// List
public static void getFile(string path, string extName)
{
lst.Clear();
getdir(path, extName);
}
///
/// 递归获取指定类型文件,包含子文件夹
///
/// 文件夹路径
/// 扩展名可以多个 例如 .mp3.wma.rm
private static void getdir(string path, string extName)
{
try
{
string[] dir = Directory.GetDirectories(path); //文件夹列表
DirectoryInfo fdir = new DirectoryInfo(path);
FileInfo[] file = fdir.GetFiles();
//FileInfo[] file = Directory.GetFiles(path); //文件列表
if (file.Length != 0 || dir.Length != 0) //当前目录文件或文件夹不为空
{
foreach (FileInfo f in file) //显示当前目录所有文件
{
if (string.IsNullOrEmpty(extName))
{
lst.Add(f);
}
else if (extName.ToLower().IndexOf(f.Extension.ToLower()) >= 0)
{
lst.Add(f);
}
}
foreach (string d in dir)
{
getdir(d, extName);//递归
}
}
}
catch (Exception ex)
{
log("getdir错误:" + ex.Message);
throw ex;
}
}
}
///
/// 删除空文件夹
///
///
private static void DeleteNullFile(string path)
{
try
{
string[] dirs = Directory.GetDirectories(path); //文件夹列表
//为了先删除子目录再删除父目录,数组先排序再倒序,让子目录排在父目录前面
Array.Sort(dirs);
Array.Reverse(dirs);
foreach (var dir in dirs)
{
var info = new DirectoryInfo(dir);
//检查是否包含子文件夹及文件
if (info.GetFileSystemInfos().Length == 0)
{
//由于子文件夹或文件随时会增加,不强制删除子文件及子文件夹
info.Delete();
}
}
}
catch (Exception ex)
{
log("DeleteNullFile错误:" + ex.Message);
MessageBox.Show(ex.Message, "错误");
throw ex;
}
}
public partial class FileGet1
{
///
/// 获得目录下所有文件或指定文件类型文件(包含所有子文件夹)
///
/// 文件夹路径
/// 扩展名可以多个 例如 .mp3.wma.rm
/// List
public static List getFile(string path, string extName)
{
try
{
List lst = new List();
string[] dir = Directory.GetDirectories(path); //文件夹列表
DirectoryInfo fdir = new DirectoryInfo(path);
FileInfo[] file = fdir.GetFiles();
//FileInfo[] file = Directory.GetFiles(path); //文件列表
if (file.Length != 0 || dir.Length != 0) //当前目录文件或文件夹不为空
{
foreach (FileInfo f in file) //显示当前目录所有文件
{
if (extName.ToLower().IndexOf(f.Extension.ToLower()) >= 0)
{
lst.Add(f);
}
}
foreach (string d in dir)
{
getFile(d, extName);//递归
}
}
return lst;
}
catch (Exception ex)
{
log(ex.Message);
throw ex;
}
}
}
}
}
在编辑程序时,遇到过两个问题,第一个是移动文件时,要处理重名文件,一开始我直接用个while循环,File.Exists判断是否有重名文件,但似乎文件列表更新是另外一个线程,更新不同步。只会一直循环报错(错误文件数就是为了检测这个问题设立)。最后自己另外写个list,先把文件列表保存下来,再从列表下把文件去重,
第二个问题是文件去重比较,一开始我是同时比较大小、md5、Hash,但这样速度非常慢,因为所有文件都会读取到内存里计算md5、Hash。所以先比较大小,如果大小相同,再计算md5和Hash(因为大小相同很可能是相同文件,这时候同时计算md5和Hash),果然,去重速度大大提高了。