【问题标题】:How can I compare two files in golang?如何比较 golang 中的两个文件?
【发布时间】:2015-06-12 20:19:22
【问题描述】:

使用 Python 我可以执行以下操作:

equals = filecmp.cmp(file_old, file_new)

在 Go 语言中是否有任何内置函数可以做到这一点?我用谷歌搜索但没有成功。

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

【问题讨论】:

  • 你能澄清一下这个问题吗?它要求两个不同的东西(filecmp.cmp 的替换和查看两个文件是否包含相同字节的方法)。
  • 当然,我用 Python 编写了一个 diff 工具(用于自学 Python),它制作补丁比较文件并使用 filecmp.cmp 函数比较新旧文件。现在我正在使用 Go Lang 编写相同的工具,但我找不到像上面这样的函数,因此我的问题是是否要找到一个内置函数来比较文件,但是,如果它不存在,我建议使用一些散列函数或编写一个字节到字节的比较函数。对不起我的英语

标签: go file-comparison filecompare


【解决方案1】:

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

SameFile 报告 fi1 和 fi2 是否描述同一个文件。例如,在 Unix 上,这意味着两个底层结构的 device 和 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
        }
    }
}

【讨论】:

  • 为什么要使用扫描仪?这需要解析字节以查找您不关心的行分隔符。它也可能无法满足您对二进制文件的期望。您可以将“块”读入一对大小合理的缓冲区,然后使用bytes.Equal(这是@captncraig 建议的)。
  • 顺便说一句,如果没有足够频繁的 0x0A 字节,它肯定不适用于二进制文件:“扫描在 EOF、第一个 I/O 错误或 令牌太大而无法放入时不可恢复地停止缓冲区。” (来自bufio.Scanner)。
  • 感谢您的反馈。我编辑了我的答案以遵循您的建议。你知道一个好的块大小默认值吗?
  • 4k、8k、64k 或 128k 可能是从文件中读取“真实”代码的选择,但任何东西都可以作为示例。通常使用io.Reader,您还必须处理短读(或使用io.ReadFull 并处理io.ErrUnexpectedEOF); os.File 似乎不能保证它不会短读。所有的极端情况开始变得烦人:(。然而,在 SO 示例中可能不值得处理。
  • 允许读者返回一个部分填充的缓冲区,即使稍后会有更多数据可用,正如文档所说 If some data is available but not len(p) bytes, Read conventionally returns what is available instead of waiting for more. 所以这里 f1f2 在阅读时可能会不同步。
【解决方案2】:

我不确定该函数是否按照您的想法执行。来自docs

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

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

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

您可以通过 os.Stat 函数在 Go 中学习所有这三件事。这实际上只会表明它们实际上是同一个文件,或指向同一文件的符号链接,或该文件的副本。

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

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

【讨论】:

    【解决方案3】:

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

    【讨论】:

    • 这篇文章有两个问题。 1.您鼓励将所有数据加载到内存中。 2. DeepEqual 使用反射,速度慢。使用 bytes.Equal 更有意义,如果不存在这样的函数,我会推荐一个 for 循环。
    • 根据@StephenWeinberg 更新,1. 好点。 2. bytes.Equal 确实存在,你是对的,它比反映、更新的代码 sn-p 快得多。
    • 根据@Dave C 3 更新。在这个例子中我很“懒惰”(我也没有包声明或主函数,所以如果有人复制粘贴,这段代码会出错) ,所以我处理了错误并更新了任何不会编译和运行的代码。希望我的回答能满足你的问题。
    • 您没有解决问题 1。您仍在将两个文件完全加载到内存中。你确实解决了问题 2 和 3。
    • 对不起,我并不是要暗示我解决了您的问题,而是在 cmets 中明确表示这只是一个简单的示例,说明有人复制/粘贴示例并遇到问题。您想提出什么替代解决方案?如果您认为需要删除我的回复,我很乐意删除它,因为它鼓励不良行为,而且已经够糟糕了,值得投反对票。
    【解决方案4】:

    你可以使用像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)
    }
    

    【讨论】:

    • const defaultMaxSize = 10000000000 // Only the first 10^10 bytes are compared. 到底是什么
    • 这个默认的最大尺寸是为了防止可能会导致无休止比较的无限流。您可以使用选项“Options.MaxSize”覆盖它。如果您有更好的处理无限流的策略,请打开拉取请求。
    【解决方案5】:

    在检查了现有答案后,我创建了一个简单的包来比较任意(有限)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)
    }
    

    【讨论】:

      【解决方案6】:

      这是一个 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
      }
      

      【讨论】:

        【解决方案7】:

        标准方法是统计它们并使用 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 报告 fi1 和 fi2 是否描述同一个文件。例如,在 Unix 上,这意味着两个底层结构的 device 和 inode 字段是相同的;在其他系统上,该决定可能基于路径名。 SameFile 仅适用于此包的 Stat 返回的结果。在其他情况下返回 false。

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

        【讨论】:

          【解决方案8】:

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

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

          此实现假定 Read() 调用不会在错误 != nil 的同时返回 N > 0(读取一些字节)。这是 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 啊哈!很好,尽管我认为 Read() 的大多数实现,例如 os.File 的“os”中的实现永远不会返回(n > 0,EOF)。它们改为返回(n > 0,nil),然后在下一次调用读取时返回(0,EOF)。但看起来你对基本“net”包中的 TCP 连接是正确的——如果我正确理解文档,这些可能会返回一些字节和错误。
          • @Christopher 我更新了文本以确保注意该警告。谢谢!
          猜你喜欢
          • 2015-12-02
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2018-09-26
          • 1970-01-01
          • 2015-06-20
          • 1970-01-01
          相关资源
          最近更新 更多