¿Cómo comparar 2 archivos rápidamente usando .NET?


Respuestas:


117

Una comparación de suma de verificación probablemente será más lenta que una comparación byte por byte.

Para generar una suma de verificación, deberá cargar cada byte del archivo y realizar el procesamiento en él. Luego tendrá que hacer esto en el segundo archivo. El procesamiento casi definitivamente será más lento que la verificación de comparación.

En cuanto a generar una suma de verificación: puede hacerlo fácilmente con las clases de criptografía. Aquí hay un breve ejemplo de cómo generar una suma de verificación MD5 con C #.

Sin embargo, una suma de verificación puede ser más rápida y tener más sentido si puede precalcular la suma de verificación del caso "prueba" o "base". Si tiene un archivo existente y está verificando si un nuevo archivo es el mismo que el existente, calcular previamente la suma de verificación en su archivo "existente" significaría que solo necesita hacer DiskIO una vez, en el archivo nuevo. Esto probablemente sería más rápido que una comparación byte por byte.


30
Asegúrese de tener en cuenta dónde se encuentran sus archivos. Si está comparando archivos locales con una copia de seguridad en todo el mundo (o en una red con un ancho de banda horrible), es mejor que primero haga hash y envíe una suma de verificación a través de la red en lugar de enviar una secuencia de bytes a comparar.
Kim

@ReedCopsey: Tengo un problema similar, ya que necesito almacenar archivos de entrada / salida producidos por varias elaboraciones que se supone que contienen muchas duplicaciones. Pensé en usar hash precalculado, pero ¿crees que puedo suponer razonablemente que si 2 (por ejemplo, MD5) hash son iguales, los 2 archivos son iguales y evito una mayor comparación byte-2-byte? Por lo que yo sé SHA1 etc colisiones MD5 / son muy poco probable ...
digEmAll

1
@digEmAll La posibilidad de colisión es baja; sin embargo, siempre puede hacer un hash más fuerte, es decir: use SHA256 en lugar de SHA1, lo que reducirá aún más la probabilidad de colisiones.
Reed Copsey

gracias por su respuesta, me estoy metiendo en .net. ¿Supongo que si uno está usando la técnica de suma de verificación / código hash, entonces los hash de la carpeta principal se almacenarán de forma persistente en algún lugar? por curiosidad, ¿cómo lo almacenarías para una aplicación WPF? ¿Qué harías? (Actualmente estoy mirando xml, archivos de texto o bases de datos).
BKSpurgeon

139

El método más lento posible es comparar dos archivos byte por byte. Lo más rápido que he podido llegar es una comparación similar, pero en lugar de un byte a la vez, usaría una matriz de bytes de tamaño Int64 y luego compararía los números resultantes.

Esto es lo que se me ocurrió:

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

En mis pruebas, pude ver que esto superaba un escenario ReadByte () directo en casi 3: 1. Con un promedio de más de 1000 carreras, obtuve este método a 1063 ms, y el método a continuación (comparación directa byte por byte) a 3031 ms. El hashing siempre regresó en menos de un segundo alrededor de un promedio de 865 ms. Esta prueba fue con un archivo de video de ~ 100MB.

Aquí están los métodos ReadByte y hash que utilicé, para fines de comparación:

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

1
Me hiciste la vida más fácil. Gracias
anindis

2
@anindis: Para completar, es posible que desee leer ambos respuesta @Lars' y @ respuesta de RandomInsano . ¡Me alegro de que haya ayudado tantos años! :)
chsh

1
El FilesAreEqual_Hashmétodo también debería tener un usingflujo en ambas secuencias de archivos ReadByte, de lo contrario se mantendrá en ambos archivos.
Ian Mercer

2
Tenga en cuenta que en FileStream.Read()realidad puede leer menos bytes que el número solicitado. Deberías usar StreamReader.ReadBlock()en su lugar.
Palec

2
En la versión Int64, cuando la longitud de la secuencia no es un múltiplo de Int64, la última iteración compara los bytes no rellenados utilizando el relleno de la iteración anterior (que también debe ser igual, por lo que está bien). Además, si la longitud de la secuencia es menor que sizeof (Int64), los bytes sin llenar son 0, ya que C # inicializa las matrices. OMI, el código probablemente debería comentar estas rarezas.
crokusek

46

Si no decide que realmente necesita una comparación completa byte por byte (consulte otras respuestas para analizar el hash), entonces la solución más fácil es:


• por System.IO.FileInfoinstancias:

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


• para System.Stringnombres de ruta:

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


A diferencia de otras respuestas publicadas, esto es concluyentemente correcto para cualquier tipo de archivo: binario, texto, medios, ejecutable, etc., pero como una comparación binaria completa , archivos que difieren solo en formas "sin importancia" (como BOM , línea -final , codificación de caracteres , metadatos de medios, espacios en blanco, relleno, comentarios de código fuente, etc.) siempre se considerarán no iguales .

