Comment comparer rapidement 2 fichiers en utilisant .NET?

Réponses:

117

Une comparaison de somme de contrôle sera probablement plus lente qu'une comparaison octet par octet.

Afin de générer une somme de contrôle, vous devrez charger chaque octet du fichier et y effectuer un traitement. Vous devrez ensuite le faire sur le deuxième fichier. Le traitement sera presque certainement plus lent que le contrôle de comparaison.

Quant à générer une somme de contrôle: vous pouvez le faire facilement avec les classes de cryptographie. Voici un petit exemple de génération d'une somme de contrôle MD5 avec C #.

Cependant, une somme de contrôle peut être plus rapide et avoir plus de sens si vous pouvez pré-calculer la somme de contrôle du cas "test" ou "de base". Si vous avez un fichier existant et que vous vérifiez si un nouveau fichier est le même que le fichier existant, le pré-calcul de la somme de contrôle sur votre fichier "existant" signifierait seulement avoir besoin de faire le DiskIO une fois, sur le nouveau fichier. Ce serait probablement plus rapide qu'une comparaison octet par octet.

Reed Copsey
la source
30
Assurez-vous de prendre en compte l'emplacement de vos fichiers. Si vous comparez des fichiers locaux à une sauvegarde à mi-chemin à travers le monde (ou sur un réseau avec une bande passante horrible), vous feriez peut-être mieux de hacher d'abord et d'envoyer une somme de contrôle sur le réseau au lieu d'envoyer un flux d'octets à comparer.
Kim
@ReedCopsey: J'ai un problème similaire, car j'ai besoin de stocker des fichiers d'entrée / sortie produits par plusieurs élaborations qui sont censées contenir beaucoup de duplications. J'ai pensé utiliser le hachage précalculé, mais pensez-vous que je peux raisonnablement supposer que si le hachage 2 (par exemple MD5) est égal, les 2 fichiers sont égaux et évitent une autre comparaison octet-2 octets? Autant que je sache, les collisions MD5 / SHA1, etc. sont vraiment improbables ...
digEmAll
1
@digEmAll Les chances de collision sont faibles - vous pouvez toujours faire un hachage plus fort, cependant - c'est-à-dire: utilisez SHA256 au lieu de SHA1, ce qui réduira davantage la probabilité de collisions.
Reed Copsey
merci pour votre réponse - je viens juste d'entrer dans .net. Je suppose que si l'on utilise la technique de hashcode / check sum, alors les hachages du dossier principal seront stockés de manière persistante quelque part? par curiosité, comment le stockeriez-vous pour une application WPF - que feriez-vous? (J'ai actuellement à la recherche de fichiers XML, de fichiers texte ou de bases de données).
BKSpurgeon
139

La méthode la plus lente possible consiste à comparer deux fichiers octet par octet. Le plus rapide que j'ai pu trouver est une comparaison similaire, mais au lieu d'un octet à la fois, vous utiliseriez un tableau d'octets dimensionné à Int64, puis compareriez les nombres résultants.

Voici ce que j'ai trouvé:

    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 voir que cela surclassait un scénario ReadByte () simple de presque 3: 1. En moyenne sur 1000 exécutions, j'ai obtenu cette méthode à 1063 ms et la méthode ci-dessous (comparaison simple octet par octet) à 3031 ms. Le hachage revenait toujours en moins de seconde à environ 865 ms en moyenne. Ce test était 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;
    }
chsh
la source
1
Tu m'as rendu la vie plus facile. Merci
anindis
2
@anindis: Pour être complet, vous pouvez lire les deux la réponse de @Lars et @ la réponse de RandomInsano . Heureux que cela ait aidé tant d'années! :)
chsh
1
La FilesAreEqual_Hashméthode doit également avoir un usingsur les deux flux de fichiers comme la ReadByteméthode, sinon elle s'accrochera aux deux fichiers.
Ian Mercer
2
Notez que FileStream.Read()peut en fait lire moins d'octets que le nombre demandé. Vous devriez utiliser à la StreamReader.ReadBlock()place.
Palec
2
Dans la version Int64, lorsque la longueur du flux n'est pas un multiple de Int64, la dernière itération compare les octets non remplis en utilisant le remplissage de l'itération précédente (qui devrait également être égal, donc ça va). De plus, si la longueur du flux est inférieure à sizeof (Int64), les octets non remplis sont 0 puisque C # initialise les tableaux. OMI, le code devrait probablement commenter ces bizarreries.
crokusek
46

