我如何比较golang中的两个文件?

How can I compare two files in golang?

有了 Python 我可以做下一个:

equals = filecmp.cmp(file_old, file_new)

go 语言有内置函数吗?我用谷歌搜索但没有成功。

我可以在 hash/crc32 包中使用一些散列函数,但这比上面的 Python 代码要多。

我不确定该功能是否如您所想。来自 docs

Unless shallow is given and is false, files with identical os.stat() signatures are taken to be equal.

您的调用仅比较 os.statsignature,其中仅包括:

  1. 文件模式
  2. 修改时间
  3. 尺寸

您可以从 os.Stat 函数中学习围棋中的所有这三样东西。这实际上只会表明它们实际上是同一个文件,或者指向同一个文件的符号链接,或者该文件的副本。

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

您可以使用 crc 或 md5 对两个文件进行哈希处理,但如果长文件的开头存在差异,您希望提前停止。我建议一次从每个 reader 中读取一些字节数并与 bytes.Compare.

进行比较

使用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.
}

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

SameFile reports whether fi1 and fi2 describe the same file. For example, on Unix this means that the device and inode fields of the two underlying structures are identical;

否则,如果您想检查文件内容,这里有一个解决方案,可以逐行检查两个文件,避免将整个文件加载到内存中。

第一次尝试: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
        }
    }
}

您可以使用 equalfile

这样的包

主要API:

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

戈多克: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)
}

这是我想出来的 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
}

The standard way is to stat them and use 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 获得的文件信息)。

func SameFile(fi1, fi2 FileInfo) bool

SameFile reports whether fi1 and fi2 describe the same file. For example, on Unix this means that the device and inode fields of the two underlying structures are identical; on other systems the decision may be based on the path names. SameFile only applies to results returned by this package's Stat. It returns false in other cases.

但是如果你真的想比较文件的内容,你必须自己做。

像这样的东西应该可以解决问题,并且与其他答案相比应该更节省内存。我查看了 github.com/udhos/equalfile,这对我来说似乎有点矫枉过正。在此处调用 compare() 之前,您应该执行两次 os.Stat() 调用并比较文件大小以获得早期的快速路径。

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

此实现假设 Read() 调用不会 return N > 0(读取一些字节)同时作为错误!= 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 和文件作为一种方便的方法: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)
}