在C#中比较二进制文件

34

我想比较两个二进制文件。其中一个已经存储在服务器上,并在最初存储它时,将其CRC32预先计算到数据库中。

我知道如果CRC不同,则文件肯定不同。但是,如果CRC相同,则不能确定文件是否相同。因此,我正在寻找一种好的高效方法来比较这两个流:一个来自上传的文件,另一个来自文件系统。

我不是流方面的专家,但我很清楚在内存使用方面可能会出现问题。

7个回答

43
static bool FileEquals(string fileName1, string fileName2)
{
    // Check the file size and CRC equality here.. if they are equal...    
    using (var file1 = new FileStream(fileName1, FileMode.Open))
        using (var file2 = new FileStream(fileName2, FileMode.Open))
            return FileStreamEquals(file1, file2);
}

static bool FileStreamEquals(Stream stream1, Stream stream2)
{
    const int bufferSize = 2048;
    byte[] buffer1 = new byte[bufferSize]; //buffer size
    byte[] 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;

        // You might replace the following with an efficient "memcmp"
        if (!buffer1.Take(count1).SequenceEqual(buffer2.Take(count2)))
            return false;
    }
}

6
要求 conunt1 == count2 可能不准确,因为 Stream.Read 函数可以返回一个长度小于请求字节数的数据块。请参阅 http://msdn.microsoft.com/en-us/library/vstudio/system.io.stream.read(v=vs.100).aspx。 - Karata
1
@Ozgur,它可以工作,但在我看来效率较低且原则不太明确。 - Mehrdad Afshari
我同意@Karata的评论。您应该通过反复调用Stream.Read()或使用StreamReader.ReadBlock()来确保缓冲区尽可能地填满。 - Palec
1
文档中提到即使对于FileStream也可能存在问题。你的意思是通常情况下不会有问题吗?还是说文档有误导性? - Palec
2
@Karata 绝对正确,这个有问题的代码不应该被放任不管(后来可能会有人在我使用的软件上使用它)。至少 FileStreamEquals 应该接受两个正确类型的 FileStream 参数;情况下可以可能认为如果没有出现问题,从文件中请求 n 字节的 Read 请求确实读取了 n 字节。但是你敢打赌你的生命(或你的公司)在每种情况下都能如此吗?网络映射驱动器呢?命名管道呢? - Peter - Reinstate Monica
显示剩余6条评论

22

我通过在读取流块上循环使用Int64进行比较,加速了"memcmp"的执行速度。这将时间缩短到大约1/4。

    private static bool StreamsContentsAreEqual(Stream stream1, Stream stream2)
    {
        const int bufferSize = 2048 * 2;
        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;
                }
            }
        }
    }

这只对64位CPU有优势,还是32位CPU也会受益? - Pretzel
无论您使用的是32位还是64位操作系统,都不应该有影响。但我从未在纯32位CPU上尝试过。您需要尝试一下,也许只需将Int64更改为int32即可。但是,大多数现代CPU是否都支持64位操作(自2004年以来的x86)呢?请放心尝试! - Lars
请查看此答案的评论(https://dev59.com/wnNA5IYBdhLWcg3wa9Kp#968980)。依赖于`count1`等于`count2`是不可靠的。 - T.J. Crowder
这个处理最后的0-7个字节的方式正确吗?你测试过两个文件,其中 fileSize % sizeof(Int64) > 0 并且只有最后一个字节不同的情况吗? - Dan Bechard

9

如果您不想依赖crc,我会这样做:

    /// <summary>
    /// Binary comparison of two files
    /// </summary>
    /// <param name="fileName1">the file to compare</param>
    /// <param name="fileName2">the other file to compare</param>
    /// <returns>a value indicateing weather the file are identical</returns>
    public static bool CompareFiles(string fileName1, string fileName2)
    {
        FileInfo info1 = new FileInfo(fileName1);
        FileInfo info2 = new FileInfo(fileName2);
        bool same = info1.Length == info2.Length;
        if (same)
        {
            using (FileStream fs1 = info1.OpenRead())
            using (FileStream fs2 = info2.OpenRead())
            using (BufferedStream bs1 = new BufferedStream(fs1))
            using (BufferedStream bs2 = new BufferedStream(fs2))
            {
                for (long i = 0; i < info1.Length; i++)
                {
                    if (bs1.ReadByte() != bs2.ReadByte())
                    {
                        same = false;
                        break;
                    }
                }
            }
        }

        return same;
    }

1
info2 应该接受 fileName2 作为参数,而不是 fileName1。否则,解决方案不错:-)。 - fbastian

6

被接受的答案存在一个错误,但从未进行更正:流读取调用不能保证返回所有请求的字节。

BinaryReader ReadBytes 调用保证返回所请求的字节数,除非先到达流的末尾。