Si vous ne vous décidez vraiment besoin d' une comparaison octet par octet complet (voir d' autres réponses pour la discussion de hashing), la solution la plus simple est:


• pour les System.IO.FileInfoinstances:

public static bool AreFileContentsEqual(FileInfo fi1, FileInfo fi2) =>
    fi1.Length == fi2.Length &&
    (fi1.Length == 0 || File.ReadAllBytes(fi1.FullName).SequenceEqual(
                        File.ReadAllBytes(fi2.FullName)));


• pour les System.Stringnoms de chemin:

public static bool AreFileContentsEqual(String path1, String path2) =>
                   AreFileContentsEqual(new FileInfo(path1), new FileInfo(path2));


Contrairement à d'autres réponses postées, c'est formellement correct pour tout type de fichier: binaire, texte, médias, exécutable, etc., mais comme un complet binaire comparaison , les fichiers qui diffèrent uniquement par des moyens « sans importance » (comme nomenclature , ligne -envoi , encodage de caractères , métadonnées multimédia, espaces, remplissage, commentaires de code source, etc.) seront toujours considérés comme non égaux .

Ce code charge entièrement les deux fichiers en mémoire, il ne doit donc pas être utilisé pour comparer des fichiers vraiment gigantesques . Au-delà de cette mise en garde importante, le chargement complet n'est pas vraiment une pénalité compte tenu de la conception du .NET GC (car il est fondamentalement optimisé pour garder de petites allocations de courte durée extrêmement bon marché ), et en fait pourrait même être optimal lorsque la taille des fichiers est attendue être inférieur à 85K , car en utilisant un minimum de code utilisateur (comme indiqué ici) implique de déléguer au maximum les problèmes de performances des fichiers au CLR, BCLet JITà bénéficier de (par exemple) la dernière technologie de conception, le code du système et l' optimisation d'exécution adaptative.

En outre, pour de tels scénarios quotidiens, les préoccupations concernant les performances de la comparaison octet par octet via des LINQénumérateurs (comme indiqué ici) sont sans objet, car frapper le disque a̲t̲ a̲l̲l̲ pour les E / S de fichier éclipsera, de plusieurs ordres de grandeur, les avantages des différentes alternatives de comparaison de mémoire. Par exemple, même si nous SequenceEqual donne en fait "l'optimisation" de l' abandon au premier décalage , cela n'a guère d'importance après avoir déjà récupéré le contenu des fichiers, chacun étant entièrement nécessaire pour confirmer la correspondance.

Glenn Slayden
la source
3
celui-ci n'a pas l'air bien pour les gros fichiers. pas bon pour l'utilisation de la mémoire car il lira les deux fichiers jusqu'à la fin avant de commencer à comparer le tableau d'octets. C'est pourquoi je préférerais un lecteur de flux avec un tampon.
Krypto_47
3
@ Krypto_47 J'ai discuté de ces facteurs et de l'utilisation appropriée dans le texte de ma réponse.
Glenn Slayden
33

En plus de la réponse de Reed Copsey :

  • Le pire des cas est celui où les deux fichiers sont identiques. Dans ce cas, il est préférable de comparer les fichiers octet par octet.

  • Si si les deux fichiers ne sont pas identiques, vous pouvez accélérer un peu les choses en détectant plus tôt qu'ils ne sont pas identiques.

Par exemple, si les deux fichiers sont de longueur différente, vous savez qu'ils ne peuvent pas être identiques et vous n'avez même pas besoin de comparer leur contenu réel.