Este código carga ambos archivos en la memoria por completo, por lo que no debe usarse para comparar archivos verdaderamente gigantescos . Más allá de esa advertencia importante, la carga completa no es realmente una penalización dado el diseño de .NET GC (porque está optimizado fundamentalmente para mantener las asignaciones pequeñas y de corta duración extremadamente baratas ), y de hecho incluso podría ser óptimo cuando se espera un tamaño de archivo a ser inferior a 85K , porque el uso de un mínimo de código de usuario (como se muestra aquí) implica delegar al máximo los problemas de rendimiento archivo al CLR, BCLy JITque se benefician de (por ejemplo) la última tecnología de diseño, código de sistema, y optimizaciones de tiempo de ejecución de adaptación.

Además, para tales escenarios de días de trabajo, las preocupaciones sobre el rendimiento de la comparación byte a byte a través de LINQenumeradores (como se muestra aquí) son discutibles, ya que golpear el disco a̲t̲ a̲l̲l̲ para E / S de archivo empequeñecerá, en varios órdenes de magnitud, los beneficios de las diversas alternativas de comparación de memoria. Por ejemplo, a pesar de que SequenceEqual no , de hecho, nos dará la "optimización" de abandonar en la primera falta de coincidencia , esto importa poco después de tener ya leídos contenidos de los ficheros, cada uno totalmente necesaria para confirmar el partido ..


3
este no se ve bien para archivos grandes. no es bueno para el uso de la memoria, ya que leerá ambos archivos hasta el final antes de comenzar a comparar la matriz de bytes. Es por eso que prefiero ir a un lector de secuencias con un búfer.
Krypto_47

3
@ Krypto_47 Discutí estos factores y el uso apropiado en el texto de mi respuesta.
Glenn Slayden

33

Además de la respuesta de Reed Copsey :

  • El peor de los casos es donde los dos archivos son idénticos. En este caso, es mejor comparar los archivos byte por byte.

  • Si los dos archivos no son idénticos, puede acelerar un poco las cosas detectando antes que no son idénticos.

Por ejemplo, si los dos archivos tienen una longitud diferente, entonces sabe que no pueden ser idénticos y ni siquiera tiene que comparar su contenido real.


10
Para completar: la otra gran ganancia se detiene tan pronto como los bytes en 1 posición son diferentes.
Henk Holterman el

66
@ Henk: pensé que esto era demasiado obvio :-)
dtb

1
Buen punto para agregar esto. Era obvio para mí, así que no lo incluí, pero es bueno mencionarlo.
Reed Copsey

16

Se está volviendo aún más rápido si no lee en fragmentos pequeños de 8 bytes pero coloca un bucle, leyendo un fragmento más grande. Reduje el tiempo de comparación promedio a 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;
                }
            }
        }
    }
}

13
En general el cheque count1 != count2no es correcto. Stream.Read()puede devolver menos del recuento que ha proporcionado, por varias razones.
porges

1
Para asegurarse de que la memoria intermedia tendrá un número par de Int64bloques, es posible que desee para calcular el tamaño de la siguiente manera: const int bufferSize = 1024 * sizeof(Int64).
Jack A.

14

Lo único que puede hacer que una comparación de suma de comprobación sea un poco más rápida que una comparación byte por byte es el hecho de que está leyendo un archivo a la vez, lo que reduce el tiempo de búsqueda del cabezal del disco. Sin embargo, esa ligera ganancia puede muy bien ser absorbida por el tiempo adicional de calcular el hash.

Además, una comparación de suma de comprobación, por supuesto, solo tiene alguna posibilidad de ser más rápida si los archivos son idénticos. Si no lo son, una comparación byte por byte terminaría en la primera diferencia, lo que lo hace mucho más rápido.

También debe considerar que una comparación de código hash solo le dice que es muy probable que los archivos sean idénticos. Para estar 100% seguro, necesita hacer una comparación byte por byte.

Si el código hash, por ejemplo, es de 32 bits, está aproximadamente 99.99999998% seguro de que los archivos son idénticos si los códigos hash coinciden. Eso está cerca del 100%, pero si realmente necesita 100% de certeza, no es eso.


Use un hash más grande y puede obtener las probabilidades de un falso positivo muy por debajo de las probabilidades que la computadora cometió un error al hacer la prueba.
Loren Pechtel