以下代码利用BinaryReader进行比较:

    static private bool FileEquals(string file1, string file2)
    {
        using (FileStream s1 = new FileStream(file1, FileMode.Open, FileAccess.Read, FileShare.Read))
        using (FileStream s2 = new FileStream(file2, FileMode.Open, FileAccess.Read, FileShare.Read))
        using (BinaryReader b1 = new BinaryReader(s1))
        using (BinaryReader b2 = new BinaryReader(s2))
        {
            while (true)
            {
                byte[] data1 = b1.ReadBytes(64 * 1024);
                byte[] data2 = b2.ReadBytes(64 * 1024);
                if (data1.Length != data2.Length)
                    return false;
                if (data1.Length == 0)
                    return true;
                if (!data1.SequenceEqual(data2))
                    return false;
            }
        }
    }

但是,这样做的缺点是它会在内存中分配两个文件。更有效的方法是使用多个FileStream上的Read来获取丢失的字节。 - dafie
@dafie 不,它并不像你所认为的那样将整个文件读入内存。是的,有一些缓冲,但我认为你会发现这段代码非常高效,以64k块的方式顺序读取两个文件。 - Larry
@我已经进行了一些基准测试:https://pastebin.com/raw/ky9D8ynd - dafie
@dafie 这很有用。我的帖子并不是为了展示绝对最优的方法,只是一个简单的方法,避免了先前贴子中的严重错误。如果我正确理解您的帖子,ForceRead方法(之前未发布)会更有效率一些。 - Larry

3
如果您将该crc更改为sha1签名,则具有相同签名但不同的机会非常小。

在大多数严肃的应用程序中,你永远不应该依赖于这个。这就像在哈希表查找中仅检查哈希而不比较实际键一样! - Mehrdad Afshari
1
不幸的是,你无法保证它出错的那一次会非常关键,可能就是那个重要的演示。 - Simon Farrow
@Simon - 哈哈,非常正确。 @Mehrdad - 可能不是,但这将大大减少您需要检查的次数,以确保超级优质。 - albertjan
取CRC并说文件大小,变化越来越小。 - kenny
2
@MehrdadAfshari,像Git这样的严肃应用程序正是依赖于这一点。引用Linus Torvalds的话,我们“很可能永远不会在[通过比较SHA的两个文件的碰撞]宇宙的全部历史中看到它”。参见https://dev59.com/PWox5IYBdhLWcg3wKBN9。 - Maate
@Maate Git不依赖SHA1来验证单个文件的相等性(这是更有可能发生的情况),但是有时候也是有意义的。 - Mehrdad Afshari

2

您甚至可以在检查CRC之前检查两个文件的长度和日期,以可能避免进行CRC检查。

但是,如果您必须比较整个文件内容,一个好方法是按与CPU位数相等的步长读取字节。例如,在32位PC上,每次读取4个字节并将它们作为int32比较。在64位PC上,您可以一次读取8个字节。这大约比逐字节进行4或8倍快。您还可能需要使用unsafe代码块,以便您可以使用指针而不是进行大量位移和OR操作来将字节转换为本机int大小。

您可以使用IntPtr.Size来确定当前处理器架构的理想大小。


1
我采用了之前的答案,并从BinaryReader.ReadBytes的源代码中添加了逻辑,以得到一个在每个循环中不重新创建缓冲区并且不受FileStream.Read意外返回值影响的解决方案。
public static bool AreSame(string path1, string path2) {
    int BUFFER_SIZE = 64 * 1024;
    byte[] buffer1 = new byte[BUFFER_SIZE];
    byte[] buffer2 = new byte[BUFFER_SIZE];

    int ReadBytes(FileStream fs, byte[] buffer) {
        int totalBytes = 0;
        int count = buffer.Length;
        while (count > 0) {
            int readBytes = fs.Read(buffer, totalBytes, count);
            if (readBytes == 0)
                break;

            totalBytes += readBytes;
            count -= readBytes;
        }

        return totalBytes;
    }

    using (FileStream fs1 = new FileStream(path1, FileMode.Open, FileAccess.Read, FileShare.Read))
    using (FileStream fs2 = new FileStream(path2, FileMode.Open, FileAccess.Read, FileShare.Read)) {
        while (true) {
            int count1 = ReadBytes(fs1, buffer1);
            int count2 = ReadBytes(fs2, buffer2);

            if (count1 != count2)
                return false;

            if (count1 == 0)
                return true;

            if (count1 == BUFFER_SIZE) {
                if (!buffer1.SequenceEqual(buffer2))
                    return false;
            } else {
                if (!buffer1.Take(count1).SequenceEqual(buffer2.Take(count2)))
                    return false;
            }
        }
    }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接