dtb
la source
10
Pour être complet: l'autre gros gain s'arrête dès que les octets à 1 position sont différents.
Henk Holterman
6
@Henk: Je pensais que c'était trop évident :-)
dtb
1
Bon point d'ajouter ceci. C'était évident pour moi, donc je ne l'ai pas inclus, mais il est bon de le mentionner.
Reed Copsey
16

Cela devient encore plus rapide si vous ne lisez pas par petits morceaux de 8 octets, mais mettez une boucle autour, en lisant un plus gros morceau. J'ai réduit le temps de comparaison moyen à 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;
                }
            }
        }
    }
}
Lars
la source
13
En général, la vérification count1 != count2n'est pas correcte. Stream.Read()peut renvoyer moins que le décompte que vous avez fourni, pour diverses raisons.
porges
1
Pour faire en sorte que le tampon tiendra un nombre pair de Int64blocs, vous pouvez calculer la taille comme ceci: const int bufferSize = 1024 * sizeof(Int64).
Jack A.
14

La seule chose qui pourrait 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 quelque peu le temps de recherche de la tête de disque. Ce léger gain peut cependant très bien être mangé par le temps supplémentaire de calcul du hachage.

De plus, une comparaison de somme de contrôle n'a bien sûr aucune chance d'être plus rapide si les fichiers sont identiques. Si ce n'est pas le cas, une comparaison octet par octet se terminerait à la première différence, ce qui la rendrait beaucoup plus rapide.

Vous devez également considérer qu'une comparaison de code de hachage vous indique uniquement qu'il est très probable que les fichiers soient identiques. Pour être sûr à 100%, vous devez effectuer une comparaison octet par octet.

Si le code de hachage, par exemple, est de 32 bits, vous êtes certain à environ 99,99999998% que les fichiers sont identiques si les codes de hachage correspondent. C'est près de 100%, mais si vous avez vraiment besoin d'une certitude à 100%, ce n'est pas tout.

Guffa
la source
Utilisez un hachage plus grand et vous pouvez obtenir les chances d'un faux positif bien en deçà des chances que l'ordinateur ait commis une erreur lors du test.
Loren Pechtel
Je ne suis pas d'accord sur le temps de hachage par rapport au temps de recherche. Vous pouvez effectuer de nombreux calculs lors d'une seule recherche de tête. Si les chances sont élevées que les fichiers correspondent, j'utiliserais un hachage avec beaucoup de bits. S'il y a une chance raisonnable d'une correspondance, je les comparerais un bloc à la fois, par exemple des blocs de 1 Mo. (Choisissez une taille de bloc que 4k divise uniformément pour vous assurer de ne jamais diviser les secteurs.)
Loren Pechtel
1
Pour expliquer le chiffre de 99,99999998% de @ Guffa, cela provient de l'informatique 1 - (1 / (2^32)), qui est la probabilité qu'un seul fichier ait un hachage 32 bits donné. La probabilité que deux fichiers différents aient le même hachage est la même, car le premier fichier fournit la valeur de hachage «donnée», et nous devons seulement considérer si l'autre fichier correspond ou non à cette valeur. Les chances de hachage 64 et 128 bits diminuent à 99,999999999999999994% et 99,9999999999999999999999999999999999997% (respectivement), comme si cela importait avec des nombres aussi insondables.
Glenn Slayden
... En effet, le fait que ces chiffres soient plus difficiles à saisir pour la plupart des gens que la notion supposément simple, bien que vraie, d '"une infinité de fichiers entrés en collision dans le même code de hachage" peut expliquer pourquoi les humains sont déraisonnablement méfiants d'accepter le hash-as- égalité.
Glenn Slayden le
13

Edit: Cette méthode ne fonctionnerait pas pour comparer des fichiers binaires!

Dans .NET 4.0, la Fileclasse a 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 pouvez utiliser:

