如何使用.NET快速比较2个文件?

Rob*_*cks 126 c# checksum compare file

典型的方法建议通过FileStream读取二进制文件并逐字节地比较它.

  • CRC校验和比较会更快吗?
  • 是否有任何.NET库可以为文件生成校验和?

chs*_*hsh 127

最慢的方法是逐字节比较两个文件.我能够提出的最快速度是类似的比较,但是你不是一次只使用一个字节,而是使用一个大小为Int64的字节数组,然后比较得到的数字.

这是我想出的:

    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;
    }
Run Code Online (Sandbox Code Playgroud)

在我的测试中,我能够看到这个表现优于简单的ReadByte()场景几乎3:1.平均超过1000次运行,我在1063ms获得此方法,并且在3031ms处获得下面的方法(直接逐字节比较).哈希总是在平均865毫秒左右回到亚秒级.这个测试是一个~100MB的视频文件.

这是我使用的ReadByte和散列方法,用于比较目的:

    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;
    }
Run Code Online (Sandbox Code Playgroud)

  • @anindis:为了完整起见,您可能想要阅读[@Lars'答案](http://stackoverflow.com/a/2637350/122268)和[@ RandomInsano的答案](http://stackoverflow.com/a/一十二万二千二百六十八分之九百零三万八千一百三十八).很高兴它帮助这么多年了!:) (2认同)
  • “FilesAreEqual_Hash”方法也应该像“ReadByte”方法一样在两个文件流上都有一个“using”,否则它将挂在两个文件上。 (2认同)
  • 请注意,`FileStream.Read()`实际上可能读取的字节数少于请求的数字.你应该使用`StreamReader.ReadBlock()`代替. (2认同)
  • 在 Int64 版本中,当流长度不是 Int64 的倍数时,最后一次迭代是使用前一次迭代的填充来比较未填充的字节(它也应该相等,所以没问题)。此外,如果流长度小于 sizeof(Int64) 则未填充的字节为 0,因为 C# 初始化数组。IMO,代码可能应该评论这些奇怪的东西。 (2认同)

Ree*_*sey 111

校验和比较很可能比逐字节比较慢.

为了生成校验和,您需要加载文件的每个字节,并对其执行处理.然后,您必须在第二个文件上执行此操作.处理几乎肯定比比较检查慢.

至于生成校验和:您可以使用加密类轻松完成此操作.这是使用C#生成MD5校验和简短示例.

但是,如果您可以预先计算"测试"或"基础"案例的校验和,则校验和可能更快并且更有意义.如果您有一个现有文件,并且您正在检查新文件是否与现有文件相同,那么预先计算"现有"文件上的校验和意味着只需要执行一次DiskIO,新文件.这可能比逐字节比较更快.

  • 请务必考虑文件的位置.如果您将本地文件与世界各地的备份(或通过带宽可怕的网络)进行比较,您可能最好首先进行哈希并通过网络发送校验和,而不是发送字节流到相比. (28认同)

Gle*_*den 40

如果决定你真正需要一个完整的逐字节的比较(见散列讨论其他的答案),然后是一个在线的解决方案是:

bool filesAreEqual = File.ReadAllBytes(path1).SequenceEqual(File.ReadAllBytes(path2));
Run Code Online (Sandbox Code Playgroud)

与其他一些发布的答案不同,这适用于任何类型的文件:二进制文本,文本,媒体,可执行文件等,但作为完整的二进制比较,文件只有 "不重要"的方式不同(例如BOM,行 -结尾,字符编码,媒体元数据,空格,填充,源代码注释等)将始终被视为不相等.

此代码完全将两个文件加载到内存中,因此不应将其用于比较巨大的文件.除了这种考虑之外,满载并不是真正的惩罚; 实际上,这可能是文件大小的最佳.NET解决方案,预计小于85K,因为小的分配.NET非常便宜,我们最大限度地将文件性能和优化委托给CLR/ BCL.

此外,对于这样的工作日场景,关于通过LINQ枚举器进行逐字节比较(如此处所示)的性能的问题是没有实际意义的,因为命中文件I/O的磁盘将使数据的好处相形见绌.各种记忆比较的替代品.例如,尽管实际上SequenceEqual 确实给了我们放弃第一次不匹配的"优化",但获取文件的内容之后这几乎不重要,每次都必须确认匹配.

另一方面,上述代码包括针对不同大小的文件的急切中止,这可以提供有形(可能可测量的)性能差异.这个是有形的,因为虽然文件长度在WIN32_FILE_ATTRIBUTE_DATA结构中是可用的(无论如何都必须首先获取任何文件访问),继续访问文件的内容需要完全不同的提取,这可能是可能避免的.如果您对此感到担心,解决方案将变为两行:

// slight optimization over the code shown above
bool filesAreEqual = new FileInfo(path1).Length == new FileInfo(path2).Length && 
       File.ReadAllBytes(path1).SequenceEqual(File.ReadAllBytes(path2));
Run Code Online (Sandbox Code Playgroud)

如果(等效)Length值都被发现为零(未显示)和/或避免每次构建false两次(也未显示),您也可以对此进行扩展以避免二次提取.

  • 这个对大文件看起来不太好.不太适合内存使用,因为在开始比较字节数组之前它会读取两个文件.这就是为什么我宁愿选择带缓冲区的streamreader. (3认同)
  • @ Krypto_47我在答案的文本中讨论了这些因素和适当的用法. (3认同)

dtb*_*dtb 34

除了里德科普塞的回答:

  • 最糟糕的情况是两个文件完全相同.在这种情况下,最好逐个字节地比较文件.

  • 如果这两个文件不相同,你可以通过更快地检测到它们不相同来加快速度.

例如,如果两个文件的长度不同,那么您就知道它们不能完全相同,甚至不必比较它们的实际内容.

  • 完成:只要1个位置的字节不同,另一个大的收益就是停止. (10认同)
  • @Henk:我觉得这太明显了:-) (6认同)

