如何在golang中比较两个文件?

13

使用Python,我可以做到以下事情:

equals = filecmp.cmp(file_old, file_new)

Go语言中是否有内置函数可以实现这个功能?我尝试使用谷歌搜索但没有成功。

我可以使用hash/crc32包中的一些哈希函数,但是相比上面的Python代码,那需要更多的工作。


1
你能澄清一下问题吗?它在询问两件不同的事情(一个替换 filecmp.cmp 的方法和一种检查两个文件是否包含相同字节的方式)。 - Paul Hankin
1
当然,我正在使用Python编写一个差异比较工具(为了自学Python),它可以比较文件并使用filecmp.cmp函数来比较新旧文件的补丁。现在我正在使用Go语言编写同样的工具,但我找不到类似上述函数的一些函数,因此我的问题是是否可以找到一个内置函数来比较文件,但如果不存在,我建议使用一些哈希函数或编写字节比较函数。抱歉我的英语。 - rvillablanca
10个回答

13
为了补充@captncraig的回答,如果您想知道两个文件是否相同,您可以使用OS包中的SameFile(fi1, fi2 FileInfo)方法。

SameFile报告fi1和fi2是否描述同一个文件。例如,在Unix上,这意味着两个底层结构的设备和inode字段是相同的;

否则,如果您想检查文件内容,这里有一个解决方案,它逐行检查两个文件,避免将整个文件加载到内存中。
首先尝试:https://play.golang.org/p/NlQZRrW1dT

编辑:按字节块读取文件并在文件大小不同时快速失败。 https://play.golang.org/p/YyYWuCRJXV

const chunkSize = 64000

func deepCompare(file1, file2 string) bool {
    // Check file size ...

    f1, err := os.Open(file1)
    if err != nil {
        log.Fatal(err)
    }
    defer f1.Close()

    f2, err := os.Open(file2)
    if err != nil {
        log.Fatal(err)
    }
    defer f2.Close()

    for {
        b1 := make([]byte, chunkSize)
        _, err1 := f1.Read(b1)

        b2 := make([]byte, chunkSize)
        _, err2 := f2.Read(b2)

        if err1 != nil || err2 != nil {
            if err1 == io.EOF && err2 == io.EOF {
                return true
            } else if err1 == io.EOF || err2 == io.EOF {
                return false
            } else {
                log.Fatal(err1, err2)
            }
        }

        if !bytes.Equal(b1, b2) {
            return false
        }
    }
}

