【问题标题】:How to read packed binary data in Go?如何在 Go 中读取打包的二进制数据?
【发布时间】:2016-03-08 19:13:06
【问题描述】:

我正在尝试找出在 Go 中读取由 Python 生成的打包二进制文件的最佳方法,如下所示:

import struct
f = open('tst.bin', 'wb')
fmt = 'iih' #please note this is packed binary: 4byte int, 4byte int, 2byte int
f.write(struct.pack(fmt,4, 185765, 1020))
f.write(struct.pack(fmt,4, 185765, 1022))
f.close()

我一直在修改我在 Github.com 和其他一些来源上看到的一些示例但我似乎无法正常工作(更新显示工作方法)。 在 Go 中做这种事情的惯用方式是什么?这是几种尝试之一

更新和工作

package main

    import (
            "fmt"
            "os"
            "encoding/binary"
            "io"
            )

    func main() {
            fp, err := os.Open("tst.bin")

            if err != nil {
                    panic(err)
            }

            defer fp.Close()

            lineBuf := make([]byte, 10) //4 byte int, 4 byte int, 2 byte int per line

            for true {
                _, err := fp.Read(lineBuf)

                if err == io.EOF{
                    break
                }

                aVal := int32(binary.LittleEndian.Uint32(lineBuf[0:4])) // same as: int32(uint32(b[0]) | uint32(b[1])<<8 | uint32(b[2])<<16 | uint32(b[3])<<24)
                bVal := int32(binary.LittleEndian.Uint32(lineBuf[4:8]))
                cVal := int16(binary.LittleEndian.Uint16(lineBuf[8:10])) //same as: int16(uint32(b[0]) | uint32(b[1])<<8)
                fmt.Println(aVal, bVal, cVal)
            }
    }

【问题讨论】:

  • 不是 Python 开发人员,我只能告诉你这么多......但是快速查看了 struct.pack 方法的文档,你的 fmtiih 意味着“ 32 位整数、32 位整数、16 位短”。你在 Go 中的结构有三个 32 位整数......不是两个 32 位整数和一个 16 位短整数。 Python 文档中还提到了一些填充/对齐,因此您需要考虑到这一点。
  • 谢谢你,Simon - 这让我更仔细地了解了数据类型和大小。 Python i 是 4 个字节, h 是 2 个字节 - 我已经更新了我的代码,现在能够读取数据并获得正确的值。现在我需要弄清楚如何遍历文件。
  • 您可能想查看为您的用例创建的Protocol Buffers。对我来说就像一个魅力,虽然我将它们用于 Golang 到 Java。
  • 感谢马库斯的提示。我会检查一下。我想出了一个解决方案,但肯定想使用最合适的解决方案。我也想写入数据(相同的 4 字节和 2 字节整数),所以我也会查看协议缓冲区。

标签: go binaryfiles


【解决方案1】:

Python 格式字符串为iih,表示两个32 位有符号整数和一个16 位有符号整数(参见docs)。您可以简单地使用您的第一个示例,但将结构更改为:

type binData struct {
    A int32
    B int32
    C int16
}

func main() {
        fp, err := os.Open("tst.bin")

        if err != nil {
                panic(err)
        }

        defer fp.Close()

        for {
            thing := binData{}
            err := binary.Read(fp, binary.LittleEndian, &thing)

            if err == io.EOF{
                break
            }

            fmt.Println(thing.A, thing.B, thing.C)
        }
}

请注意,Python 打包没有明确指定字节序,但如果您确定运行它的系统生成了小字节序二进制文件,这应该可以工作。

编辑:添加了main()函数来解释我的意思。

编辑 2: 大写结构字段,以便 binary.Read 可以写入其中。

【讨论】:

  • Python 格式字符串 iih 表示两个 4 字节整数,后跟一个 2 字节整数,每个文档通过 Python 结构打包二进制文件:docs.python.org/2/library/struct.html 我想出了一个解决方案,发布在更新中,并且我现在正在寻找更合适的替代解决方案。
  • 如果你所有的二进制数据都是这样的结构,我推荐使用 struct 方法,因为你可以创建一个 binData 结构切片,并使用正确的类型轻松访问每个结构的 a、b、c 属性。
  • mjois,我没有看到如何从 4 和 2 字节整数数组转到结构。在我当前的工作示例中,我可以从那些 4 和 2 字节数组转到 int32 和 int16,但我不知道如何直接从这些字节数组到结构。现在,我当然可以将我拥有的内容分配给该结构,然后创建一个包含 binData 结构的结构,但这似乎有很多开销。有什么想法,你会怎么做的例子?
  • 您根本不需要整数数组或字节数组,您只需调用binary.Read 并将结构地址作为第三个参数(您最初将它作为您的第一次尝试)。更好的是,您可以创建一个切片[]binData,遍历输入并在每次迭代时读取一个结构,然后附加到切片。更好的是,如果你事先知道你要阅读 N 个结构,你可以这样做slice := make([]binData, N); binary.Read(fp, binary.LittleEndian, slice)
  • 我刚试过......它奏效了。我确实必须将 a,b,c 更改为 A,B,C 才能将它们导出,所以binary.Read 可以填写它们。我向你保证这是最优雅的方式。
【解决方案2】:

Google's "Protocol Buffers" 是一个很好的便携且相当简单的方法来处理这个问题。虽然现在已经为时已晚,因为你让它工作了,但我花了一些精力来解释和编码它,所以我还是把它发布了。

您可以在https://github.com/mwmahlberg/ProtoBufDemo找到代码