Guf*_*ffa 15

唯一可能使校验和比较比逐字节比较快一点的事实是你一次读取一个文件,这有点减少磁盘头的查找时间.然而,通过计算散列的额外时间可以很好地消除这种微小的增益.

此外,校验和比较当然只有在文件相同时才有可能更快.如果不是这样,逐字节比较将以第一个差异结束,使其快得多.

您还应该考虑哈希码比较仅告诉您文件很可能是相同的.要100%确定,您需要进行逐字节比较.

例如,如果哈希码是32位,那么如果哈希码匹配,则大约99.99999998%确定文件是相同的.这接近100%,但如果你真的需要100%的确定性,那不是它.


Lar*_*ars 15

如果你不读取小的8字节块但是放一个循环,读取更大的块,它会变得更快.我将平均比较时间缩短到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;
                }
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

  • 通常,检查`count1!= count2`是不正确的.由于各种原因,`Stream.Read()`可以返回少于你提供的计数. (13认同)

Sam*_*ell 12

编辑:此方法不适用于比较二进制文件!

在.NET 4.0中,File该类具有以下两种新方法:

public static IEnumerable<string> ReadLines(string path)
public static IEnumerable<string> ReadLines(string path, Encoding encoding)
Run Code Online (Sandbox Code Playgroud)

这意味着您可以使用:

bool same = File.ReadLines(path1).SequenceEqual(File.ReadLines(path2));
Run Code Online (Sandbox Code Playgroud)

  • @DaedalusAlpha 它返回一个可枚举的,因此这些行将按需加载,而不是一直存储在内存中。另一方面,ReadAllBytes 确实将整个文件作为数组返回。 (2认同)

Ran*_*ano 6

老实说,我认为你需要尽可能地修剪你的搜索树.

在逐字节前进行检查:

  1. 尺寸是否相同?
  2. 文件A中的最后一个字节是否与文件B不同

此外,由于驱动器更快地读取顺序字节,因此一次读取大块将更有效.逐字节转换不仅会导致更多系统调用,而且如果两个文件位于同一驱动器上,它会导致传统硬盘驱动器的读取头更频繁地来回搜索.

将块A和块B读入字节缓冲区,并进行比较(不要使用Array.Equals,请参阅注释).调整块的大小,直到达到你认为在内存和性能之间的良好折衷.您也可以多线程比较,但不要多线程磁盘读取.


And*_*ott 6

我的答案是@lars 的衍生物,但修复了调用Stream.Read. 我还添加了一些其他答案所具有的快速路径检查和输入验证。总之,这应该是答案:

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;
                    }
                }
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

或者,如果你想变得超级棒,你可以使用 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;
                    }
                }
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)