如何从具有多个可变长度记录的二进制数据文件中读取和提取数据?
我正在使用Python(3.1或2.6)尝试读取由GPS接收器生成的二进制数据文件。每小时的数据存储在一个单独的文件中,每个文件大约18兆字节。这些数据文件包含多个可变长度的记录,但现在我只需要从其中一条记录中提取数据。
到目前为止,我已经能够部分解码文件的头部信息。我说是“部分”,因为有些数字看起来不太对劲,但大部分是正确的。在这方面花了几天时间(我刚开始学习用Python编程),但没有什么进展,所以我决定寻求帮助。
参考指南给了我消息头的结构和记录的结构。头部信息的长度是可变的,但通常是28个字节。
Header
Field # Field Name Field Type Desc Bytes Offset
1 Sync char Hex 0xAA 1 0
2 Sync char Hex 0x44 1 1
3 Sync char Hex 0x12 1 2
4 Header Lgth uchar Length of header 1 3
5 Message ID ushort Message ID of log 2 4
8 Message Lgth ushort length of message 2 8
11 Time Status enum Quality of GPS time 1 13
12 Week ushort GPS week number 2 14
13 Milliseconds GPSec Time in ms 4 16
Record
Field # Data Bytes Format Units Offset
1 Header 0
2 Number of SV Observations 4 integer n/a H
*For first SV Observation*
3 PRN 4 integer n/a H+4
4 SV Azimuth angle 4 float degrees H+8
5 SV Elevation angle 4 float degrees H+12
6 C/N0 8 double db-Hz H+16
7 Total S4 8 double n/a H+24
...
27 L2 C/N0 8 double db-Hz H+148
28 *For next SV Observation*
SV Observation is satellite - there could be anywhere from 8 to 13
in view.
这是我尝试理解头部信息的代码:
import struct
filename = "100301_110000.nvd"
f = open(filename, "rb")
s = f.read(28)
x, y, z, lgth, msg_id, mtype, port, mlgth, seq, idletime, timestatus, week, millis, recstatus, reserved, version = struct.unpack("<cccBHcBHHBcHLLHH", s)
print(x, y, z, lgth, msg_id, mtype, port, mlgth, seq, idletime, timestatus, week, millis, recstatus, reserved, version)
它输出了:
b'\xaa' b'D' b'\x12' 28 274 b'\x02' 32 1524 0 78 b'\xa0' 1573 126060000 10485760 3545 35358
3个同步字段应该返回xAA x44 x12。(D是x44的ASCII等价物,我想是这样。)
我关注的记录ID是274 - 这个看起来是正确的。
GPS周数返回的是1573 - 这个也看起来是正确的。
毫秒数返回的是126060000 - 我原本期待的是126015000。
我该如何找到标识为274的记录并提取它们呢?(我正在学习Python和编程,所以请考虑到你给有经验的程序员的答案可能对我来说太复杂了。)
3 个回答
除了写一个能正确读取文件的解析器,你可以尝试一种比较简单粗暴的方法……把数据读到内存里,然后用“Sync”这个标记来分割数据。需要注意的是,这样做可能会出现一些错误的结果。不过……
f = open('filename')
data = f.read()
messages = data.split('\xaa\x44\x12')
mymessages = [ msg for msg in messages if len(msg) > 5 and msg[4:5] == '\x12\x01' ]
不过,这其实是一种很不优雅的解决办法……
你需要分块读取数据。这不是因为内存不够用,而是因为解析数据时的要求。18MB的数据在内存中很容易处理。在一台4GB的机器上,这个数据可以在内存中存放200次。
下面是常见的设计模式。
首先只读取前4个字节。使用
struct
来解析这4个字节。确认同步字节并获取头部长度。如果你想要获取剩下的头部信息,知道了长度后就可以继续读取剩下的字节。
如果你不需要头部信息,可以用
seek
跳过它。读取记录的前4个字节,以获取SV观测的数量。使用
struct
来解析这个数量。根据这个数量进行计算,然后读取相应数量的字节,以获取记录中的所有SV观测。
解析这些数据,然后进行你需要做的操作。
我强烈建议在处理数据之前,先从数据中构建
namedtuple
对象。
如果你想要所有的数据,就必须实际读取所有的数据。
“而且不需要一字节一字节地读取18MB的文件?”我不理解这个限制。你必须读取所有字节才能获取所有字节。
你可以利用长度信息来以有意义的块读取字节。但你无法避免读取所有字节。
另外,很多次读取(和跳转)通常是足够快的。你的操作系统会为你进行缓冲,所以不用担心微调读取次数。
只需遵循“读取长度 -- 读取数据”的模式。
18 MB的文件在内存中是可以轻松处理的,所以我会把整个文件一次性读进一个大的字节字符串里,使用这行代码:with open(thefile, 'rb') as f: data = f.read()
。然后我会对这些数据进行“解析”,逐条记录地处理。这种方法更方便,而且可能比从文件中多次小块读取要快(不过这对下面的逻辑没有影响,因为无论如何,“当前关注的数据点”总是向前移动,移动的量是根据每次解包几个字节计算出来的,以找到头部和记录的长度)。
当你知道“记录的开始”位置后,可以通过查看一个字节来确定它的头部长度(这个字节是“字段四”,在头部开始位置的偏移量为3),然后查看消息ID(下一个字段,占用2个字节),来判断这是否是你关心的记录(所以只需要解包这3个字节就可以了)。
不管这是不是你想要的记录,接下来你需要计算记录的长度(要么跳过它,要么获取它的全部内容);为此,你需要计算实际记录数据的开始位置(记录开始位置加上头部长度,再加上记录的下一个字段(头部后面的4个字节)乘以观察值的长度(如果我理解没错的话是32个字节))。
这样一来,你要么就能提取出要传给struct.unpack
的子字符串(当你最终到达你想要的记录时),要么就把头部和记录的总长度加到“记录开始”位置,得到下一个记录的开始位置。