您需要使用您喜欢的方法(pip、OS 包管理、源代码)和Go 为 python 安装协议缓冲区

.proto 文件

.proto 文件对于我们的示例来说相当简单。我叫它data.proto

syntax = "proto2";
package main;

message Demo {
  required uint32  A = 1;
  required uint32 B = 2;

  // A shortcomning: no 16 bit ints
  // We need to make this sure in the applications
  required uint32 C = 3;
}

现在您需要在文件上调用 protoc 并让它为 Python 和 Go 提供代码:

protoc --go_out=. --python_out=. data.proto

生成文件data_pb2.pydata.pb.go。这些文件提供对协议缓冲区数据的特定语言访问。

使用github上的代码,只需要发布即可

go generate

在源目录中。

Python 代码

import data_pb2

def main():

    # We create an instance of the message type "Demo"...
    data = data_pb2.Demo()

    # ...and fill it with data
    data.A = long(5)
    data.B = long(5)
    data.C = long(2015)


    print "* Python writing to file"
    f = open('tst.bin', 'wb')

    # Note that "data.SerializeToString()" counterintuitively
    # writes binary data
    f.write(data.SerializeToString())
    f.close()

    f = open('tst.bin', 'rb')
    read = data_pb2.Demo()
    read.ParseFromString(f.read())
    f.close()

    print "* Python reading from file"
    print "\tDemo.A: %d, Demo.B: %d, Demo.C: %d" %(read.A, read.B, read.C)

if __name__ == '__main__':
    main()

我们导入protoc生成的文件并使用它。这里没有太多魔法。

Go 文件

package main

//go:generate protoc --python_out=. data.proto
//go:generate protoc --go_out=. data.proto
import (
    "fmt"
    "os"

    "github.com/golang/protobuf/proto"
)

func main() {

    // Note that we do not handle any errors for the sake of brevity
    d := Demo{}
    f, _ := os.Open("tst.bin")
    fi, _ := f.Stat()

    // We create a buffer which is big enough to hold the entire message
    b := make([]byte,fi.Size())

    f.Read(b)

    proto.Unmarshal(b, &d)
    fmt.Println("* Go reading from file")

    // Note the explicit pointer dereference, as the fields are pointers to a pointers
    fmt.Printf("\tDemo.A: %d, Demo.B: %d, Demo.C: %d\n",*d.A,*d.B,*d.C)
}

请注意,我们不需要显式导入,因为data.proto 的包是main

结果

生成所需文件并编译源代码后,当您发布时

$ python writer.py && ./ProtoBufDemo

结果是

* Python writing to file
* Python reading from file
    Demo.A: 5, Demo.B: 5, Demo.C: 2015
* Go reading from file
    Demo.A: 5, Demo.B: 5, Demo.C: 2015

请注意,存储库中的 Makefile 提供了生成代码、编译 .go 文件并运行这两个程序的快捷方式:

make run

【讨论】:

  • Markus,非常感谢您提供的示例!我将逐步进行比较。我有时会读取一些高达 80GB 的大型二进制文件,所以这很有帮助。如您所知,在性能足够的情况下正常工作和正常工作之间存在巨大差异!
  • @ChrisTownsend 随意使用 repo 中的代码:它是在 WTFPL 的礼貌版本下获得许可的。
【解决方案3】:

正如我在帖子中提到的,我不确定这是在 Go 中执行此操作的惯用方式,但这是我在经过大量修改和调整几个不同示例后提出的解决方案。再次注意,这会将 4 和 2 字节 int 分别解包到 Go int32 和 int16 中。发布以便有一个有效的答案,以防有人来找。希望有人会发布一种更惯用的方式来实现这一点,但目前,这可行。

package main

    import (
            "fmt"
            "os"
            "encoding/binary"
            "io"
            )

    func main() {
            fp, err := os.Open("tst.bin")

            if err != nil {
                    panic(err)
            }

            defer fp.Close()

            lineBuf := make([]byte, 10) //4 byte int, 4 byte int, 2 byte int per line

            for true {
                _, err := fp.Read(lineBuf)

                if err == io.EOF{
                    break
                }

                aVal := int32(binary.LittleEndian.Uint32(lineBuf[0:4])) // same as: int32(uint32(b[0]) | uint32(b[1])<<8 | uint32(b[2])<<16 | uint32(b[3])<<24)
                bVal := int32(binary.LittleEndian.Uint32(lineBuf[4:8]))
                cVal := int16(binary.LittleEndian.Uint16(lineBuf[8:10])) //same as: int16(uint32(b[0]) | uint32(b[1])<<8)
                fmt.Println(aVal, bVal, cVal)
            }
    }

【讨论】:

    【解决方案4】:

    试试binpacker库。

    示例:

    示例数据:

    buffer := new(bytes.Buffer)
    packer := binpacker.NewPacker(buffer)
    unpacker := binpacker.NewUnpacker(buffer)
    packer.PushByte(0x01)
    packer.PushUint16(math.MaxUint16)
    

    解压:

    var val1 byte
    var val2 uint16
    var err error
    val1, err = unpacker.ShiftByte()
    val2, err = unpacker.ShiftUint16()
    

    或者:

    var val1 byte
    var val2 uint16
    var err error
    unpacker.FetchByte(&val1).FetchUint16(&val2)
    unpacker.Error() // Make sure error is nil
    

    【讨论】:

      猜你喜欢
      • 2013-01-08
      • 2012-08-06
      • 1970-01-01
      • 2015-06-20
      • 2010-09-13
      • 2020-05-09
      • 2017-11-13
      • 1970-01-01
      • 2018-10-11
      相关资源
      最近更新 更多