Golang二进制文件读取问题:新手提问

Golang二进制文件读取问题:新手提问 你好,我需要解释一下“重生新手”这个说法,指的是几年前我曾深入学习过Go语言,当时跟着Todd McLeod的课程学习。之后,我把时间都投入到了电子音乐制作上,显然,我的Go语言技能已经生疏了不少。现在,因为音乐相关的事情,我需要重新拾起Go语言的能力,希望能得到大家的帮助!

我遇到一种混合了二进制和文本的文件格式。其中的文本部分看起来可以转换成一个字典(Dictionary),包含一系列键,比如“comment”、“author”等。文本似乎是采用Pascal风格的字符串格式,即在文本前有一个(或多个)字节来表示字符串的长度。

在附图中,标记为“comment”的文本位于0x83位置,这在该类型的所有文件中似乎是固定的(注意0x82处的长度字节——我在想,这是否是覆盖前3个字节的更大数字结构的一部分,从而构成一个64位的长度)。但是,所有后续文本片段的位置会根据内容而变化。所有文件都包含相同的键集合。

Screenshot 2021-05-27 at 00.07.44

我感兴趣并想要提取的文本都包含在前0x0200个字节内。理想情况下,我希望有一段代码能够将这些键/值对提取到一个字典结构中并打印出来。

我并不是想要一个现成的解决方案。相反,如果有人愿意花时间给我一些指导性的提示,引导我走向正确的方向,我将不胜感激!

我已经设法使用 ioutil.ReadFile 将文件读入了一个切片,但不确定接下来该怎么做。我猜我需要使用不同的接口来读取数字数据和文本数据。

func main() {
    fmt.Println("hello world")
}

更多关于Golang二进制文件读取问题:新手提问的实战教程也可以访问 https://www.itying.com/category-94-b0.html

11 回复

@ukhobo 感谢回复!我明天会查看你的代码。我对文件的结构已经有了相当清晰的了解——哪些部分是数字,哪些部分是文本。像 file.ReadAt 这样的方法听起来确实会很有用。

// 代码部分保持原样,不翻译

更多关于Golang二进制文件读取问题:新手提问的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


那么,你已经成功地将文件读取到一个字节切片中。 现在,你需要将该切片解码成某种数据结构,以便以有意义的形式保存数据。

或许可以先定义这个结构——你希望数据以什么格式呈现。 然后,你可以开始编写代码,遍历字节切片并填充这个结构。

是的!谢谢 @amnon!尽管我还不确定它们将如何被填充,但我没有理由不能开始定义和编写用于存储提取信息的数据结构。从心理学的角度来看,这绝对会让人感觉有进展,当然,在这个过程中,答案可能会从我潜意识的深处跳出来,给我一个惊喜 😉

// 代码部分保持原样,不翻译

我已经取得了一些进展,最近主要专注于非编码问题,比如重新熟悉 git 和 GitHub,以及将代码结构组织成 cmdoutput 包。现在有一个问题出现了。我已经成功创建了一个 go.mod 文件,但似乎没有伴随的 go.sum 文件。go.sum 文件是在什么时候创建的呢?

顺便提一下,代码仓库在 https://github.com/carlca/gowig。

从个人角度来看,我有点难以阅读、跟随并立即理解你第二个代码片段的逻辑,老实说,但这可能是因为我看不到你的全部代码。

有时人们会说,Go 代码不应该试图过于巧妙,最好编写易于理解的代码,即使这会使代码更冗长。另一方面,这种想法可能只适用于需要被许多人阅读和维护的代码。我认为,如果这是一个个人项目,那么实现任何对你/未来的你有效的代码模式都是可以的。

你的第一个代码片段在 Playground 中似乎可以编译和运行,没有任何错误:https://play.golang.org/p/3fKyT5gffxH

进展不错,很高兴听到我这个充满漏洞的想法(到目前为止)还算有点用处 :blush:

关于 go.mod / go.sum 方面,go.mod 包含包引用,而 go.sum 包含项目外部包的哈希值计算。如果你在代码中添加了对某个外部包的导入,当你执行 go build 时,应该会看到 god.mod 中填充了该包的信息,并且 go.sum 会自动填充该包的哈希值。如果你没有导入或引用任何外部包,那么 go.mod 将基本保持为空,go.sum 也不会存在,因为没有必要。