No estoy de acuerdo sobre el tiempo hash vs tiempo de búsqueda. Puede hacer muchos cálculos durante una sola búsqueda de cabeza. Si las probabilidades de que los archivos coincidan son altas, usaría un hash con muchos bits. Si hay una posibilidad razonable de una coincidencia, los compararía un bloque a la vez, por ejemplo, bloques de 1 MB. (Elija un tamaño de bloque que 4k se divida de manera uniforme para asegurarse de que nunca divida sectores).
Loren Pechtel

1
Para explicar la cifra del 99.99999998% de @ Guffa, proviene de la informática 1 - (1 / (2^32)), que es la probabilidad de que cualquier archivo tenga algún hash de 32 bits. La probabilidad de que dos archivos diferentes tengan el mismo hash es la misma, porque el primer archivo proporciona el valor de hash "dado", y solo necesitamos considerar si el otro archivo coincide o no con ese valor. Las posibilidades con el hashing de 64 y 128 bits disminuyen a 99.999999999999999994% y 99.9999999999999999999999999999999999997% (respectivamente), como si eso fuera importante con esos números insondables.
Glenn Slayden

... De hecho, el hecho de que estos números son más difíciles de comprender para la mayoría de las personas que la noción supuestamente simple, aunque cierto, de "infinitos archivos colisionando en el mismo código hash" puede explicar por qué los humanos sospechan irrazonablemente de aceptar hash-as- igualdad.
Glenn Slayden

13

Editar: ¡ Este método no funcionaría para comparar archivos binarios!

En .NET 4.0, la Fileclase tiene los siguientes dos métodos nuevos:

public static IEnumerable<string> ReadLines(string path)
public static IEnumerable<string> ReadLines(string path, Encoding encoding)

Lo que significa que podrías usar:

bool same = File.ReadLines(path1).SequenceEqual(File.ReadLines(path2));

1
@dtb: no funciona para archivos binarios. Probablemente ya estabas escribiendo el comentario cuando me di cuenta de eso y agregué la edición en la parte superior de mi publicación. : o
Sam Harwell

@ 280Z28: No dije nada ;-)
dtb

¿No necesitarías también almacenar ambos archivos en la memoria?
RandomInsano

Tenga en cuenta que File también tiene la función ReadAllBytes, que también puede usar SequenceEquals, así que úsela, ya que funcionaría en todos los archivos. Y como dijo @RandomInsano, esto se almacena en la memoria, por lo que si bien está muy bien usarlo para archivos pequeños, tendría cuidado de usarlo con archivos grandes.
DaedalusAlpha

1
@DaedalusAlpha Devuelve un enumerable, por lo que las líneas se cargarán a pedido y no se almacenarán en la memoria todo el tiempo. ReadAllBytes, por otro lado, devuelve todo el archivo como una matriz.
IllidanS4 quiere que Monica regrese el

7

Honestamente, creo que necesitas podar tu árbol de búsqueda tanto como sea posible.

Cosas para verificar antes de ir byte a byte:

  1. ¿Los tamaños son iguales?
  2. Es el último byte en el archivo A diferente al archivo B

Además, leer bloques grandes a la vez será más eficiente ya que las unidades leen bytes secuenciales más rápidamente. Ir byte a byte causa no solo muchas más llamadas al sistema, sino que hace que el cabezal de lectura de un disco duro tradicional busque más adelante y atrás si ambos archivos están en la misma unidad.

Lea el fragmento A y el fragmento B en un búfer de bytes, y compárelos (NO use Array.Equals, vea los comentarios). Ajuste el tamaño de los bloques hasta que encuentre lo que cree que es un buen intercambio entre memoria y rendimiento. También puede hacer varios subprocesos en la comparación, pero no haga varios subprocesos en las lecturas del disco.


Usar Array.Equals es una mala idea porque compara toda la matriz. Es probable que al menos una lectura de bloque no llene toda la matriz.
Doug Clutter

¿Por qué es una mala idea comparar todo el conjunto? ¿Por qué una lectura de bloque no llena la matriz? Definitivamente hay un buen punto de ajuste, pero es por eso que juegas con los tamaños. Puntos extra por hacer la comparación en un hilo separado.
RandomInsano

Cuando define una matriz de bytes, tendrá una longitud fija. (por ejemplo, var buffer = nuevo byte [4096]) Cuando lee un bloque del archivo, puede devolver o no los 4096 bytes completos. Por ejemplo, si el archivo tiene solo 3000 bytes de longitud.
Doug Clutter

Ah, ahora entiendo! La buena noticia es que la lectura devolverá el número de bytes cargados en la matriz, por lo que si la matriz no se puede llenar, habrá datos. Como estamos probando la igualdad, los datos del búfer antiguo no importan. Documentos: msdn.microsoft.com/en-us/library/9kstw824(v=vs.110).aspx
RandomInsano

