Approches typiques recommandons de lire le binaire via FileStream et de le comparer octet par octet.
Une comparaison de somme de contrôle sera probablement plus lente qu'une comparaison octet par octet.
Pour générer une somme de contrôle, vous devez charger chaque octet du fichier et effectuer le traitement correspondant. Vous devrez ensuite le faire sur le deuxième fichier. Le traitement sera presque certainement plus lent que le contrôle de comparaison.
Pour ce qui est de générer une somme de contrôle: Vous pouvez le faire facilement avec les classes de cryptographie. Voici un exemple short de génération d'une somme de contrôle MD5 avec C #.
Cependant, une somme de contrôle peut être plus rapide et plus logique si vous pouvez pré-calculer la somme de contrôle du cas "test" ou "base". Si vous avez un fichier existant et que vous vérifiez si un nouveau fichier est identique au fichier existant, pré-calculer la somme de contrôle sur votre fichier "existant" signifierait que vous n'avez besoin de faire le DiskIO qu'une seule fois, à la fois. nouveau fichier. Cela serait probablement plus rapide qu'une comparaison octet par octet.
La méthode la plus lente possible consiste à comparer deux fichiers octet par octet. Le plus rapide que j'ai pu arriver est une comparaison similaire, mais au lieu d'un octet à la fois, vous utiliseriez un tableau d'octets de la taille Int64, puis compariez les résultats.
Voici ce que je suis venu avec:
const int BYTES_TO_READ = sizeof(Int64);
static bool FilesAreEqual(FileInfo first, FileInfo second)
{
if (first.Length != second.Length)
return false;
if (string.Equals(first.FullName, second.FullName, StringComparison.OrdinalIgnoreCase))
return true;
int iterations = (int)Math.Ceiling((double)first.Length / BYTES_TO_READ);
using (FileStream fs1 = first.OpenRead())
using (FileStream fs2 = second.OpenRead())
{
byte[] one = new byte[BYTES_TO_READ];
byte[] two = new byte[BYTES_TO_READ];
for (int i = 0; i < iterations; i++)
{
fs1.Read(one, 0, BYTES_TO_READ);
fs2.Read(two, 0, BYTES_TO_READ);
if (BitConverter.ToInt64(one,0) != BitConverter.ToInt64(two,0))
return false;
}
}
return true;
}
Lors de mes tests, j'ai pu constater que ce scénario surpasse de loin un scénario simple ReadByte () de presque 3: 1. En moyenne sur 1 000 exécutions, j’ai eu cette méthode à 1063ms et la méthode ci-dessous (comparaison simple octet par octet) à 3031ms. Le hachage est toujours revenu en moins d'une seconde, à environ 865 ms en moyenne. Ce test a été effectué avec un fichier vidéo d’environ 100 Mo.
Voici les méthodes ReadByte et de hachage que j'ai utilisées à des fins de comparaison:
static bool FilesAreEqual_OneByte(FileInfo first, FileInfo second)
{
if (first.Length != second.Length)
return false;
if (string.Equals(first.FullName, second.FullName, StringComparison.OrdinalIgnoreCase))
return true;
using (FileStream fs1 = first.OpenRead())
using (FileStream fs2 = second.OpenRead())
{
for (int i = 0; i < first.Length; i++)
{
if (fs1.ReadByte() != fs2.ReadByte())
return false;
}
}
return true;
}
static bool FilesAreEqual_Hash(FileInfo first, FileInfo second)
{
byte[] firstHash = MD5.Create().ComputeHash(first.OpenRead());
byte[] secondHash = MD5.Create().ComputeHash(second.OpenRead());
for (int i=0; i<firstHash.Length; i++)
{
if (firstHash[i] != secondHash[i])
return false;
}
return true;
}
En plus de la réponse de Reed Copsey:
Le pire des cas est où les deux fichiers sont identiques. Dans ce cas, il est préférable de comparer les fichiers octet par octet.
Si les deux fichiers ne sont pas identiques, vous pouvez accélérer le processus en détectant plus tôt qu’ils ne sont pas identiques.
Par exemple, si les deux fichiers ont une longueur différente, vous savez qu'ils ne peuvent pas être identiques et vous n'avez même pas à comparer leur contenu réel.
Cela devient encore plus rapide si vous ne lisez pas de petits morceaux de 8 octets, mais faites une boucle, en lisant un plus gros morceau. J'ai réduit le temps moyen de comparaison à 1/4.
public static bool FilesContentsAreEqual(FileInfo fileInfo1, FileInfo fileInfo2)
{
bool result;
if (fileInfo1.Length != fileInfo2.Length)
{
result = false;
}
else
{
using (var file1 = fileInfo1.OpenRead())
{
using (var file2 = fileInfo2.OpenRead())
{
result = StreamsContentsAreEqual(file1, file2);
}
}
}
return result;
}
private static bool StreamsContentsAreEqual(Stream stream1, Stream stream2)
{
const int bufferSize = 1024 * sizeof(Int64);
var buffer1 = new byte[bufferSize];
var buffer2 = new byte[bufferSize];
while (true)
{
int count1 = stream1.Read(buffer1, 0, bufferSize);
int count2 = stream2.Read(buffer2, 0, bufferSize);
if (count1 != count2)
{
return false;
}
if (count1 == 0)
{
return true;
}
int iterations = (int)Math.Ceiling((double)count1 / sizeof(Int64));
for (int i = 0; i < iterations; i++)
{
if (BitConverter.ToInt64(buffer1, i * sizeof(Int64)) != BitConverter.ToInt64(buffer2, i * sizeof(Int64)))
{
return false;
}
}
}
}
}
La seule chose qui peut rendre une comparaison de somme de contrôle légèrement plus rapide qu'une comparaison octet par octet est le fait que vous lisez un fichier à la fois, ce qui réduit légèrement le temps de recherche de la tête de disque. Ce léger gain peut cependant très bien être consommé par le temps supplémentaire de calcul du hachage.
En outre, une comparaison de somme de contrôle n'a bien entendu aucune chance d'être plus rapide si les fichiers sont identiques. S'ils ne le sont pas, une comparaison octet par octet aboutirait à la première différence, ce qui le rendrait beaucoup plus rapide.
Vous devriez également considérer qu'une comparaison de code de hachage vous indique seulement qu'il est très probablement que les fichiers sont identiques. Pour être certain à 100%, vous devez faire une comparaison octet par octet.
Si le code de hachage est par exemple 32 bits, vous êtes à peu près certain à 99,99999998 que les fichiers sont identiques si les codes de hachage correspondent. C'est presque 100%, mais si vous avez vraiment besoin de 100% de certitude, ce n'est pas ça.
Edit: Cette méthode ne fonctionnerait pas pour pour comparer des fichiers binaires!
Dans .NET 4.0, la classe File
utilise les deux nouvelles méthodes suivantes:
public static IEnumerable<string> ReadLines(string path)
public static IEnumerable<string> ReadLines(string path, Encoding encoding)
Ce qui signifie que vous pourriez utiliser:
bool same = File.ReadLines(path1).SequenceEqual(File.ReadLines(path2));
Honnêtement, je pense que vous devez tailler votre arbre de recherche le plus bas possible.
Choses à vérifier avant d'aller octet par octet:
En outre, la lecture de grands blocs à la fois sera plus efficace, car les lecteurs lisent plus rapidement les octets séquentiels. Passer octet par octet provoque non seulement beaucoup plus d'appels système, mais oblige également la tête de lecture d'un disque dur à rechercher plus souvent si les deux fichiers se trouvent sur le même disque.
Lisez le fragment A et le fragment B dans un tampon d'octets et comparez-les (n'utilisez PAS Array.Equals, voir les commentaires). Réglez la taille des blocs jusqu'à obtenir ce que vous estimez être un bon compromis entre mémoire et performance. Vous pouvez également utiliser la comparaison sur plusieurs threads, mais ne lisez pas le disque à la lecture.
Ma réponse est un dérivé de @lars mais corrige le bogue dans l'appel à Stream.Read
. J'ajoute également des éléments de vérification rapide d'autres réponses et une validation des entrées. En bref, cela devrait être le réponse:
using System;
using System.IO;
namespace ConsoleApp4
{
class Program
{
static void Main(string[] args)
{
var fi1 = new FileInfo(args[0]);
var fi2 = new FileInfo(args[1]);
Console.WriteLine(FilesContentsAreEqual(fi1, fi2));
}
public static bool FilesContentsAreEqual(FileInfo fileInfo1, FileInfo fileInfo2)
{
if (fileInfo1 == null)
{
throw new ArgumentNullException(nameof(fileInfo1));
}
if (fileInfo2 == null)
{
throw new ArgumentNullException(nameof(fileInfo2));
}
if (string.Equals(fileInfo1.FullName, fileInfo2.FullName, StringComparison.OrdinalIgnoreCase))
{
return true;
}
if (fileInfo1.Length != fileInfo2.Length)
{
return false;
}
else
{
using (var file1 = fileInfo1.OpenRead())
{
using (var file2 = fileInfo2.OpenRead())
{
return StreamsContentsAreEqual(file1, file2);
}
}
}
}
private static int ReadFullBuffer(Stream stream, byte[] buffer)
{
int bytesRead = 0;
while (bytesRead < buffer.Length)
{
int read = stream.Read(buffer, bytesRead, buffer.Length - bytesRead);
if (read == 0)
{
// Reached end of stream.
return bytesRead;
}
bytesRead += read;
}
return bytesRead;
}
private static bool StreamsContentsAreEqual(Stream stream1, Stream stream2)
{
const int bufferSize = 1024 * sizeof(Int64);
var buffer1 = new byte[bufferSize];
var buffer2 = new byte[bufferSize];
while (true)
{
int count1 = ReadFullBuffer(stream1, buffer1);
int count2 = ReadFullBuffer(stream2, buffer2);
if (count1 != count2)
{
return false;
}
if (count1 == 0)
{
return true;
}
int iterations = (int)Math.Ceiling((double)count1 / sizeof(Int64));
for (int i = 0; i < iterations; i++)
{
if (BitConverter.ToInt64(buffer1, i * sizeof(Int64)) != BitConverter.ToInt64(buffer2, i * sizeof(Int64)))
{
return false;
}
}
}
}
}
}
Ou si vous voulez être super génial, vous pouvez utiliser la variante asynchrone:
using System;
using System.IO;
using System.Threading.Tasks;
namespace ConsoleApp4
{
class Program
{
static void Main(string[] args)
{
var fi1 = new FileInfo(args[0]);
var fi2 = new FileInfo(args[1]);
Console.WriteLine(FilesContentsAreEqualAsync(fi1, fi2).GetAwaiter().GetResult());
}
public static async Task<bool> FilesContentsAreEqualAsync(FileInfo fileInfo1, FileInfo fileInfo2)
{
if (fileInfo1 == null)
{
throw new ArgumentNullException(nameof(fileInfo1));
}
if (fileInfo2 == null)
{
throw new ArgumentNullException(nameof(fileInfo2));
}
if (string.Equals(fileInfo1.FullName, fileInfo2.FullName, StringComparison.OrdinalIgnoreCase))
{
return true;
}
if (fileInfo1.Length != fileInfo2.Length)
{
return false;
}
else
{
using (var file1 = fileInfo1.OpenRead())
{
using (var file2 = fileInfo2.OpenRead())
{
return await StreamsContentsAreEqualAsync(file1, file2).ConfigureAwait(false);
}
}
}
}
private static async Task<int> ReadFullBufferAsync(Stream stream, byte[] buffer)
{
int bytesRead = 0;
while (bytesRead < buffer.Length)
{
int read = await stream.ReadAsync(buffer, bytesRead, buffer.Length - bytesRead).ConfigureAwait(false);
if (read == 0)
{
// Reached end of stream.
return bytesRead;
}
bytesRead += read;
}
return bytesRead;
}
private static async Task<bool> StreamsContentsAreEqualAsync(Stream stream1, Stream stream2)
{
const int bufferSize = 1024 * sizeof(Int64);
var buffer1 = new byte[bufferSize];
var buffer2 = new byte[bufferSize];
while (true)
{
int count1 = await ReadFullBufferAsync(stream1, buffer1).ConfigureAwait(false);
int count2 = await ReadFullBufferAsync(stream2, buffer2).ConfigureAwait(false);
if (count1 != count2)
{
return false;
}
if (count1 == 0)
{
return true;
}
int iterations = (int)Math.Ceiling((double)count1 / sizeof(Int64));
for (int i = 0; i < iterations; i++)
{
if (BitConverter.ToInt64(buffer1, i * sizeof(Int64)) != BitConverter.ToInt64(buffer2, i * sizeof(Int64)))
{
return false;
}
}
}
}
}
}
Mes expériences montrent qu'il est certainement utile d'appeler Stream.ReadByte () moins de fois, mais utiliser BitConverter pour conditionner les octets ne fait pas beaucoup de différence par rapport à la comparaison d'octets dans un tableau d'octets.
Il est donc possible de remplacer cette boucle "Math.Ceiling and iterations" dans le commentaire ci-dessus par la plus simple:
for (int i = 0; i < count1; i++)
{
if (buffer1[i] != buffer2[i])
return false;
}
Je suppose que cela a à voir avec le fait que BitConverter.ToInt64 doit faire un peu de travail (vérifier les arguments, puis effectuer le transfert de bits) avant de comparer et que le travail soit identique, comparant 8 octets dans deux tableaux .
Si vous avez seulement besoin de comparer deux fichiers, je suppose que le moyen le plus rapide serait (en C, je ne sais pas si cela s’applique à .NET)
OTOH, si vous devez rechercher des fichiers en double dans un ensemble de N fichiers, le moyen le plus rapide consiste sans aucun doute à utiliser un hachage pour éviter les comparaisons bit par bit N-way.
Une autre amélioration sur des fichiers volumineux de longueur identique pourrait consister à ne pas lire les fichiers séquentiellement, mais plutôt à comparer des blocs plus ou moins aléatoires.
Vous pouvez utiliser plusieurs threads, en commençant par différentes positions du fichier et en comparant en avant ou en arrière.
De cette façon, vous pouvez détecter les modifications au milieu/à la fin du fichier, plus rapidement que vous ne le feriez avec une approche séquentielle.
Voici quelques fonctions utilitaires vous permettant de déterminer si deux fichiers (ou deux flux) contiennent des données identiques.
J'ai fourni une version "rapide" multi-thread car elle compare les tableaux d'octets (chaque tampon rempli de ce qui a été lu dans chaque fichier) dans différents threads à l'aide de Tasks.
Comme prévu, il est beaucoup plus rapide (environ 3 fois plus rapide), mais consomme plus de processeur (car il est multi-threadé) et plus de mémoire (car il nécessite des tampons de tableau de deux octets par thread de comparaison).
public static bool AreFilesIdenticalFast(string path1, string path2)
{
return AreFilesIdentical(path1, path2, AreStreamsIdenticalFast);
}
public static bool AreFilesIdentical(string path1, string path2)
{
return AreFilesIdentical(path1, path2, AreStreamsIdentical);
}
public static bool AreFilesIdentical(string path1, string path2, Func<Stream, Stream, bool> areStreamsIdentical)
{
if (path1 == null)
throw new ArgumentNullException(nameof(path1));
if (path2 == null)
throw new ArgumentNullException(nameof(path2));
if (areStreamsIdentical == null)
throw new ArgumentNullException(nameof(path2));
if (!File.Exists(path1) || !File.Exists(path2))
return false;
using (var thisFile = new FileStream(path1, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
{
using (var valueFile = new FileStream(path2, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
{
if (valueFile.Length != thisFile.Length)
return false;
if (!areStreamsIdentical(thisFile, valueFile))
return false;
}
}
return true;
}
public static bool AreStreamsIdenticalFast(Stream stream1, Stream stream2)
{
if (stream1 == null)
throw new ArgumentNullException(nameof(stream1));
if (stream2 == null)
throw new ArgumentNullException(nameof(stream2));
const int bufsize = 80000; // 80000 is below LOH (85000)
var tasks = new List<Task<bool>>();
do
{
// consumes more memory (two buffers for each tasks)
var buffer1 = new byte[bufsize];
var buffer2 = new byte[bufsize];
int read1 = stream1.Read(buffer1, 0, buffer1.Length);
if (read1 == 0)
{
int read3 = stream2.Read(buffer2, 0, 1);
if (read3 != 0) // not eof
return false;
break;
}
// both stream read could return different counts
int read2 = 0;
do
{
int read3 = stream2.Read(buffer2, read2, read1 - read2);
if (read3 == 0)
return false;
read2 += read3;
}
while (read2 < read1);
// consumes more cpu
var task = Task.Run(() =>
{
return IsSame(buffer1, buffer2);
});
tasks.Add(task);
}
while (true);
Task.WaitAll(tasks.ToArray());
return !tasks.Any(t => !t.Result);
}
public static bool AreStreamsIdentical(Stream stream1, Stream stream2)
{
if (stream1 == null)
throw new ArgumentNullException(nameof(stream1));
if (stream2 == null)
throw new ArgumentNullException(nameof(stream2));
const int bufsize = 80000; // 80000 is below LOH (85000)
var buffer1 = new byte[bufsize];
var buffer2 = new byte[bufsize];
var tasks = new List<Task<bool>>();
do
{
int read1 = stream1.Read(buffer1, 0, buffer1.Length);
if (read1 == 0)
return stream2.Read(buffer2, 0, 1) == 0; // check not eof
// both stream read could return different counts
int read2 = 0;
do
{
int read3 = stream2.Read(buffer2, read2, read1 - read2);
if (read3 == 0)
return false;
read2 += read3;
}
while (read2 < read1);
if (!IsSame(buffer1, buffer2))
return false;
}
while (true);
}
public static bool IsSame(byte[] bytes1, byte[] bytes2)
{
if (bytes1 == null)
throw new ArgumentNullException(nameof(bytes1));
if (bytes2 == null)
throw new ArgumentNullException(nameof(bytes2));
if (bytes1.Length != bytes2.Length)
return false;
for (int i = 0; i < bytes1.Length; i++)
{
if (bytes1[i] != bytes2[i])
return false;
}
return true;
}
Quelque chose (espérons-le) raisonnablement efficace:
public class FileCompare
{
public static bool FilesEqual(string fileName1, string fileName2)
{
return FilesEqual(new FileInfo(fileName1), new FileInfo(fileName2));
}
/// <summary>
///
/// </summary>
/// <param name="file1"></param>
/// <param name="file2"></param>
/// <param name="bufferSize">8kb seemed like a good default</param>
/// <returns></returns>
public static bool FilesEqual(FileInfo file1, FileInfo file2, int bufferSize = 8192)
{
if (!file1.Exists || !file2.Exists || file1.Length != file2.Length) return false;
var buffer1 = new byte[bufferSize];
var buffer2 = new byte[bufferSize];
using (var stream1 = file1.Open(FileMode.Open, FileAccess.Read, FileShare.Read))
{
using (var stream2 = file2.Open(FileMode.Open, FileAccess.Read, FileShare.Read))
{
while (true)
{
var bytesRead1 = stream1.Read(buffer1, 0, bufferSize);
var bytesRead2 = stream2.Read(buffer2, 0, bufferSize);
if (bytesRead1 != bytesRead2) return false;
if (bytesRead1 == 0) return true;
if (!ArraysEqual(buffer1, buffer2, bytesRead1)) return false;
}
}
}
}
/// <summary>
///
/// </summary>
/// <param name="array1"></param>
/// <param name="array2"></param>
/// <param name="bytesToCompare"> 0 means compare entire arrays</param>
/// <returns></returns>
public static bool ArraysEqual(byte[] array1, byte[] array2, int bytesToCompare = 0)
{
if (array1.Length != array2.Length) return false;
var length = (bytesToCompare == 0) ? array1.Length : bytesToCompare;
var tailIdx = length - length % sizeof(Int64);
//check in 8 byte chunks
for (var i = 0; i < tailIdx; i += sizeof(Int64))
{
if (BitConverter.ToInt64(array1, i) != BitConverter.ToInt64(array2, i)) return false;
}
//check the remainder of the array, always shorter than 8 bytes
for (var i = tailIdx; i < length; i++)
{
if (array1[i] != array2[i]) return false;
}
return true;
}
}
Si les fichiers ne sont pas trop gros, vous pouvez utiliser:
public static byte[] ComputeFileHash(string fileName)
{
using (var stream = File.OpenRead(fileName))
return System.Security.Cryptography.MD5.Create().ComputeHash(stream);
}
La comparaison des hachages ne sera possible que si ceux-ci sont utiles à stocker.
(Edité le code pour quelque chose de beaucoup plus propre.)
Je pense qu'il existe des applications dans lesquelles le "hachage" est plus rapide que la comparaison octet par octet . Si vous avez besoin de comparer un fichier avec d'autres ou d'avoir une miniature d'une photo qui peut changer . Cela dépend où et comment il utilise.
private bool CompareFilesByte(string file1, string file2)
{
using (var fs1 = new FileStream(file1, FileMode.Open))
using (var fs2 = new FileStream(file2, FileMode.Open))
{
if (fs1.Length != fs2.Length) return false;
int b1, b2;
do
{
b1 = fs1.ReadByte();
b2 = fs2.ReadByte();
if (b1 != b2 || b1 < 0) return false;
}
while (b1 >= 0);
}
return true;
}
private string HashFile(string file)
{
using (var fs = new FileStream(file, FileMode.Open))
using (var reader = new BinaryReader(fs))
{
var hash = new SHA512CryptoServiceProvider();
hash.ComputeHash(reader.ReadBytes((int)file.Length));
return Convert.ToBase64String(hash.Hash);
}
}
private bool CompareFilesWithHash(string file1, string file2)
{
var str1 = HashFile(file1);
var str2 = HashFile(file2);
return str1 == str2;
}
Ici, vous pouvez obtenir ce qui est le plus rapide.
var sw = new Stopwatch();
sw.Start();
var compare1 = CompareFilesWithHash(receiveLogPath, logPath);
sw.Stop();
Debug.WriteLine(string.Format("Compare using Hash {0}", sw.ElapsedTicks));
sw.Reset();
sw.Start();
var compare2 = CompareFilesByte(receiveLogPath, logPath);
sw.Stop();
Debug.WriteLine(string.Format("Compare byte-byte {0}", sw.ElapsedTicks));
Facultativement, nous pouvons enregistrer le hachage dans une base de données.
J'espère que cela peut aider
Encore une autre réponse, dérivée de @chsh. MD5 avec utilisations et raccourcis pour le même fichier, le fichier n'existe pas et sa longueur
/// <summary>
/// Performs an md5 on the content of both files and returns true if
/// they match
/// </summary>
/// <param name="file1">first file</param>
/// <param name="file2">second file</param>
/// <returns>true if the contents of the two files is the same, false otherwise</returns>
public static bool IsSameContent(string file1, string file2)
{
if (file1 == file2)
return true;
FileInfo file1Info = new FileInfo(file1);
FileInfo file2Info = new FileInfo(file2);
if (!file1Info.Exists && !file2Info.Exists)
return true;
if (!file1Info.Exists && file2Info.Exists)
return false;
if (file1Info.Exists && !file2Info.Exists)
return false;
if (file1Info.Length != file2Info.Length)
return false;
using (FileStream file1Stream = file1Info.OpenRead())
using (FileStream file2Stream = file2Info.OpenRead())
{
byte[] firstHash = MD5.Create().ComputeHash(file1Stream);
byte[] secondHash = MD5.Create().ComputeHash(file2Stream);
for (int i = 0; i < firstHash.Length; i++)
{
if (i>=secondHash.Length||firstHash[i] != secondHash[i])
return false;
}
return true;
}
}