2
为什么会有扫描器的开销呢?因为它需要解析字节以查找你不关心的行分隔符。对于二进制文件,它也可能无法按照你的预期工作。你可以只读取“块”到一对合理大小的缓冲区中,并在进行操作时使用 bytes.Equal(这就是 @captncraig 建议的方法)。 - Dave C
顺便提一句,如果二进制文件中没有足够频繁的0x0A字节,这种方法肯定行不通:“扫描将在EOF、第一个I/O错误或无法适应缓冲区的令牌时不可恢复地停止。”(来自bufio.Scanner)。 - Dave C
谢谢您的反馈。我根据您的建议编辑了我的答案。您有没有一个好的块大小默认值的想法? - Pith
1
4k、8k、64k 或 128k 是从文件中读取“真实”代码的可能选择,但任何示例都可以。通常使用 io.Reader 还需要处理短读取(或使用 io.ReadFull 并处理 io.ErrUnexpectedEOF);os.File 似乎不能保证不会出现短读取。所有这些边界情况开始变得烦人 :(. 然而,在 Stack Overflow 的示例中可能不值得处理这些问题。 - Dave C
读者可以返回部分填充的缓冲区,即使稍后会有更多数据可用,因为文档中说:“如果有一些数据可用但不足 len(p) 字节,则 Read 通常会返回可用的数据而不是等待更多数据。” 因此,在读取时,“f1”和“f2”可能会失去同步。 - mat007
这段代码有几个问题:
  1. 如果Read读取两个文件的字节数不同会发生什么?
  2. 你不应该在循环中分配大型可重用缓冲区。
  3. 根据io文档:“调用者在考虑错误err之前应始终处理返回的n>0字节”。
- cbarrick

11
使用bytes.Equal如何?
package main

import (
"fmt"
"io/ioutil"
"log"
"bytes"
)

func main() {
    // per comment, better to not read an entire file into memory
    // this is simply a trivial example.
    f1, err1 := ioutil.ReadFile("lines1.txt")

    if err1 != nil {
        log.Fatal(err1)
    }

    f2, err2 := ioutil.ReadFile("lines2.txt")

    if err2 != nil {
        log.Fatal(err2)
    }

    fmt.Println(bytes.Equal(f1, f2)) // Per comment, this is significantly more performant.
}

3
这篇文章有两个问题。1. 你鼓励将所有数据加载到内存中。2. DeepEqual 使用反射,速度较慢。更明智的做法是使用 bytes.Equal,如果没有这样的函数,我建议使用 for 循环。 - Stephen Weinberg
根据@StephenWeinberg的更新,1. 很好的观点。2. bytes.Equal确实存在,你是正确的,它比反射快得多,代码片段已更新。 - chaseadamsio
根据@Dave C 3的建议进行了更新。在这个例子中,我有些“懒”(我也没有添加包声明或主函数,所以如果有人复制粘贴代码,会出现错误),因此我处理了错误并更新了任何不能编译和运行的代码。希望这能满足您对我的回答的问题。 - chaseadamsio
1
你没有解决问题1。你仍然完全将两个文件加载到内存中。你已经解决了问题2和3。 - Stephen Weinberg
抱歉,我并不是想暗示我解决了你的问题,而是在评论中明确指出这只是一个微不足道的例子,以防有人复制/粘贴该示例并遇到问题。你有什么替代方案想提出吗?如果你认为我的回答鼓励了不良行为,并且足够糟糕以至于值得被踩,我很乐意删除它。 - chaseadamsio
bytes.Equal() 的作用就是:return string(a) == string(b)。请参见 https://github.com/golang/go/blob/master/src/bytes/bytes.go。 - jftuga

11

我不确定该函数是否做了你想要的事情。根据文档

除非给出 shallow 参数并且为 false,否则具有相同 os.stat() 签名的文件将被视为相等。

你的调用只比较了 os.stat签名,这仅包括:

  1. 文件模式
  2. 修改时间
  3. 大小

你可以从 os.Stat 函数中了解到 Go 语言中的这三个信息。这真的只表示它们是完全相同的文件,或者是指向同一个文件的符号链接,或者是该文件的副本。

如果你想更深入地了解,可以打开两个文件并进行比较(python 版本每次读取 8k)。

你可以使用 crc 或 md5 对两个文件进行散列,但如果长文件开头存在差异,则应尽早停止。我建议每次从每个读取器中读取一定数量的字节,并使用bytes.Compare进行比较。


2

在查看现有答案后,我制作了一个简单的包来比较任意(有限的)io.Reader和文件作为一种方便的方法:https://github.com/hlubek/readercomp

示例:

package main

import (
    "fmt"
    "log"
    "os"

    "github.com/hlubek/readercomp"
)

func main() {
    result, err := readercomp.FilesEqual(os.Args[1], os.Args[2])
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println(result)
}

1

您可以使用类似equalfile的包

主要API:

func CompareFile(path1, path2 string) (bool, error)

Godoc: https://godoc.org/github.com/udhos/equalfile

示例:

package main

import (
    "fmt"
    "os"
    "github.com/udhos/equalfile"
 )

func main() {
    if len(os.Args) != 3 {
        fmt.Printf("usage: equal file1 file2\n")
        os.Exit(2)
    }

    file1 := os.Args[1]
    file2 := os.Args[2]

    equal, err := equalfile.CompareFile(file1, file2)
    if err != nil {
        fmt.Printf("equal: error: %v\n", err)
        os.Exit(3)
    }

    if equal {
        fmt.Println("equal: files match")
        os.Exit(0)
    }

    fmt.Println("equal: files differ")
    os.Exit(1)
}

2
const defaultMaxSize = 10000000000 // 只比较前10^10个字节。 - youfu
3
默认的最大大小是为了防止可能导致无休止比较的无限流,您可以通过使用选项“Options.MaxSize”来覆盖它。如果您有更好的处理无限流的策略,请提交拉取请求。 - Everton

0
这是一个针对比较io Readers进行优化的函数,它处理了读取器可能返回少于缓冲区大小的字节,但尚未到达EOF的情况。它通过不使用io.ReadFullio.ReadAtLeast来进行快速失败优化,这样当可能已经有更多可以比较的数据时,它就不会继续尝试从慢速源读取。
如果从第二个读取器中检索到较少的数据,则会在读取更多数据到缓冲区之前进行比较。
const chunkSize = 64000

func readersEqual(r io.Reader, t io.Reader) (bool, error) {
    rBuf := make([]byte, chunkSize)
    tBuf := make([]byte, chunkSize)

    for {
        readFromR, errR := r.Read(rBuf)
        if errR != nil && !errors.Is(errR, io.EOF) {
            return false, errR
        }

        readFromT := 0
        tCmpBuf := tBuf[:readFromR]

        if readFromR == 0 && errors.Is(errR, io.EOF) {
            readFromT, errT := t.Read(tBuf[:1])
            if readFromT == 0 && errors.Is(errT, io.EOF) {
                return true, nil
            } else {
                return false, errT
            }
        }

        for readFromR > readFromT {
            nextReadFromT, errT := t.Read(tCmpBuf[readFromT:])
            if errT != nil && !errors.Is(errT, io.EOF) {
                return false, errT
            }
            prevReadFromT := readFromT
            readFromT = prevReadFromT + nextReadFromT
            if !bytes.Equal(rBuf[prevReadFromT:readFromT], tCmpBuf[prevReadFromT:readFromT]) {
                return false, nil
            }
            if errors.Is(errR, io.EOF) && errors.Is(errT, io.EOF) {
                return true, nil
            }
            if errors.Is(errR, io.EOF) || errors.Is(errT, io.EOF) {
                return false, nil
            }
        }
    }
}

0

这个程序会逐个比较两个文件,一旦知道它们不同就停止。它只需要标准库函数。

这是对this的改进,通过使用io.ReadFull()解决了mat007christopher提出的短读问题。它还避免了重新分配缓冲区。

package util

import (
    "bytes"
    "io"
    "os"
)

// Decide if two files have the same contents or not.
// chunkSize is the size of the blocks to scan by; pass 0 to get a sensible default.
// *Follows* symlinks.
//
// May return an error if something else goes wrong; in this case, you should ignore the value of 'same'.
//
// derived from https://dev59.com/ml0b5IYBdhLWcg3wLOrP#30038571
// under CC-BY-SA-4.0 by several contributors
func FileCmp(file1, file2 string, chunkSize int) (same bool, err error) {

    if chunkSize == 0 {
        chunkSize = 4 * 1024
    }

    // shortcuts: check file metadata
    stat1, err := os.Stat(file1)
    if err != nil {
        return false, err
    }

    stat2, err := os.Stat(file2)
    if err != nil {
        return false, err
    }

    // are inputs are literally the same file?
    if os.SameFile(stat1, stat2) {
        return true, nil
    }

    // do inputs at least have the same size?
    if stat1.Size() != stat2.Size() {
        return false, nil
    }

    // long way: compare contents
    f1, err := os.Open(file1)
    if err != nil {
        return false, err
    }
    defer f1.Close()

    f2, err := os.Open(file2)
    if err != nil {
        return false, err
    }
    defer f2.Close()

    b1 := make([]byte, chunkSize)
    b2 := make([]byte, chunkSize)
    for {
        n1, err1 := io.ReadFull(f1, b1)
        n2, err2 := io.ReadFull(f2, b2)

        // https://pkg.go.dev/io#Reader
        // > Callers should always process the n > 0 bytes returned
        // > before considering the error err. Doing so correctly
        // > handles I/O errors that happen after reading some bytes
        // > and also both of the allowed EOF behaviors.

        if !bytes.Equal(b1[:n1], b2[:n2]) {
            return false, nil
        }

        if (err1 == io.EOF && err2 == io.EOF) || (err1 == io.ErrUnexpectedEOF && err2 == io.ErrUnexpectedEOF) {
            return true, nil
        }

        // some other error, like a dropped network connection or a bad transfer
        if err1 != nil {
            return false, err1
        }
        if err2 != nil {
            return false, err2
        }
    }
}

让我惊讶的是,这在标准库中竟然找不到。


0
标准的方法是使用stat并使用os.SameFile。
-- https://groups.google.com/g/golang-nuts/c/G-5D6agvz2Q/m/2jV_6j6LBgAJ os.SameFile应该大致与Python的filecmp.cmp(f1, f2)相同(即shallow=true,这意味着它仅比较通过stat获得的文件信息)。
SameFile函数报告fi1和fi2是否描述了相同的文件。例如,在Unix上,这意味着两个底层结构的设备和inode字段是相同的;在其他系统上,决策可能基于路径名。SameFile仅适用于此包的Stat返回的结果。在其他情况下,它返回false。
但是,如果您实际上想要比较文件的内容,则必须自己完成。

0

这是我写的一个 io.Reader。你可以使用 _, err := io.Copy(ioutil.Discard, newCompareReader(a, b)) 来获取错误,如果两个流不共享相同的内容。此实现通过限制不必要的数据复制来优化性能。

package main

import (
    "bytes"
    "errors"
    "fmt"
    "io"
)

type compareReader struct {
    a    io.Reader
    b    io.Reader
    bBuf []byte // need buffer for comparing B's data with one that was read from A
}

func newCompareReader(a, b io.Reader) io.Reader {
    return &compareReader{
        a: a,
        b: b,
    }
}

func (c *compareReader) Read(p []byte) (int, error) {
    if c.bBuf == nil {
        // assuming p's len() stays the same, so we can optimize for both of their buffer
        // sizes to be equal
        c.bBuf = make([]byte, len(p))
    }

    // read only as much data as we can fit in both p and bBuf
    readA, errA := c.a.Read(p[0:min(len(p), len(c.bBuf))])
    if readA > 0 {
        // bBuf is guaranteed to have at least readA space
        if _, errB := io.ReadFull(c.b, c.bBuf[0:readA]); errB != nil { // docs: "EOF only if no bytes were read"
            if errB == io.ErrUnexpectedEOF {
                return readA, errors.New("compareReader: A had more data than B")
            } else {
                return readA, fmt.Errorf("compareReader: read error from B: %w", errB)
            }
        }

        if !bytes.Equal(p[0:readA], c.bBuf[0:readA]) {
            return readA, errors.New("compareReader: bytes not equal")
        }
    }
    if errA == io.EOF {
        // in happy case expecting EOF from B as well. might be extraneous call b/c we might've
        // got it already from the for loop above, but it's easier to check here
        readB, errB := c.b.Read(c.bBuf)
        if readB > 0 {
            return readA, errors.New("compareReader: B had more data than A")
        }

        if errB != io.EOF {
            return readA, fmt.Errorf("compareReader: got EOF from A but not from B: %w", errB)
        }
    }

    return readA, errA
}

-1

这样做应该可以解决问题,并且与其他答案相比应该更节省内存。我查看了 github.com/udhos/equalfile,但对我来说似乎有些过头了。在调用 compare() 函数之前,您应该执行两个os.Stat() 调用并比较文件大小,以便进行早期退出快速路径。

使用此实现的原因是因为如果没有必要,您不希望将两个文件的全部内容存储在内存中。您可以从 A 和 B 中读取一个数量,进行比较,然后继续读取下一个数量,每次从每个文件中加载一个缓冲区,直到完成。只需小心,因为您可能会从 A 中读取 50 字节,然后从 B 中读取 60 字节,因为您的读取可能因某种原因而被阻塞。

此实现假定 Read() 调用不会同时返回 N > 0(读取了一些字节)和 error != nil。这是 os.File 的行为方式,但不是其他 Read 实现的行为方式,例如 net.TCPConn。

import (
  "os"
  "bytes"
  "errors"
)

var errNotSame = errors.New("File contents are different")

func compare(p1, p2 string) error {
    var (
        buf1 [8192]byte
        buf2 [8192]byte
    )

    fh1, err := os.Open(p1)
    if err != nil {
        return err
    }
    defer fh1.Close()

    fh2, err := os.Open(p2)
    if err != nil {
        return err
    }
    defer fh2.Close()

    for {
        n1, err1 := fh1.Read(buf1[:])
        n2, err2 := fh2.Read(buf2[:])

        if err1 == io.EOF && err2 == io.EOF {
            // files are the same!
            return nil
        }
        if err1 == io.EOF || err2 == io.EOF {
            return errNotSame
        }
        if err1 != nil {
            return err1
        }
        if err2 != nil {
            return err2
        }

        // short read on n1
        for n1 < n2 {
            more, err := fh1.Read(buf1[n1:n2])
            if err == io.EOF {
                return errNotSame
            }
            if err != nil {
                return err
            }
            n1 += more
        }
        // short read on n2
        for n2 < n1 {
            more, err := fh2.Read(buf2[n2:n1])
            if err == io.EOF {
                return errNotSame
            }
            if err != nil {
                return err
            }
            n2 += more
        }
        if n1 != n2 {
            // should never happen
            return fmt.Errorf("file compare reads out of sync: %d != %d", n1, n2)
        }

        if bytes.Compare(buf1[:n1], buf2[:n2]) != 0 {
            return errNotSame
        }
    }
}

这段代码乍一看很不错,但由于 io.Reader 的语义问题,存在一些问题,例如:
  1. 如果第一次调用 Read 返回 io.EOF 和非零字节数 - 并不一定意味着文件 <8K 是相同的。允许在遇到 EOF 时返回错误和非零字节数。因此仍需进行比较。
  2. 如果其中一个读取操作返回 io.EOF 而另一个没有,则不能确定文件是否不同,因为其中一个可能是“短读取”。
- Christopher
@Christopher 哈哈,好发现,不过我认为大多数Read()的实现,比如在“os”中的os.File,永远不会返回(n > 0, EOF)。它们会返回(n > 0, nil),然后在下一次调用read时返回(0, EOF)。但是看起来你对基本的"net"包中的TCP连接是正确的——如果我正确理解文档的话,它们可能会返回一些字节和一个错误。 - Yobert
@Christopher 我更新了文本,以确保注意到那个警告。谢谢! - Yobert

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