También es importante, mi recomendación de usar el método Equals () es una mala idea. En Mono, hacen una comparación de memoria ya que los elementos son contiguos en la memoria. Sin embargo, Microsoft no lo anula, sino que solo hace una comparación de referencia que aquí siempre sería falsa.
RandomInsano

4

Mi respuesta es un derivado de @lars pero corrige el error en la llamada a Stream.Read. También agrego algunas comprobaciones de ruta rápida que tenían otras respuestas y validación de entrada. En resumen, esta debería ser la respuesta:

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

O si quieres ser súper genial, puedes usar la variante asíncrona:

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

¿no sería mejor el bit del convertidor de bits como `` `for (var i = 0; i <count; i + = sizeof (long)) {if (BitConverter.ToInt64 (buffer1, i)! = BitConverter.ToInt64 (buffer2, i)) { falso retorno; }} `` `
Simon

2

Mis experimentos muestran que definitivamente ayuda llamar Stream.ReadByte () menos veces, pero usar BitConverter para empaquetar bytes no hace mucha diferencia en comparación de bytes en una matriz de bytes.

Por lo tanto, es posible reemplazar ese bucle "Math.Ceiling and iterations" en el comentario anterior con el más simple:

            for (int i = 0; i < count1; i++)
            {
                if (buffer1[i] != buffer2[i])
                    return false;
            }

Supongo que tiene que ver con el hecho de que BitConverter.ToInt64 necesita hacer un poco de trabajo (verifique los argumentos y luego realice el cambio de bits) antes de comparar y eso termina siendo la misma cantidad de trabajo que comparar 8 bytes en dos matrices .


1
Array.Equals profundiza en el sistema, por lo que es probable que sea mucho más rápido que ir byte a byte en C #. No puedo hablar por Microsoft, pero en el fondo, Mono usa el comando memcpy () de C para la igualdad de matriz. No puede ser mucho más rápido que eso.
RandomInsano

2
@RandomInsano supongo que te refieres a memcmp (), no memcpy ()
SQL Police

1

Si los archivos no son demasiado grandes, puede usar:

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

Solo será factible comparar hashes si los hashes son útiles para almacenar.

(Editó el código a algo mucho más limpio).


1

Otra mejora en archivos grandes con una longitud idéntica podría ser no leer los archivos secuencialmente, sino comparar bloques más o menos aleatorios.

Puede usar múltiples hilos, comenzando en diferentes posiciones en el archivo y comparando hacia adelante o hacia atrás.

De esta forma, puede detectar cambios en el medio / final del archivo, más rápido de lo que lo haría utilizando un enfoque secuencial.


1
¿La vibración del disco podría causar problemas aquí?
RandomInsano

Las unidades de disco físico sí, las SSD manejarían esto.
TheLegendaryCopyCoder

1

Si solo necesita comparar dos archivos, supongo que la forma más rápida sería (en C, no sé si es aplicable a .NET)

  1. abrir ambos archivos f1, f2
  2. obtener la longitud de archivo respectiva l1, l2
  3. si l1! = l2 los archivos son diferentes; detener
  4. mmap () ambos archivos
  5. use memcmp () en los archivos mmap () ed

OTOH, si necesita encontrar si hay archivos duplicados en un conjunto de N archivos, entonces la forma más rápida es, sin duda, usar un hash para evitar comparaciones N-way bit a bit.


1

Algo (con suerte) razonablemente eficiente:

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

1

Estas son algunas funciones de utilidad que le permiten determinar si dos archivos (o dos secuencias) contienen datos idénticos.

He proporcionado una versión "rápida" que es multiproceso, ya que compara los conjuntos de bytes (cada búfer lleno de lo que se ha leído en cada archivo) en diferentes subprocesos utilizando Tareas.

Como se esperaba, es mucho más rápido (alrededor de 3 veces más rápido) pero consume más CPU (porque es multiproceso) y más memoria (porque necesita dos buffers de matriz de bytes por hilo de comparación).

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

0

Creo que hay aplicaciones donde "hash" es más rápido que comparar byte por byte. Si necesita comparar un archivo con otros o tener una miniatura de una foto que pueda cambiar. Depende de dónde y cómo se use.

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

Aquí, puedes obtener lo que es más rápido.

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

Opcionalmente, podemos guardar el hash en una base de datos.

Espero que esto pueda ayudar


0

Otra respuesta, derivada de @chsh. MD5 con usos y accesos directos para el mismo archivo, el archivo no existe y diferentes longitudes:

/// <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;
    }
}

Usted dice if (i>=secondHash.Length ...¿Bajo qué circunstancias serían dos hashes MD5 de diferentes longitudes?
frogpelt

-1

Esto he encontrado que funciona bien al comparar primero la longitud sin leer los datos y luego comparar la secuencia de bytes de lectura

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)));
}
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.