language-agnostic - salary - stackoverflow
Extrae datos comprimidos zlib del archivo binario en python (2)
Mi empresa utiliza un formato de archivo heredado para datos de electromiografía, que ya no está en producción. Sin embargo, existe cierto interés en mantener la retrocompatibilidad, por lo que estoy estudiando la posibilidad de escribir un lector para ese formato de archivo.
Al analizar un antiguo código fuente muy intrincado escrito en Delphi, el lector / escritor de archivos usa ZLIB, y dentro de un HexEditor parece que hay un encabezado de archivo en ASCII binario (con campos como "Reproductor", "Analizador" fácilmente legible), seguido de una cadena comprimida que contiene datos brutos.
Mi duda es: ¿cómo debo proceder para identificar:
- Si es un flujo comprimido;
- ¿Dónde comienza el flujo comprimido y dónde termina?
De la Wikipedia:
Los datos comprimidos zlib generalmente se escriben con un envoltorio gzip o zlib. El contenedor encapsula los datos brutos de DEFLATE agregando un encabezado y un avance. Esto proporciona identificación de flujo y detección de errores
¿Es esto relevante?
Estaré encantado de publicar más información, pero no sé qué sería más relevante.
Gracias por cualquier pista.
EDITAR: Tengo la aplicación en funcionamiento, y puedo usarla para registrar datos reales de cualquier duración, obteniendo archivos incluso menores de 1 KB si es necesario.
Algunos archivos de muestra:
Una recién creada, sin datastream: https://dl.dropbox.com/u/4849855/Mio_File/HeltonEmpty.mio
Lo mismo que arriba después de guardar un flujo de datos muy corto (1 segundo?): Https://dl.dropbox.com/u/4849855/Mio_File/HeltonFilled.mio
Una diferente, de un paciente llamado "manco" en lugar de "Helton", con un flujo aún más corto (ideal para ver Hex): https://dl.dropbox.com/u/4849855/Mio_File/manco_short.mio
Instrucciones: cada archivo debe ser el archivo de un paciente (una persona). Dentro de estos archivos, se guardan uno o más exámenes, cada examen consta de una o más series temporales. Los archivos provistos contienen solo un examen, con una serie de datos.
Para empezar, ¿por qué no escanear los archivos para todas las transmisiones zip válidas (es lo suficientemente bueno para archivos pequeños y para descubrir el formato)?
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))
Parece que las secuencias de datos contienen datos de coma flotante de doble precisión de 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()
zlib es una envoltura delgada alrededor de datos comprimidos con el algoritmo DEFLATE y se define en 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 | +=====================+---+---+---+---+
Por lo tanto, agrega al menos dos, posiblemente seis bytes antes y 4 bytes con una suma de comprobación ADLER32 después de los datos comprimidos sin procesar DEFLATE.
El primer byte contiene el CMF (método de compresión y banderas), que se divide en CM (método de compresión) (primeros 4 bits) y CINFO (información de compresión) (últimos 4 bits).
A partir de esto, queda bastante claro que, lamentablemente, los dos primeros bytes de una secuencia de zlib pueden variar mucho dependiendo del método de compresión y la configuración que se hayan utilizado.
Afortunadamente, me topé con una publicación de Mark Adler, el autor del algoritmo ADLER32, donde enumera las combinaciones más comunes y menos comunes de esos dos bytes iniciales .
Con eso fuera del camino, veamos cómo podemos usar Python para examinar 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'']
Entonces, los datos zlib creados por el módulo zlib
de Python (usando las opciones predeterminadas) comienzan con 78 9c
. Lo usaremos para crear un script que escriba un formato de archivo personalizado que contenga un preámbulo, algunos datos comprimidos zlib y un pie de página.
Luego, escribimos un segundo script que escanea un archivo para ese patrón de dos bytes, comienza a descomprimir todo lo que sigue como una secuencia zlib y determina dónde termina la secuencia y comienza el pie de página.
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)
Aquí tomamos msg
, lo comprimimos con zlib y lo rodeamos con un encabezado y un pie de página antes de escribirlo en un archivo.
El encabezado y el pie de página son de longitud fija en este ejemplo, pero por supuesto podrían tener longitudes arbitrarias y desconocidas.
Ahora, para la secuencia de comandos que intenta encontrar una secuencia zlib en dicho archivo. Porque para este ejemplo, sabemos exactamente qué marcador esperar, estoy usando solo uno, pero obviamente la lista ZLIB_MARKERS
podría llenarse con todos los marcadores de la publicación mencionada anteriormente.
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."
La idea es esta:
Comience por el principio del archivo y cree una ventana de búsqueda de dos bytes.
Mueva la ventana de búsqueda hacia adelante en incrementos de un byte.
Para cada ventana, compruebe si coincide con cualquiera de los dos marcadores de bytes que definimos.
Si se encuentra una coincidencia, registre la posición de inicio, pare de buscar e intente descomprimir todo lo que sigue.
Ahora, encontrar el final de la secuencia no es tan trivial como buscar dos bytes de marcador. Las secuencias zlib no están terminadas por una secuencia de bytes fija ni su longitud se indica en ninguno de los campos de encabezado. En cambio, termina con una suma de control ADLER32 de cuatro bytes que debe coincidir con los datos hasta este punto.
La forma en que funciona es que la función interna C inflate()
continuamente intenta descomprimir la secuencia tal como la lee, y si se encuentra con una suma de verificación coincidente, lo señala a la persona que llama, indicando que el resto de los datos no está parte de la secuencia zlib más.
En Python, este comportamiento queda al descubierto cuando se utilizan objetos de descompresión en lugar de simplemente llamar a zlib.decompress()
. Llamar a decompress(string)
en un objeto Decompress
descomprimirá una secuencia zlib en string
y devolverá los datos descomprimidos que formaban parte de la secuencia. Todo lo que sigue a la transmisión se almacenará en unused_data
y luego se podrá recuperar.
Esto debería producir el siguiente resultado en un archivo creado con el primer script:
Start of zlib stream found at byte 6
Header: HEADER
Message: foo
Footer: FOOTER
El ejemplo se puede modificar fácilmente para escribir el mensaje sin comprimir en un archivo en lugar de imprimirlo. Luego, puede analizar aún más los datos comprimidos anteriormente zlib e intentar identificar los campos conocidos en los metadatos en el encabezado y pie de página que ha separado.