二进制数据文件头应包含什么?

10

我有一个模拟程序,需要读取我们创建的大型二进制数据文件(10至100 GB)。出于速度考虑,我们使用二进制文件格式。这些文件是系统相关的,每次在运行时从文本文件进行转换,所以我不关心可移植性。目前,这些文件是由多个POD结构体的实例组成,并使用fwrite进行写入。

我需要改变结构体内容,因此我想添加一个头部,其中包含一个文件版本号,在每次结构体更改时将其递增。既然我正在进行这个操作,我想加入一些其他信息。我考虑加入结构体大小、字节顺序,以及创建二进制文件的代码svn版本号。还有其他什么有用的信息可以添加吗?

12个回答

15

根据我的经验,对所需数据进行猜测通常是浪费时间的。重要的是以可扩展的方式组织你的元数据。对于XML文件来说,这很简单,但对于二进制文件需要多思考一些。

我倾向于将元数据存储在文件结尾而不是开头的结构中。这有两个优点:

  • 截断/未结束的文件容易被检测到。
  • 元数据页脚经常可以附加到现有文件中,而不影响其读取代码。

我使用的最简单的元数据页脚看起来像这样:

struct MetadataFooter{
  char[40] creatorVersion;
  char[40] creatorApplication;
  .. or whatever
} 

struct FileFooter
{
  int64 metadataFooterSize;  // = sizeof(MetadataFooter)
  char[10] magicString;   // a unique identifier for the format: maybe "MYFILEFMT"
};

在原始数据之后,先写入元数据页脚,然后再写文件页脚。

读取文件时,将指针定位到末尾 - sizeof(FileFooter)。 读取页脚并验证magicString。 然后,根据metadataFooterSize进行回溯,并读取元数据。 根据文件中包含的页脚大小,您可以对缺失字段使用默认值。

正如KeithB所指出的,甚至可以使用这种技术将元数据存储为XML字符串,既具有完全可扩展的元数据的优点,又具备二进制数据的紧凑性和速度。


2
这是一个有趣的方法,我之前没有想过。你甚至可以将MetadataFooter作为XML字符串,获得二进制数据文件的所有好处,并且仍然具有易于扩展的存储元数据方案。 - KeithB
@KeithB:啊!那是一种我没有考虑过的技巧。我喜欢它;-) - Roddy

7
对于大二进制文件,我会严肃考虑使用HDF5(搜索Google)。即使您不想采用它,它也可能指导您在设计自己的格式时朝着一些有用的方向。

我听说过HDF5,但从未有时间去了解它。它似乎是科学计算的标准。对于我们所需的数据(只是一个结构体的多个副本),它可能过于复杂了。如果需要处理更复杂的数据,我会考虑使用它。 - KeithB
我已经用它处理了一些简单的东西,它非常有用。(虽然我希望它们有一个更简单的Java绑定。)他们提供标准工具+MATLAB可以解析它。 - Jason S

4

对于大型二进制文件,除了版本号外,我倾向于添加记录计数和CRC。原因是大型二进制文件比较容易在传输或者存储过程中出现截断或损坏的情况。最近我惊奇地发现Windows并没有很好地处理这个问题。我使用资源管理器将大约2TB的数据复制到附加的NAS设备上,但是每次复制都会有2-3个文件损坏(没有完全复制)。


记录计数是另一种好的检查方法。数据移动并不频繁,因此 CRC 可能有些过头了。但是,当文件被写入时很容易计算,不需要每次读取文件时都进行检查。我们可以为此编写一个独立的实用程序。 - KeithB
我在文件结尾存储元数据的技巧是解决这个问题的快速方法。 - Roddy
我倾向于在文件开始处而不是结尾处存储元数据,因为根据我的经验,文件的结尾更容易丢失。如果情况确实如此,将元数据放在开头会提高局部恢复损坏数据的机会。 - SmacL
此外,将元数据放置在文件末尾可能会使向文件追加内容变得更慢或更复杂。 - SmacL

3

如果您以后会将其他结构写入二进制文件,那么为文件类型指定标识符将非常有用。 也许这可以是一个短字符串,这样您就可以通过查看文件(通过十六进制编辑器)来确定它包含的内容。


