手上有一批设备采集下来的二进制数据,没文档,只有几组不同后缀的文件,要把它们读出来画图。全程靠 hex 编辑器一点点猜,记一下逆向这种私有二进制格式的大概路数,顺便记一个踩得很疼的 off-by-one。

先把头和样点数摸清楚

这种采集文件,基本结构都是「一个文件头 + 后面一大片采样数据」。第一步是估文件头多大。把文件丢进 hex 编辑器,前面一段往往是固定模式、跟后面的「数据区」长得不一样,头一般是个整数(512、1024、2048 这种)。这批文件摸下来头是 1024 字节。

样点数通常藏在文件头的某个偏移里。怎么找——算一道简单除法:(文件总大小 - 头) / 每道字节数 = 道数,而 每道字节数 = 样点数 × 每个样点的字节。如果数据是 16 位(2 字节)存的,那去文件头里找一个值,使得「头之后的字节数 / 2 / 这个值」是个整数、而且这个值本身是合理的样点数(比如 512)。这批文件样点数在头偏移 4 的位置,值是 512。

到这里,最朴素的读法就有了:跳过 1024 头,后面按 int16 一道一道读:

nsamp = 512           # 头里偏移4读出来的样点数
header = 1024 # 头大小
raw = np.frombuffer(data[header:], dtype='<i2') # 小端 int16
ntrace = raw.size // nsamp
traces = raw[:ntrace * nsamp].reshape(ntrace, nsamp)

用 magic 字节区分格式

事情没这么简单,因为这批数据有四种后缀、对应四种格式,得知道手里这份是哪种。靠后缀判断不可靠(后缀能随便改),靠谱的是文件头里的「魔数」——每个格式在固定位置有一段独特的字节,相当于这个格式的身份证。

实测下来这批文件:一种开头是 00 07,一种是 56 04 00 00 04 04 00 00,另两种是 01 0000 00。读的时候先嗅探(sniff)一下这几个字节,命中哪种就走哪种的解析器,后缀只当兜底:

def read_auto(path):
head = open(path, 'rb').read(8)
if head[:2] == b'\x00\x07': return read_kind_a(path)
if head[:4] == b'\x56\x04\x00\x00': return read_kind_b(path)
# 剩下的退回看后缀
...

有意思的是,其中三种格式摸到最后发现是同一个套路——都是 [1024 头][int16 数据],只是 magic 不一样,连样点数偏移都一样(纯属不同厂家巧合撞了布局)。所以这三个的解析逻辑抽到了一个公共函数,各自只是薄薄包一层填自己的 magic。

一个 off-by-one,数据道数多算一条

真正折腾人的是第四种。一开始按老套路把它当「[1024 头][int16]」来读,画出来的图第一道总是一根奇怪的直线、后面整体对不齐,怎么调参数都不对。

后来把它的字节流摊开一格一格看,才发现它根本不是「全局头 + 数据」那种结构,而是定长记录流:整个文件就是一条条固定 1028 字节的记录首尾拼接,没有独立的全局头。而且记录还分类型:

  • 第 0 条是「头记录」,前 4 字节是类型码 1110,后面基本是 0;
  • 后面才是数据道记录,每条开头是类型码 82,再跟 512 个 int16 样点。

之前的读法把第 0 条头记录也当成了数据道,所以道数多算了一条、第一道里混进了那条头记录的残值,图自然对不齐。改完之后,先检测开头是不是 1110、是的话跳过这条头记录,真实道数从第 1 条开始数,图一下就正了。

def read_record_stream(data):
rec = 1028
nrec = len(data) // rec
first = np.frombuffer(data[:4], dtype='<i4')[0]
start = 1 if first == 1110 else 0 # 头记录(type=1110)不算数据道
traces = []
for i in range(start, nrec):
seg = data[i*rec + 4 : i*rec + 4 + 512*2] # 跳过 type 码读 512 样点
traces.append(np.frombuffer(seg, dtype='<i2'))
return np.array(traces)

逆向这种没文档的二进制格式,路数其实挺固定:先估头大小、再从文件大小反推样点数偏移、用 magic 区分同源的几种格式、剩下就是格格里看字节把记录结构猜出来。最后那个 off-by-one 也提醒一件事——别想当然觉得同一批数据都是一种结构,哪怕是同一个采集软件出的,不同后缀可能用的是完全不同的文件模型,按老套路硬套就会多算一条少算一条。