func main() {
    fmt.Println("hello world")
}

您认为将 err != nil 的逻辑反转以进一步扁平化代码如何?只要非错误路径没有变得过于缩进,我认为这相当优雅,尽管我可能大错特错了 😉

func readNextSizeAndChunk(f *os.File, streamPos int32) (int32, int32, string, error) {
	var err error
	if streamPos, size, err := readIntChunk(f, streamPos); err == nil {
		if streamPos, size, text, err := readTextChunk(f, streamPos, size); err == nil {
			return streamPos, size, text, nil
		}
	}
	return 0, 0, "", err
}

你可以使用 *File.ReadAt 从文件中读取特定位置的数据块,类似这样:

f, err := os.Open("./dataFile.dat")
if err != nil {
    log.Panic(err)
}

chunk, err := readFromFile(f, 64, 20) // 从文件的第64字节开始读取20个字节

func readFromFile(file *os.File, offset, size int) ([]byte, error) {
    res := make([]byte, size)
    if _, err := file.ReadAt(res, int64(offset)); err != nil {
        return nil, err
    }
    return res, nil
}

……然后,当你获得一小块数据切片后,如果你的结构体字节对齐方式与数据块中的字节内容相匹配,你可以像下面这样将字节反序列化为结构体:

var target someStructType
buf := bytes.NewReader(chunk)
err := binary.Read(buf, binary.LittleEndian, &target)
if err != nil {
    fmt.Println("binary.Read failed:", err)
}

这个想法能够成功的关键在于,你需要充分理解文件中数据的结构,以便能够准确地将文件字节块与结构体对齐。

顺便说一句,我不确定上面的代码是否能编译通过,因为我是直接在这里输入的……但它应该足够接近,能给你一些关于如何着手处理的思路。

@ukhobo,我很高兴地报告,在你的帮助下,我取得了一些进展。这是我的代码…

	streamPos := 0x7f
	chunk, err := readFromFile(f, streamPos, 4) //从文件的 0x7f 字节处读取 4 个字节

	var size int32
	buf := bytes.NewReader(chunk)
	err = binary.Read(buf, binary.BigEndian, &size)
	if err != nil {
		fmt.Println("binary.Read failed:", err)
	}
	fmt.Println(size)

我抓取了 4 个字节的数据:0, 0, 0 和 7 到 chunk 中,然后将其放入 buf。我必须将 sizeint64 改为 int32,以匹配 buf 的 4 字节长度(否则我会遇到意外的 EOF)。最后,我不得不将 LittleEndian 改为 BigEndian。暴露这一点的是,size 的输出结果是 1879048192,也就是 0x70000000!受到这次成功的鼓舞,我打算今晚就到这里,明天重新开始。在我之前作为开发人员的职业生涯中学到的一件事是,在深夜这个时间点,最好见好就收 ;)

到目前为止,谢谢你的帮助。这正是我所希望得到的那种帮助 :)

我又取得了一些进展。以下是我最新代码的一部分……

func ProcessPreset(filename string) error {
	f, err := os.Open(filename)
	if err != nil {
		log.Fatal(err)
	}
	defer f.Close()

	var streamPos int32 = 0x7f
	var size int32
	var text string

	if streamPos, size, err = readIntChunk(f, streamPos); err != nil {
		return err
	}
	fmt.Println("size: ", size)
	fmt.Println("stringPos: ", streamPos)

	if streamPos, size, text, err = readTextChunk(f, streamPos, size); err != nil {
		return err
	}

	fmt.Println("size: ", size)
	fmt.Println("stringPos: ", streamPos)
	fmt.Println("text: ", text)

	streamPos++

	if streamPos, size, err = readIntChunk(f, streamPos); err != nil {
		return err
	}
	fmt.Println("size: ", size)
	fmt.Println("stringPos: ", streamPos)

	if streamPos, size, text, err = readTextChunk(f, streamPos, size); err != nil {
		return err
	}

	fmt.Println("size: ", size)
	fmt.Println("stringPos: ", streamPos)
	fmt.Println("text: ", text)

	return nil
}

