使用Python,我可以做到以下事情:
equals = filecmp.cmp(file_old, file_new)
Go语言中是否有内置函数可以实现这个功能?我尝试使用谷歌搜索但没有成功。
我可以使用hash/crc32
包中的一些哈希函数,但是相比上面的Python代码,那需要更多的工作。
使用Python,我可以做到以下事情:
equals = filecmp.cmp(file_old, file_new)
Go语言中是否有内置函数可以实现这个功能?我尝试使用谷歌搜索但没有成功。
我可以使用hash/crc32
包中的一些哈希函数,但是相比上面的Python代码,那需要更多的工作。
否则,如果您想检查文件内容,这里有一个解决方案,它逐行检查两个文件,避免将整个文件加载到内存中。SameFile报告fi1和fi2是否描述同一个文件。例如,在Unix上,这意味着两个底层结构的设备和inode字段是相同的;
编辑:按字节块读取文件并在文件大小不同时快速失败。 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
}
}
}
bytes.Equal
(这就是 @captncraig 建议的方法)。 - Dave Cio.Reader
还需要处理短读取(或使用 io.ReadFull
并处理 io.ErrUnexpectedEOF
);os.File
似乎不能保证不会出现短读取。所有这些边界情况开始变得烦人 :(. 然而,在 Stack Overflow 的示例中可能不值得处理这些问题。 - Dave Cbytes.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.
}
bytes.Equal()
的作用就是:return string(a) == string(b)
。请参见 https://github.com/golang/go/blob/master/src/bytes/bytes.go。 - jftuga我不确定该函数是否做了你想要的事情。根据文档,
除非给出 shallow 参数并且为 false,否则具有相同 os.stat() 签名的文件将被视为相等。
你的调用只比较了 os.stat
的 签名,这仅包括:
你可以从 os.Stat
函数中了解到 Go 语言中的这三个信息。这真的只表示它们是完全相同的文件,或者是指向同一个文件的符号链接,或者是该文件的副本。
如果你想更深入地了解,可以打开两个文件并进行比较(python 版本每次读取 8k)。
你可以使用 crc 或 md5 对两个文件进行散列,但如果长文件开头存在差异,则应尽早停止。我建议每次从每个读取器中读取一定数量的字节,并使用bytes.Compare
进行比较。
在查看现有答案后,我制作了一个简单的包来比较任意(有限的)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)
}
您可以使用类似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)
}
io.ReadFull
或io.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
}
}
}
}
这个程序会逐个比较两个文件,一旦知道它们不同就停止。它只需要标准库函数。
这是对this的改进,通过使用io.ReadFull()
解决了mat007和christopher提出的短读问题。它还避免了重新分配缓冲区。
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
}
}
}
让我惊讶的是,这在标准库中竟然找不到。
os.SameFile
应该大致与Python的filecmp.cmp(f1, f2)
相同(即shallow=true
,这意味着它仅比较通过stat获得的文件信息)。这是我写的一个 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
}
这样做应该可以解决问题,并且与其他答案相比应该更节省内存。我查看了 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
}
}
}
Read
返回 io.EOF
和非零字节数 - 并不一定意味着文件 <8K 是相同的。允许在遇到 EOF 时返回错误和非零字节数。因此仍需进行比较。io.EOF
而另一个没有,则不能确定文件是否不同,因为其中一个可能是“短读取”。
filecmp.cmp
的方法和一种检查两个文件是否包含相同字节的方式)。 - Paul Hankin