bool same = File.ReadLines(path1).SequenceEqual(File.ReadLines(path2));
Sam Harwell
la source
1
@dtb: Cela ne fonctionne pas pour les fichiers binaires. Vous étiez probablement déjà en train de taper le commentaire lorsque j'ai réalisé cela et ajouté la modification en haut de mon message. : o
Sam Harwell
@ 280Z28: Je n'ai rien dit ;-)
dtb
N'auriez-vous pas également besoin de stocker les deux fichiers en mémoire?
RandomInsano
Notez que File a également la fonction ReadAllBytes qui peut également utiliser SequenceEquals, alors utilisez-la à la place car elle fonctionnerait sur tous les fichiers. Et comme @RandomInsano l'a dit, cela est stocké en mémoire.Par conséquent, même si c'est parfait à utiliser pour de petits fichiers, je ferais attention de l'utiliser avec de gros fichiers.
DaedalusAlpha
1
@DaedalusAlpha Il renvoie un énumérable, donc les lignes seront chargées à la demande et non stockées en mémoire tout le temps. ReadAllBytes, en revanche, renvoie le fichier entier sous forme de tableau.
IllidanS4 veut que Monica revienne
7

Honnêtement, je pense que vous devez élaguer votre arbre de recherche autant que possible.

Choses à vérifier avant de passer octet par octet:

  1. Les tailles sont-elles les mêmes?
  2. Le dernier octet du fichier A est-il différent du fichier B

En outre, la lecture de gros blocs à la fois sera plus efficace car les lecteurs lisent plus rapidement les octets séquentiels. Le passage octet par octet entraîne non seulement beaucoup plus d'appels système, mais il incite également la tête de lecture d'un disque dur traditionnel à faire des va-et-vient plus fréquents si les deux fichiers sont sur le même lecteur.

Lisez les blocs A et B dans un tampon d'octets et comparez-les (n'utilisez PAS Array.Equals, voir les commentaires) Ajustez la taille des blocs jusqu'à ce que vous atteigniez ce que vous pensez être un bon compromis entre la mémoire et les performances. Vous pouvez également multi-threader la comparaison, mais ne pas multi-thread les lectures de disque.

RandomInsano
la source
Utiliser Array.Equals est une mauvaise idée car il compare l'ensemble du tableau. Il est probable qu'au moins un bloc lu ne remplira pas tout le tableau.
Doug Clutter
Pourquoi comparer l'ensemble du tableau est-il une mauvaise idée? Pourquoi une lecture de bloc ne remplirait-elle pas le tableau? Il y a certainement un bon point d'accord, mais c'est pourquoi vous jouez avec les tailles. Points supplémentaires pour faire la comparaison dans un fil séparé.
RandomInsano
Lorsque vous définissez un tableau d'octets, il aura une longueur fixe. (par exemple - var buffer = new byte [4096]) Lorsque vous lisez un bloc du fichier, il peut ou non renvoyer les 4096 octets complets. Par exemple, si le fichier ne fait que 3000 octets.
Doug Clutter
Ah, maintenant je comprends! La bonne nouvelle est que la lecture renverra le nombre d'octets chargés dans le tableau, donc si le tableau ne peut pas être rempli, il y aura des données. Puisque nous testons l'égalité, les anciennes données de tampon n'auront pas d'importance. Docs: msdn.microsoft.com/en-us/library/9kstw824(v=vs.110).aspx
RandomInsano
Également important, ma recommandation d'utiliser la méthode Equals () est une mauvaise idée. En Mono, ils font une comparaison de mémoire puisque les éléments sont contigus en mémoire. Cependant, Microsoft ne le remplace pas, mais ne fait qu'une comparaison de référence qui serait toujours fausse.
RandomInsano
4

Ma réponse est un dérivé de @lars mais corrige le bogue dans l'appel à Stream.Read. J'ajoute également une vérification rapide des autres réponses et une validation d'entrée. En bref, cela devrait être la 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 async:

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;
                    }
                }
            }
        }
    }
}
Andrew Arnott
la source
le bit du convertisseur de bits ne serait-il pas meilleur comme `` `` for (var i = 0; i <count; i + = sizeof (long)) {if (BitConverter.ToInt64 (buffer1, i)! = BitConverter.ToInt64 (buffer2, i)) {return false; }} `` ``
Simon
2

