使用结构体读写头部信息

1 投票
1 回答
1769 浏览
提问于 2025-04-17 16:27

我有一个文件头,里面包含了一些关于文件内容的信息,比如版本信息和其他字符串值。

写入文件其实不太难,看起来很简单:

outfile.write(struct.pack('<s', "myapp-0.0.1"))

但是,当我尝试在另一个方法中读取文件头时:

header_version = struct.unpack('<s', infile.read(struct.calcsize('s')))

我遇到了以下错误:

struct.error: unpack requires a string argument of length 2

我该如何修复这个错误,具体是什么地方出问题了呢?

1 个回答

3

写文件其实并不难,看起来很简单:

但其实没有你想的那么简单。试着看看文件里的内容,或者直接打印出你写的东西:

>>> struct.pack('<s', 'myapp-0.0.1')
'm'

正如文档所解释的:

对于格式字符's'来说,计数被理解为字符串的大小,而不是像其他格式字符那样的重复计数;例如,'10s'表示一个10字节的字符串,而'10c'表示10个字符。如果没有给出计数,默认值是1。

那么,你该怎么处理这个问题呢?

  1. 如果不需要,就别用struct。使用struct的主要原因是为了和C代码交互,直接从缓冲区/文件/套接字等读写C的struct对象,或者是以类似风格编写的二进制格式(比如IP头)。它并不适合用来一般性地序列化Python数据。正如Jon Clements在评论中指出的,如果你只想存储一个字符串,就直接write这个字符串。如果你想存储更复杂的东西,可以考虑使用json模块;如果想要更灵活、更强大,可以使用pickle

  2. 使用固定长度的字符串。如果你的文件格式要求名字必须始终小于或等于255个字符,那就写'<255s'。短字符串会被填充,长字符串会被截断(你可能想加个检查,以便在截断时抛出异常,而不是默默地截断)。

  3. 使用某种方式传递长度信息。最常见的是长度前缀。(你可能可以使用'p''P'格式来帮助,但这真的取决于你想匹配的C布局/二进制格式;通常你需要做一些比较麻烦的事情,比如struct.pack('<h{}s'.format(len(name)), len(name), name)。)


至于你的代码为什么失败,有多个原因。首先,read(11)并不保证能读取11个字符。如果文件里只有1个字符,那你就只能得到这个。其次,你实际上并没有调用read(11),而是调用了read(1),因为struct.calcsize('s')返回1(这个原因应该很明显)。第三,你的代码可能并不是你上面展示的那样,或者infile的文件指针不在正确的位置,因为按这样写的代码会成功读取字符串'm'并解包为'm'。(我这里假设你用的是Python 2.x;3.x会有更多问题,但你根本不会走到这一步。)


对于你的具体用例(“文件头……包含关于内容的信息;版本信息和其他字符串值”),我建议直接用write写字符串,并加上换行符。(如果字符串中可以有嵌入的换行符,你可以用反斜杠转义成\n,使用C风格或RFC822风格的续行,或者用引号括起来等等。)

这样做有很多好处。首先,它使格式非常容易被人类读取(也容易编辑和调试)。而且,虽然有时这会占用更多空间,但一个单字符的终止符至少和长度前缀格式一样高效,甚至可能更高效。最后但同样重要的是,这意味着生成和解析头部的代码非常简单。

在后面的评论中,你澄清了你还想写整数,但这并不会改变什么。一个'i'整数值会占用4个字节,但大多数应用程序写很多小数字,如果把它们当字符串写,只需要1-2个字节(再加1个字节作为终止符/分隔符)。而且如果你写的不是小数字,Python的int可能会大到无法放入C的int中——在这种情况下,struct会默默溢出,只写低32位。

撰写回答