如你所见,除了在函数的最开始部分,我选择了使用稍微扁平化的方法来处理错误。我就是想不出如何在不引起编译器报错的情况下做到这一点。有什么想法吗?

根据你的描述,这是一个典型的二进制文件解析问题。你需要处理Pascal风格的字符串(长度前缀+数据)和可能的变长数据结构。以下是具体的实现方案:

package main

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

// 解析Pascal风格字符串:先读长度,再读内容
func readPascalString(r io.Reader) (string, error) {
	var length uint8
	if err := binary.Read(r, binary.LittleEndian, &length); err != nil {
		return "", err
	}
	
	data := make([]byte, length)
	if _, err := io.ReadFull(r, data); err != nil {
		return "", err
	}
	return string(data), nil
}

// 解析可能的64位长度前缀字符串
func readPascalString64(r io.Reader) (string, error) {
	var length uint64
	if err := binary.Read(r, binary.LittleEndian, &length); err != nil {
		return "", err
	}
	
	data := make([]byte, length)
	if _, err := io.ReadFull(r, data); err != nil {
		return "", err
	}
	return string(data), nil
}

func main() {
	// 读取文件
	data, err := os.ReadFile("yourfile.bin")
	if err != nil {
		panic(err)
	}

	// 创建字节读取器
	r := &sliceReader{data: data, pos: 0}
	dict := make(map[string]string)

	// 跳过固定偏移到0x83位置
	r.pos = 0x83

	// 示例:解析comment字段(根据你的格式调整)
	// 假设0x82处是长度字节
	r.pos = 0x82
	comment, err := readPascalString(r)
	if err == nil {
		dict["comment"] = comment
	}

	// 继续解析其他字段...
	// 你需要根据实际格式确定每个字段的位置和长度编码方式

	// 打印结果
	for k, v := range dict {
		fmt.Printf("%s: %s\n", k, v)
	}
}

// 自定义读取器,方便控制位置
type sliceReader struct {
	data []byte
	pos  int
}

func (r *sliceReader) Read(p []byte) (n int, err error) {
	if r.pos >= len(r.data) {
		return 0, io.EOF
	}
	n = copy(p, r.data[r.pos:])
	r.pos += n
	return n, nil
}

// 如果需要读取特定字节序的数字
func (r *sliceReader) ReadUint16() (uint16, error) {
	if r.pos+2 > len(r.data) {
		return 0, io.EOF
	}
	val := binary.LittleEndian.Uint16(r.data[r.pos:])
	r.pos += 2
	return val, nil
}

func (r *sliceReader) ReadUint32() (uint32, error) {
	if r.pos+4 > len(r.data) {
		return 0, io.EOF
	}
	val := binary.LittleEndian.Uint32(r.data[r.pos:])
	r.pos += 4
	return val, nil
}

关键点说明:

  1. 二进制读取:使用binary.Read读取固定长度的数字数据,注意字节序(你的情况可能是Little Endian)

  2. 字符串解析

    • 单字节长度前缀:readPascalString函数
    • 多字节长度前缀:readPascalString64函数(根据实际情况调整类型)
  3. 位置控制

    • 使用自定义的sliceReader可以精确控制读取位置
    • 通过r.pos直接跳转到特定偏移量
  4. 格式分析

    • 你需要先用hex编辑器分析文件结构
    • 确定每个字段的起始位置、长度编码方式(1字节、2字节、4字节还是8字节长度前缀)
    • 确定字段顺序是否固定
  5. 调试建议

// 打印十六进制视图辅助分析
func hexDump(data []byte, offset int) {
	for i := 0; i < len(data); i += 16 {
		fmt.Printf("%04x: ", offset+i)
		for j := 0; j < 16; j++ {
			if i+j < len(data) {
				fmt.Printf("%02x ", data[i+j])
			} else {
				fmt.Print("   ")
			}
		}
		fmt.Println()
	}
}

根据你的截图,0x82处的字节可能是长度,0x83开始是字符串数据。你需要验证长度字节是否包含自身或其他元数据。如果遇到解析问题,先用小段代码测试单个字段的解析逻辑。

回到顶部