Извлечение сжатых данных zlib из двоичного файла в python

моя компания использует устаревший формат файла для данных электромиографии, который больше не находится в производстве. Тем не менее, существует определенный интерес к поддержанию ретро-совместимости, поэтому я изучаю возможность написания читателя для этого формата файла.

анализируя очень запутанный бывший исходный код, написанный на Delphi, программа чтения/записи файлов использует ZLIB, а внутри HexEditor похоже, что есть заголовок файла в двоичном ASCII (с такими полями, как" Player"," Analyzer " легко читаемый), за которым следует сжатая строка, содержащая необработанные данные.

мое сомнение: как я должен действовать, чтобы идентифицировать:

  • если это сжатый поток;
  • где начинается сжатый поток и где он заканчивается;

Из Википедии:

сжатые данные zlib обычно записываются с помощью gzip или zlib обертка. Оболочка инкапсулирует необработанные данные DEFLATE путем добавления заголовок и трейлер. Это обеспечивает идентификацию потока и ошибку обнаружение

Это имеет значение?

Я буду рад опубликовать больше информации, но я не знаю, что было бы наиболее актуальным.

Спасибо за любую подсказку.

EDIT: у меня есть рабочее приложение, и я могу использовать его для записи фактических данных любой длины времени, получая файлы даже меньше, чем 1kB, если это необходимо.


некоторые файлы примеров:

только что созданный, без поток данных:https://dl.dropbox.com/u/4849855/Mio_File/HeltonEmpty.mio

то же самое, что и выше, после очень короткого (1 секунда?) сохранен поток данных:https://dl.dropbox.com/u/4849855/Mio_File/HeltonFilled.mio

другой, от пациента по имени "manco" вместо "Helton", с еще более коротким потоком (идеально подходит для просмотра Hex): https://dl.dropbox.com/u/4849855/Mio_File/manco_short.mio

инструкции: каждый файл должен быть файл пациента (человека). Внутри этих файлов сохраняются один или несколько экзаменов, каждый из которых состоит из одного или нескольких временных рядов. Предоставленные файлы содержат только один экзамен, с одним рядом данных.

2 ответов


чтобы начать, почему бы не сканировать файлы для всех допустимых потоков 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))

похоже, что потоки данных содержат данные с плавающей запятой с двойной точностью:

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()

zlib-это тонкая оболочка вокруг данных, сжатых с помощью сдуется алгоритм и определяется в 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    |
     +=====================+---+---+---+---+

таким образом, он добавляет по крайней мере два, возможно, шесть байтов до и 4 байта с ADLER32 контрольная сумма после необработанного выкачивания сжатых данных.

первый байт содержит CMF (метод сжатия и флаги), который разделен в CM (сжатия метод) (первые 4 бита) и CINFO (информация о сжатии) (последний 4 бита).

из этого совершенно ясно, что, к сожалению, уже первые два байта потока zlib может сильно отличаться в зависимости от того, какой метод сжатия и были использованы настройки.

перечислены наиболее распространенные и менее распространенные комбинации два старта байты.

С этим из пути, давайте посмотрим, как мы можем использовать 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']

таким образом, данные zlib, созданные Python 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, которая должна соответствовать данным до этого момента.

так получается, что функция ч 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 сжатые данные и попытаться определить известные поля в метаданных в верхний и Нижний колонтитулы вы разделили.