3
如果文件很大,我建议在文件开头保留一块健康的空间(64K?),并以XML格式将元数据放在那里,然后跟随一个文件结束字符(DOS / Windows为Ctrl-Z,Unix为Ctrl-D)。这样,您可以使用各种工具集轻松检查和解析元数据。
否则,我会选择其他人已经提到的方法:文件创建时间戳,标识符表明是在哪台机器上创建的,基本上任何其他你能想到的用于诊断目的的信息。理想情况下,您应该包括结构格式本身的定义。如果您经常更改结构,则维护正确版本的代码以读取旧数据文件的各种格式非常麻烦。
正如@highpercomp所提到的,HDF5的一个巨大优势是,只要您有某些名称和数据类型的约定,就不必担心结构格式的更改。结构名称和数据类型都存储在文件本身中,因此您可以将C代码粉碎成渣,也可以从HDF5文件中检索数据。它让您更少地关注数据的格式,而更多地关注数据的结构,即我不关心字节序列,这是HDF5的问题,但我确实关心字段名称之类的内容。
我喜欢HDF5的另一个原因是您可以选择使用压缩,这需要很少的时间,并且如果数据缓慢变化或除了一些错误的有趣点之外基本相同,则可以在存储空间上获得巨大的优势。

2
@rstevens说:“文件类型的标识符”是个明智的建议。一般地,这被称为魔数,并且在文件中不是一种侮辱性的术语(与在代码中的情况不同)。基本上,它是一个数字 - 通常至少4个字节,我通常确保其中至少有一个字节不是ASCII码 - 您可以使用它来验证文件是否为您期望的类型,并且极小的可能会混淆。您还可以在/etc/magic(或本地等效)中编写规则,以报告包含您魔数的文件是您特殊的文件类型。
您应该包括文件格式版本号。但是,我建议不要使用代码的SVN编号,因为当文件格式不同时,您的代码可能会更改。

我原本打算在文件创建者中包含文件格式版本和SVN版本,这样如果我们以后发现某些版本的创建者存在错误,就很容易找出哪些数据文件受到影响。 - KeithB
好的 - 这也可以(事实上,这是一个相当不错的想法)。但关键点在于文件格式有一个版本号,该版本号与编写给定格式文件的代码的版本号是分开的。 - Jonathan Leffler

1
根据我在电信设备配置和固件升级方面的经验,你只需要在版本信息的开始 (这很重要) 使用几个预定义字节,它们从版本号(标头的固定部分)开始。标头的其余部分是可选的,通过指示适当的版本,您可以始终显示如何处理它。这里的重要事情是,最好将标头的“变量”部分放在文件末尾。如果您计划在不修改文件内容本身的情况下对标头进行操作,这也会简化“附加”操作,应重新计算可变标头部分。
固定大小标头(在开头处)的良好特性:
- 通用“长度”字段(包括标头)。 - 类似CRC32的东西(包括标头)。
好吧,对于变量部分,XML或某些相当可扩展的格式在标头中是个好主意,但真的需要吗?我有很多ASN编码的经验...在大多数情况下,它的使用都过度了。
嗯,也许当你看到像RFC 2126 (第4.3章)中所描述的TPKT格式之类的东西时,你会有额外的理解。

1

除了架构版本控制所需的任何信息外,还应添加可能在故障排除时有价值的详细信息。例如:

  • 文件创建和更新的时间戳(如果适用)。
  • 来自构建的版本字符串(理想情况下,您拥有一个版本字符串,该字符串在每个“官方”构建中自动递增...这与文件架构版本不同)。
  • 创建文件的系统名称,以及可能与您的应用程序相关的其他统计信息

我们发现这非常有用,一方面可以获得我们否则必须要求客户提供的信息,另一方面可以获得正确的信息-令人惊讶的是,有多少客户报告他们正在运行与数据声称的软件版本不同的版本!


1
你可以考虑在头文件中固定位置放置一个文件偏移量,以告诉你实际数据在文件中的起始位置。这样可以在需要时更改头文件的大小。
在某些情况下,我将值0x12345678放入头文件中,以便检测文件格式是否与处理它的机器的字节序匹配。

0

我的变体结合了Roddy和Jason S的方法。

总之 - 在文件末尾放置格式化文本元数据,并在其他地方存储确定其长度的方式。

1)在文件开头放置一个长度字段,以便您知道文件末尾的元数据的长度,而不是假定固定长度。这样,要获取元数据,只需读取该固定长度的初始字段,然后从文件末尾获取元数据块。

2)使用XML或YAML或JSON进行元数据。如果元数据附加在末尾,则特别有用/安全,因为读取文件的任何人都不会自动认为它全部是XML,只因为它以XML开头。

这种方法的唯一缺点是当元数据增长时,您必须更新文件头和尾部,但很可能其他部分已经被更新。如果只是更新最后访问日期等琐事,则元数据长度不会改变,因此只需要原地更新即可。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接