Mes expériences montrent qu'il est vraiment utile d'appeler Stream.ReadByte () moins de fois, mais utiliser BitConverter pour empaqueter des 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 décalage de bits) avant de comparer et cela finit par être la même quantité de travail que comparer 8 octets dans deux tableaux .

Romeok
la source
1
Array.Equals va plus loin dans le système, donc ce sera probablement beaucoup plus rapide que d'aller octet par octet en C #. Je ne peux pas parler pour Microsoft, mais au fond, Mono utilise la commande memcpy () de C pour l'égalité des tableaux. Je ne peux pas aller beaucoup plus vite que ça.
RandomInsano
2
@RandomInsano suppose que vous voulez dire memcmp (), pas memcpy ()
SQL Police
1

Si les fichiers ne sont pas trop volumineux, vous pouvez utiliser:

public static byte[] ComputeFileHash(string fileName)
{
    using (var stream = File.OpenRead(fileName))
        return System.Security.Cryptography.MD5.Create().ComputeHash(stream);
}

Il ne sera possible de comparer les hachages que si les hachages sont utiles à stocker.

(Modification du code en quelque chose de beaucoup plus propre.)

Cecil a un nom
la source
1

Une autre amélioration sur les fichiers volumineux de longueur identique peut être de ne pas lire les fichiers séquentiellement, mais plutôt de comparer des blocs plus ou moins aléatoires.

Vous pouvez utiliser plusieurs threads, en commençant à différentes positions dans le fichier et en comparant vers l'avant ou vers l'arrière.

De cette façon, vous pouvez détecter les changements au milieu / à la fin du fichier, plus rapidement que vous ne le feriez en utilisant une approche séquentielle.

Thomas Kjørnes
la source
1
Le battage de disque causerait-il des problèmes ici?
RandomInsano
Disques physiques oui, les SSD géreraient cela.
TheLegendaryCopyCoder
1

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)

  1. ouvrez les deux fichiers f1, f2
  2. obtenir la longueur de fichier respective l1, l2
  3. si l1! = l2 les fichiers sont différents; Arrêtez
  4. mmap () les deux fichiers
  5. utiliser memcmp () sur les fichiers mmap () ed

OTOH, si vous avez besoin de trouver s'il y a des fichiers en double dans un ensemble de N fichiers, le moyen le plus rapide est sans aucun doute d'utiliser un hachage pour éviter les comparaisons bit par bit à N voies.

CAFxX
la source
1

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;
    }
}
Zar Shardan
la source
1

Voici quelques fonctions utilitaires qui vous permettent de déterminer si deux fichiers (ou deux flux) contiennent des données identiques.

J'ai fourni une version "rapide" qui est multithread car elle compare des tableaux d'octets (chaque tampon rempli à partir de ce qui a été lu dans chaque fichier) dans différents threads en utilisant des tâches.

Comme prévu, il est beaucoup plus rapide (environ 3 fois plus rapide) mais il consomme plus de CPU (car il est multi-thread) et plus de mémoire (car il a besoin de deux tampons de tableau d'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;
    }
Simon Mourier
la source
0

Je pense qu'il y a des applications où 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 de l'endroit et de la manière dont il est utilisé.

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));

En option, nous pouvons enregistrer le hachage dans une base de données.

J'espère que cela peut aider

Antonio
la source
0

Encore une autre réponse, dérivée de @chsh. MD5 avec utilisations et raccourcis pour le fichier même, le fichier n'existe pas et différentes longueurs:

/// <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;
    }
}
Andrew Taylor
la source
Vous dites if (i>=secondHash.Length ...Dans quelles circonstances deux hachages MD5 auraient-ils des longueurs différentes?
frogpelt
-1

Cela fonctionne bien en comparant d'abord la longueur sans lire les données, puis en comparant la séquence d'octets de lecture

private static bool IsFileIdentical(string a, string b)
{            
   if (new FileInfo(a).Length != new FileInfo(b).Length) return false;
   return (File.ReadAllBytes(a).SequenceEqual(File.ReadAllBytes(b)));
}
kernowcode
la source