6

我的公司对 Electromiography 数据使用旧文件格式,该文件已不再生产。但是,维护复古兼容性有一些兴趣,因此我正在研究为该文件格式编写阅读器的可能性。

通过分析用 Delphi 编写的非常复杂的前源代码,文件读取器/写入器使用 ZLIB,并且在 HexEditor 内部看起来有一个二进制 ASCII 文件头(“播放器”、“分析器”等字段易于阅读),后跟一个包含原始数据的压缩字符串。

我的疑问是:我应该如何确定:

  • 如果是压缩流;
  • 压缩流从哪里开始,在哪里结束;

来自维基百科:

zlib 压缩数据通常使用 gzip 或 zlib 包装器编写。包装器通过添加标头和尾标来封装原始 DEFLATE 数据。这提供了流识别和错误检测

这相关吗?

我很乐意发布更多信息,但我不知道什么是最相关的。

感谢您的任何提示。

编辑:我有工作应用程序,可以使用它来记录任何时间长度的实际数据,如有必要,可以获取小于 1kB 的文件。


一些示例文件:

一个新创建的,没有数据流:https ://dl.dropbox.com/u/4849855/Mio_File/HeltonEmpty.mio

保存非常短(1 秒?)的数据流后与上述相同:https ://dl.dropbox.com/u/4849855/Mio_File/HeltonFilled.mio

一个不同的,来自名为“manco”而不是“Helton”的患者,流更短(非常适合十六进制查看):https ://dl.dropbox.com/u/4849855/Mio_File/manco_short.mio

说明:每个文件应该是一个病人(一个人)的文件。在这些文件中,保存了一个或多个检查,每个检查由一个或多个时间序列组成。提供的文件仅包含一项检查和一个数据系列。

4

2 回答 2

10

首先,为什么不扫描文件以查找所有有效的 zip 流(对于小文件并找出格式已经足够了):

import zlib
from glob import glob

def zipstreams(filename):
    """Return all zip streams and their positions in file."""
    with open(filename, 'rb') as fh:
        data = fh.read()
    i = 0
    while i < len(data):
        try:
            zo = zlib.decompressobj()
            yield i, zo.decompress(data[i:])
            i += len(data[i:]) - len(zo.unused_data)
        except zlib.error:
            i += 1

for filename in glob('*.mio'):
    print(filename)
    for i, data in zipstreams(filename):
        print (i, len(data))

看起来数据流包含 little-endian 双精度浮点数据:

import numpy
from matplotlib import pyplot

for filename in glob('*.mio'):
    for i, data in zipstreams(filename):
        if data:
            a = numpy.fromstring(data, '<f8')
            pyplot.plot(a[1:])
            pyplot.title(filename + ' - %i' % i)
            pyplot.show()
于 2012-08-28T00:35:03.267 回答
8

zlib 是使用 DEFLATE算法压缩的数据的瘦包装器,并在RFC1950中定义:

  A zlib stream has the following structure:

       0   1
     +---+---+
     |CMF|FLG|   (more-->)
     +---+---+

  (if FLG.FDICT set)

       0   1   2   3
     +---+---+---+---+
     |     DICTID    |   (more-->)
     +---+---+---+---+

     +=====================+---+---+---+---+
     |...compressed data...|    ADLER32    |
     +=====================+---+---+---+---+

因此,它在原始 DEFLATE 压缩数据之前添加至少两个(可能是六个)字节和带有 ADLER32校验和的 4 个字节。

第一个字节包含CMF(压缩方法和标志),分为CM(压缩方法)(前 4 位)和CINFO(压缩信息)(后 4 位)。

从这里可以清楚地看出,不幸的是,zlib 流的前两个字节可能会根据使用的压缩方法和设置而有很大差异。

幸运的是,我偶然发现了 ADLER32 算法的作者 Mark Adler 的一篇文章,他在其中列出了这两个起始字节的最常见和不太常见的组合

顺便说一句,让我们看看如何使用 Python 来检查 zlib:

>>> import zlib
>>> msg = 'foo'
>>> [hex(ord(b)) for b in zlib.compress(msg)]
['0x78', '0x9c', '0x4b', '0xcb', '0xcf', '0x7', '0x0', '0x2', '0x82', '0x1', '0x45']

因此 Python 的zlib模块(使用默认选项)创建的 zlib 数据以 78 9c. 我们将使用它来创建一个脚本,该脚本编写一个自定义文件格式,包含一个序言、一些 zlib 压缩数据和一个页脚。

然后,我们编写第二个脚本,扫描文件以查找该双字节模式,开始解压缩后面的所有内容作为 zlib 流,并确定流的结束位置和页脚的开始位置。

创建.py

import zlib

msg = 'foo'
filename = 'foo.compressed'

compressed_msg = zlib.compress(msg)
data = 'HEADER' + compressed_msg + 'FOOTER'

with open(filename, 'wb') as outfile:
    outfile.write(data)

在这里,我们msg使用 zlib 对其进行压缩,并在将其写入文件之前用页眉和页脚将其包围。

在此示例中,页眉和页脚的长度是固定的,但它们当然可以具有任意的未知长度。

现在对于尝试在此类文件中查找 zlib 流的脚本。因为对于这个例子,我们确切地知道我只使用了一个标记,但显然列表ZLIB_MARKERS可以填充上面提到的帖子中的所有标记。

身份文件

import zlib

ZLIB_MARKERS = ['\x78\x9c']
filename = 'foo.compressed'

infile = open(filename, 'r')
data = infile.read()

pos = 0
found = False

while not found:
    window = data[pos:pos+2]
    for marker in ZLIB_MARKERS:
        if window == marker:
            found = True
            start = pos
            print "Start of zlib stream found at byte %s" % pos
            break
    if pos == len(data):
        break
    pos += 1

if found:
    header = data[:start]

    rest_of_data = data[start:]
    decomp_obj = zlib.decompressobj()
    uncompressed_msg = decomp_obj.decompress(rest_of_data)

    footer = decomp_obj.unused_data

    print "Header: %s" % header
    print "Message: %s" % uncompressed_msg
    print "Footer: %s" % footer

if not found:
    print "Sorry, no zlib streams starting with any of the markers found."

这个想法是这样的:

  • 从文件的开头开始并创建一个两字节的搜索窗口。

  • 以一个字节为增量向前移动搜索窗口。

  • 对于每个窗口,检查它是否匹配我们定义的两个字节标记中的任何一个。

  • 如果找到匹配项,记录起始位置,停止搜索并尝试解压缩后面的所有内容。

现在,找到流的结尾并不像寻找两个标记字节那么简单。zlib 流既不以固定的字节序列终止,也不在任何头字段中指示它们的长度。相反,它由一个四字节的 ADLER32 校验和终止,该校验和必须与此时的数据匹配。

它的工作方式是内部 C 函数inflate()在读取流时不断尝试解压缩流,如果遇到匹配的校验和,则向其调用者发出信号,表明其余数据不属于zlib 流了。

在 Python 中,当使用解压缩对象而不是简单地调用zlib.decompress(). 调用decompress(string)一个Decompress对象将解压缩一个 zlib 流string并返回作为流的一部分的解压缩数据。流之后的所有内容都将被存储unused_data并可以在之后检索。

这应该在使用第一个脚本创建的文件上产生以下输出:

Start of zlib stream found at byte 6
Header: HEADER
Message: foo
Footer: FOOTER

该示例可以很容易地修改为将未压缩的消息写入文件而不是打印它。然后你可以进一步分析以前zlib压缩的数据,并尝试在你分离出来的页眉和页脚中识别元数据中的已知字段。

于 2012-08-27T22:45:59.463 回答