从python中的二进制文件中提取zlib压缩数据

时间:2012-08-27 18:29:29

标签: language-agnostic zlib binaryfiles compression

我的公司使用遗留文件格式的Electromiography数据,不再生产。但是,有一些兴趣保持复古兼容性,所以我正在研究为该文件格式编写阅读器的可能性。

通过分析用Delphi编写的非常复杂的前源代码,文件读取器/写入器使用ZLIB,并且在HexEditor内部看起来像二进制ASCII中的文件头(像“Player”,“Analyzer”这样的字段很容易可读),后跟包含原始数据的压缩字符串。

我的疑问是:我应该如何进行识别:

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

来自维基百科:

  

zlib压缩数据通常使用gzip或zlib编写   包装。包装器通过添加a来封装原始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

说明:每个文件应该是患者(一个人)的文件。在这些文件中,保存一个或多个考试,每个考试由一个或多个时间序列组成。提供的文件只包含一个考试,其中包含一个数据系列。

2 个答案:

答案 0 :(得分:9)

首先,为什么不扫描所有有效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()

答案 1 :(得分: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    |
     +=====================+---+---+---+---+

所以它至少增加了两个,可能是6个字节,4个字节用 原始DEFLATE压缩数据后的ADLER32校验和。

第一个字节包含 CMF (压缩方法和标志),它是分开的 进入 CM (压缩方法)(前4位)和 CINFO (压缩信息)(最后 4位)。

从这一点可以很清楚,遗憾的是已经是前两个字节了 根据压缩方法和方法,zlib流的变化很大 设置已被使用。

幸运的是,我偶然发现了ADLER32的作者马克阿德勒的一篇文章 算法,他lists the most common and less common combinations of those two starting bytes

有了这个,让我们看一下如何使用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流并计算出来 流结束并且页脚开始的地方。

<强> create.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可以填充所有标记 上面提到的帖子。

<强> ident.py

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 将解压缩string中的zlib流并返回作为流的一部分的解压缩数据。流后面的所有内容都将存储在unused_data中,并且可以存储在Start of zlib stream found at byte 6 Header: HEADER Message: foo Footer: FOOTER 中 之后检索。

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

{{1}}

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