Cómo comprimir y descomprimir datos en memoria o textos en 128 líneas o menos en C

Publicado el 03 diciembre 2015 por Gaspar Fernández Moreno @gaspar_fm

Si queremos que nuestra información ocupe menos, ésta es una buena forma de conseguirlo. Muy útil cuando queremos transmitir información y reducir los bytes transmitidos, es lo que hacen muchas páginas web actualmente, el servidor nos manda la página comprimida y luego el navegador se encarga de descomprimirla antes de mostrarla y como por lo general se tarda menos en comprimir-enviar-descomprimir que en enviar sin comprimir antes es algo que acelera la navegación web.

También es muy útil, si necesitamos almacenar información a la que no accedemos muy a menudo, pero no vamos a borrar, por ejemplo logs antiguos, archivos javascript y css que no vayan a cambiar (ahora en lugar de comprimir-enviar-descomprimir, sólo enviar-descomprimir, tardamos menos y nuestro servidor no se estresa), o por ejemplo, los comentarios de nuestros posts antiguos (tardaremos más en acceder a ellos, pero no accedemos a menudo y así ocupan menos).

Para este propósito me voy a ayudar de Zlib, una biblioteca de compresión completa y eficiente (ok, las hay mejores como en todo). Además, esta biblioteca la utilizan miles de programas tanto comerciales como no comerciales.

Lo más normal es comprimir y descomprimir ficheros, tenemos el ejemplo del desarrollador aquí. Aunque si la información la tenemos directamente en memoria, en una variable, por ejemplo una cadena de caracteres, como puede ser una web que hayamos estado generando o datos que estemos recibiendo desde la red, a veces queda feo escribirlo en disco para comprimirlo o descomprimirlo y ya veremos qué hacemos con esa información, que tal vez no deba pisar un archivo.

El problema de las cadenas de caracteres, tal y como las conocemos en C, es que utilizan un byte especial como terminador (\0), cosa que los datos binarios no tienen. Por ejemplo, el número 8960 en binario sería (00100011 00000000), vamos, tenemos el byte menos significativo es 0, y es totalmente necesario para nuestro dato por lo que no podemos depender de ese terminador para nuestro propósito.

Bueno, a la hora de comprimir, si sabemos que sólo vamos a comprimir cadenas de caracteres (aunque queremos hacer algo que valga para todo, pero bueno) que tengan su terminador, sí que podemos depender del terminador, pero no valdría por ejemplo para una foto, o cualquier otro dato (porque puede contener bytes a 0 y no se completaría la compresión).

A la hora de descomprimir, sí que no podemos utilizar los \0 bajo ningún concepto, y es que, los datos comprimidos claro que pueden tener un byte a 0 y mejor no ponernos a intentar predecir cuándo nos va a salir un 0 y cuándo no.

Toda esta explicación es para decir que en lugar de un terminador vamos a utilizar una variable char* donde estarán los datos, y otra variable que nos indica el tamaño del dato.

Método largo

Allá va el ejemplo y seguimos comentando, bueno, antes avisar de una cosa. En el título dice 128 líneas y como veis hay más, las 128 líneas salen quitando líneas de comentarios, en blanco y el texto de ejemplo, e incluso se puede quedar en menos (que como podréis observar, es largo):

Bueno, el código es muy parecido al ejemplo, con alguna modificación y si compilamos, debe funcionar del tirón.
Tanto compresión como descompresión van por trozos (chunks), en este caso su tamaño viene dado por una constante (CHUNK) que nos da el tamaño en bytes de cada trozo. Cuando estemos comprimiendo, leeremos bloques de ese tamaño y los comprimiremos (deben ocupar menos, pero como mucho consideramos que ocuparán también CHUNK bytes). Y aunque normalmente en una llamada a deflate() podremos tener los datos comprimidos, está en un bucle por si hay muchos datos de entrada y necesitamos dar alguna pasada más.

Con la descompresión pasa algo parecido, cada CHUNK que leamos puede resultar en más de un CHUNK de salida.

Tanto en un caso como en otro, en lugar de leer de streams (archivos, por ejemplo), lo que hacemos es mover la posición de un puntero, en este caso, gastaremos más memoria si queremos comprimir o descomprimir algo muy grande, pero iremos muy rápido para cosas pequeñas al no necesitar archivos.

Ahora bien, en el ejemplo, el tamaño de la entrada es strlen(texto)+1, ¿por qué? Es para comprimir el texto también con el carácter terminador, es decir para que el \0 último de la cadena también entre en nuestra cadena comprimida, y así nosotros sabremos dónde parar.

Método corto

Venga, podemos hacerlo con menos líneas, y es que zlib trae un par de funciones que nos ayudan a nuestro propósito (luego diré a qué viene el método largo).
Por cierto el texto aquí es mucho más corto, para no sufrir tanto leyendo.

Como veis compress() y uncompress() nos valen para lo que queremos, con algunas diferencias con respecto a los métodos de arriba. compress2() nos permite indicar nivel de compresión.
En este caso, el orden de los argumentos varía, ahora ponemos primero destino y tamaño del destino, luego origen y tamaño del origen por referencia. Por otro lado, el tamaño del destino va por referencia, con el fin de que pueda ser modificado por la función y finalmente devuelva el tamaño exacto del destino (recordemos que de primeras, le tenemos que decir el tamaño máximo que tenemos reservado para la información comprimida).

¿Y para qué me vale el primero?

Está claro que si queremos comprimir o descomprimir, y ya, con el segundo método vale. Pero en el caso de que queramos comprimir o descomprimir a medida que van llegando datos por red, por ejemplo, o a medida que vayamos generando dicha información sí que necesitaremos el primer método.

Y como idea, podíamos comprimir/descomprimir en multihilo y esto puede estar muy chulo, ya que tenemos un bucle y tenemos que repetir una tarea muchas veces, y como sabemos en qué punto de la cadena original tenemos que leer cada vez podíamos hacer que cada thread comprimiera un trozo.

A la hora de comprimir necesitaríamos que alguien se encargara de junta los trozos ya que, de primeras no podríamos saber cuánto ocuparía un trozo comprimido. Para descomprimir pasaría algo parecido, no sabemos cuánto ocupa un CHUNK descomprimido y necesitaríamos juntar los trozos.

Tendríamos que mirar si de verdad nos compensa repartir threads-que trabajen los threads-juntar los trozos tanto en tiempo como en memoria y deberíamos complicar un poco el código para controlar el lanzamiento de threads (por ejemplo, que no haya más de 4 trabajando a la vez, dependiendo de los cores de nuestro ordenador, ya que como las tareas tienen una carga de CPU intensa no hay muchos tiempos muertos).

Foto: Masha Danilova ( Unsplash)