Cadena de compresión / descompresión con C #

144

Soy novato en .net. Estoy haciendo una cadena de compresión y descompresión en C #. Hay un XML y estoy convirtiendo en cadena y después de eso estoy haciendo compresión y descompresión. No hay ningún error de compilación en mi código, excepto cuando descomprimo mi código y devuelvo mi cadena, solo devuelve la mitad del XML.

A continuación se muestra mi código, corrígeme donde estoy equivocado.

Código:

class Program
{
    public static string Zip(string value)
    {
        //Transform string into byte[]  
        byte[] byteArray = new byte[value.Length];
        int indexBA = 0;
        foreach (char item in value.ToCharArray())
        {
            byteArray[indexBA++] = (byte)item;
        }

        //Prepare for compress
        System.IO.MemoryStream ms = new System.IO.MemoryStream();
        System.IO.Compression.GZipStream sw = new System.IO.Compression.GZipStream(ms, System.IO.Compression.CompressionMode.Compress);

        //Compress
        sw.Write(byteArray, 0, byteArray.Length);
        //Close, DO NOT FLUSH cause bytes will go missing...
        sw.Close();

        //Transform byte[] zip data to string
        byteArray = ms.ToArray();
        System.Text.StringBuilder sB = new System.Text.StringBuilder(byteArray.Length);
        foreach (byte item in byteArray)
        {
            sB.Append((char)item);
        }
        ms.Close();
        sw.Dispose();
        ms.Dispose();
        return sB.ToString();
    }

    public static string UnZip(string value)
    {
        //Transform string into byte[]
        byte[] byteArray = new byte[value.Length];
        int indexBA = 0;
        foreach (char item in value.ToCharArray())
        {
            byteArray[indexBA++] = (byte)item;
        }

        //Prepare for decompress
        System.IO.MemoryStream ms = new System.IO.MemoryStream(byteArray);
        System.IO.Compression.GZipStream sr = new System.IO.Compression.GZipStream(ms,
            System.IO.Compression.CompressionMode.Decompress);

        //Reset variable to collect uncompressed result
        byteArray = new byte[byteArray.Length];

        //Decompress
        int rByte = sr.Read(byteArray, 0, byteArray.Length);

        //Transform byte[] unzip data to string
        System.Text.StringBuilder sB = new System.Text.StringBuilder(rByte);
        //Read the number of bytes GZipStream red and do not a for each bytes in
        //resultByteArray;
        for (int i = 0; i < rByte; i++)
        {
            sB.Append((char)byteArray[i]);
        }
        sr.Close();
        ms.Close();
        sr.Dispose();
        ms.Dispose();
        return sB.ToString();
    }

    static void Main(string[] args)
    {
        XDocument doc = XDocument.Load(@"D:\RSP.xml");
        string val = doc.ToString(SaveOptions.DisableFormatting);
        val = Zip(val);
        val = UnZip(val);
    }
} 

Mi tamaño XML es de 63 KB.

Mohit Kumar
fuente
1
Sospecho que el problema se "arreglará solo" si usa UTF8Encoding (o UTF16 o cualquier otra cosa) y GetBytes / GetString. También simplificará enormemente el código. También recomiendo usar using.
No puede convertir char en byte y viceversa como lo hace (usando un simple elenco). Debe usar una codificación y la misma codificación para la compresión / descompresión. Ver la respuesta de xanatos a continuación.
Simon Mourier
@pst no, no lo hará; estarías usando Encodingel camino equivocado. Necesitas base-64 aquí, según la respuesta de xanatos
Marc Gravell
@Marc Gravell Cierto, se perdió esa parte de la firma / intención. Definitivamente no es mi primera opción de firmas.

Respuestas:

257

El código para comprimir / descomprimir una cadena

public static void CopyTo(Stream src, Stream dest) {
    byte[] bytes = new byte[4096];

    int cnt;

    while ((cnt = src.Read(bytes, 0, bytes.Length)) != 0) {
        dest.Write(bytes, 0, cnt);
    }
}

public static byte[] Zip(string str) {
    var bytes = Encoding.UTF8.GetBytes(str);

    using (var msi = new MemoryStream(bytes))
    using (var mso = new MemoryStream()) {
        using (var gs = new GZipStream(mso, CompressionMode.Compress)) {
            //msi.CopyTo(gs);
            CopyTo(msi, gs);
        }

        return mso.ToArray();
    }
}

public static string Unzip(byte[] bytes) {
    using (var msi = new MemoryStream(bytes))
    using (var mso = new MemoryStream()) {
        using (var gs = new GZipStream(msi, CompressionMode.Decompress)) {
            //gs.CopyTo(mso);
            CopyTo(gs, mso);
        }

        return Encoding.UTF8.GetString(mso.ToArray());
    }
}

static void Main(string[] args) {
    byte[] r1 = Zip("StringStringStringStringStringStringStringStringStringStringStringStringStringString");
    string r2 = Unzip(r1);
}

Recuerde que Zipdevuelve a byte[], mientras que Unzipdevuelve a string. Si quiere una cadena Zip, puede codificarla en Base64 (por ejemplo, usando Convert.ToBase64String(r1)) (¡el resultado Zipes MUY binario! No es algo que pueda imprimir en la pantalla o escribir directamente en un XML)

La versión sugerida es para .NET 2.0, para .NET 4.0 use el MemoryStream.CopyTo.

IMPORTANTE: El contenido comprimido no se puede escribir en la secuencia de salida hasta GZipStreamque sepa que tiene toda la entrada (es decir, para comprimirlo efectivamente necesita todos los datos). Es necesario asegurarse de que Dispose()el GZipStreamantes de inspeccionar el flujo de salida (por ejemplo, mso.ToArray()). Esto se hace con el using() { }bloque de arriba. Tenga en cuenta que GZipStreames el bloque más interno y se accede a los contenidos fuera de él. Lo mismo vale para descomprimir: Dispose()de los GZipStreamantes de intentar acceder a los datos.

xanatos
fuente
Gracias por la respuesta. Cuando uso su código, me da un error de compilación. "CopyTo () no tiene espacio de nombres o referencia de ensamblado". Después de eso, busqué en Google y encontré que CopyTo () forma parte de .NET 4 Framework. Pero estoy trabajando en .net 2.0 y 3.5 framework. Sugiérame. :)
Mohit Kumar
Solo quiero enfatizar que el GZipStream debe eliminarse antes de llamar a ToArray () en la secuencia de salida. Ignoré esa parte, ¡pero hace la diferencia!
Wet Noodles
1
¿Es esta la forma más efectiva de comprimir en .net 4.5?
MonsterMMORPG
1
Tenga en cuenta que esto no funciona (descomprimido cuerdas! = Original) en caso de cadena que contiene pares suplentes por ejemplo string s = "X\uD800Y". Noté que funciona si cambiamos la codificación a UTF7 ... pero con UTF7, ¿estamos seguros de que todos los caracteres se pueden representar?
digEmAll
@digEmAll Diré que no funciona si hay pares sustitutos NO VÁLIDOS (como en su caso). La conversión UTF8 GetByes reemplaza silenciosamente el par sustituto no válido con 0xFFFD.
xanatos
103

De acuerdo con este fragmento, uso este código y funciona bien:

using System;
using System.IO;
using System.IO.Compression;
using System.Text;

namespace CompressString
{
    internal static class StringCompressor
    {
        /// <summary>
        /// Compresses the string.
        /// </summary>
        /// <param name="text">The text.</param>
        /// <returns></returns>
        public static string CompressString(string text)
        {
            byte[] buffer = Encoding.UTF8.GetBytes(text);
            var memoryStream = new MemoryStream();
            using (var gZipStream = new GZipStream(memoryStream, CompressionMode.Compress, true))
            {
                gZipStream.Write(buffer, 0, buffer.Length);
            }

            memoryStream.Position = 0;

            var compressedData = new byte[memoryStream.Length];
            memoryStream.Read(compressedData, 0, compressedData.Length);

            var gZipBuffer = new byte[compressedData.Length + 4];
            Buffer.BlockCopy(compressedData, 0, gZipBuffer, 4, compressedData.Length);
            Buffer.BlockCopy(BitConverter.GetBytes(buffer.Length), 0, gZipBuffer, 0, 4);
            return Convert.ToBase64String(gZipBuffer);
        }

        /// <summary>
        /// Decompresses the string.
        /// </summary>
        /// <param name="compressedText">The compressed text.</param>
        /// <returns></returns>
        public static string DecompressString(string compressedText)
        {
            byte[] gZipBuffer = Convert.FromBase64String(compressedText);
            using (var memoryStream = new MemoryStream())
            {
                int dataLength = BitConverter.ToInt32(gZipBuffer, 0);
                memoryStream.Write(gZipBuffer, 4, gZipBuffer.Length - 4);

                var buffer = new byte[dataLength];

                memoryStream.Position = 0;
                using (var gZipStream = new GZipStream(memoryStream, CompressionMode.Decompress))
                {
                    gZipStream.Read(buffer, 0, buffer.Length);
                }

                return Encoding.UTF8.GetString(buffer);
            }
        }
    }
}
fubo
fuente
2
Solo quería agradecerte por publicar este código. Lo incluí en mi proyecto y funcionó de inmediato sin ningún problema.
BoltBait
3
Sip trabajando fuera de la caja! También me gustó la idea de agregar longitud como primeros cuatro bytes
JustADev
2
Esta es la mejor respuesta. ¡Este debe ser marcado como la respuesta!
Eriawan Kusumawardhono
1
@Matt es como comprimir un archivo .zip - .png ya es un contenido comprimido
fubo
2
La respuesta que está marcada como respuesta no es estable. Esta es la mejor respuesta.
Sari
38

Con la llegada de .NET 4.0 (y superior) con los métodos Stream.CopyTo (), pensé en publicar un enfoque actualizado.

También creo que la siguiente versión es útil como un claro ejemplo de una clase autónoma para comprimir cadenas regulares a cadenas codificadas en Base64, y viceversa:

public static class StringCompression
{
    /// <summary>
    /// Compresses a string and returns a deflate compressed, Base64 encoded string.
    /// </summary>
    /// <param name="uncompressedString">String to compress</param>
    public static string Compress(string uncompressedString)
    {
        byte[] compressedBytes;

        using (var uncompressedStream = new MemoryStream(Encoding.UTF8.GetBytes(uncompressedString)))
        {
            using (var compressedStream = new MemoryStream())
            { 
                // setting the leaveOpen parameter to true to ensure that compressedStream will not be closed when compressorStream is disposed
                // this allows compressorStream to close and flush its buffers to compressedStream and guarantees that compressedStream.ToArray() can be called afterward
                // although MSDN documentation states that ToArray() can be called on a closed MemoryStream, I don't want to rely on that very odd behavior should it ever change
                using (var compressorStream = new DeflateStream(compressedStream, CompressionLevel.Fastest, true))
                {
                    uncompressedStream.CopyTo(compressorStream);
                }

                // call compressedStream.ToArray() after the enclosing DeflateStream has closed and flushed its buffer to compressedStream
                compressedBytes = compressedStream.ToArray();
            }
        }

        return Convert.ToBase64String(compressedBytes);
    }

    /// <summary>
    /// Decompresses a deflate compressed, Base64 encoded string and returns an uncompressed string.
    /// </summary>
    /// <param name="compressedString">String to decompress.</param>
    public static string Decompress(string compressedString)
    {
        byte[] decompressedBytes;

        var compressedStream = new MemoryStream(Convert.FromBase64String(compressedString));

        using (var decompressorStream = new DeflateStream(compressedStream, CompressionMode.Decompress))
        {
            using (var decompressedStream = new MemoryStream())
            {
                decompressorStream.CopyTo(decompressedStream);

                decompressedBytes = decompressedStream.ToArray();
            }
        }

        return Encoding.UTF8.GetString(decompressedBytes);
    }

Aquí hay otro enfoque que utiliza la técnica de métodos de extensión para extender la clase String para agregar compresión y descompresión de cadenas. Puede colocar la clase a continuación en un proyecto existente y luego usarla así:

var uncompressedString = "Hello World!";
var compressedString = uncompressedString.Compress();

y

var decompressedString = compressedString.Decompress();

Esto es:

public static class Extensions
{
    /// <summary>
    /// Compresses a string and returns a deflate compressed, Base64 encoded string.
    /// </summary>
    /// <param name="uncompressedString">String to compress</param>
    public static string Compress(this string uncompressedString)
    {
        byte[] compressedBytes;

        using (var uncompressedStream = new MemoryStream(Encoding.UTF8.GetBytes(uncompressedString)))
        {
            using (var compressedStream = new MemoryStream())
            { 
                // setting the leaveOpen parameter to true to ensure that compressedStream will not be closed when compressorStream is disposed
                // this allows compressorStream to close and flush its buffers to compressedStream and guarantees that compressedStream.ToArray() can be called afterward
                // although MSDN documentation states that ToArray() can be called on a closed MemoryStream, I don't want to rely on that very odd behavior should it ever change
                using (var compressorStream = new DeflateStream(compressedStream, CompressionLevel.Fastest, true))
                {
                    uncompressedStream.CopyTo(compressorStream);
                }

                // call compressedStream.ToArray() after the enclosing DeflateStream has closed and flushed its buffer to compressedStream
                compressedBytes = compressedStream.ToArray();
            }
        }

        return Convert.ToBase64String(compressedBytes);
    }

    /// <summary>
    /// Decompresses a deflate compressed, Base64 encoded string and returns an uncompressed string.
    /// </summary>
    /// <param name="compressedString">String to decompress.</param>
    public static string Decompress(this string compressedString)
    {
        byte[] decompressedBytes;

        var compressedStream = new MemoryStream(Convert.FromBase64String(compressedString));

        using (var decompressorStream = new DeflateStream(compressedStream, CompressionMode.Decompress))
        {
            using (var decompressedStream = new MemoryStream())
            {
                decompressorStream.CopyTo(decompressedStream);

                decompressedBytes = decompressedStream.ToArray();
            }
        }

        return Encoding.UTF8.GetString(decompressedBytes);
    }
Jace
fuente
2
Jace: Creo que te faltan usingdeclaraciones para las instancias de MemoryStream. Y para los desarrolladores # F por ahí: se abstengan de utilizar la palabra clave usepara la instancia compressorStream / decompressorStream, porque tienen que ser eliminados manualmente antes de que ToArray()se vuelve a llamar
knocte
1
¿Será mejor usar GZipStream ya que agrega una validación adicional? ¿Clase GZipStream o DeflateStream?
Michael Freidgeim
2
@Michael Freidgeim No lo creo para comprimir y descomprimir flujos de memoria. Para archivos o transportes poco confiables tiene sentido. Diré que en mi caso de uso particular, la alta velocidad es muy deseable, por lo que cualquier sobrecarga que pueda evitar es mucho mejor.
Jace
Sólido. Tomé mi cadena de 20MB de JSON a 4.5MB. 🎉
James Esh
1
Funciona muy bien, pero debe deshacerse de la secuencia de memoria después de su uso, o poner cada secuencia en uso según lo sugerido por @knocte
Sebastian
8

Esta es una versión actualizada para .NET 4.5 y posterior usando async / await e IEnumerables:

public static class CompressionExtensions
{
    public static async Task<IEnumerable<byte>> Zip(this object obj)
    {
        byte[] bytes = obj.Serialize();

        using (MemoryStream msi = new MemoryStream(bytes))
        using (MemoryStream mso = new MemoryStream())
        {
            using (var gs = new GZipStream(mso, CompressionMode.Compress))
                await msi.CopyToAsync(gs);

            return mso.ToArray().AsEnumerable();
        }
    }

    public static async Task<object> Unzip(this byte[] bytes)
    {
        using (MemoryStream msi = new MemoryStream(bytes))
        using (MemoryStream mso = new MemoryStream())
        {
            using (var gs = new GZipStream(msi, CompressionMode.Decompress))
            {
                // Sync example:
                //gs.CopyTo(mso);

                // Async way (take care of using async keyword on the method definition)
                await gs.CopyToAsync(mso);
            }

            return mso.ToArray().Deserialize();
        }
    }
}

public static class SerializerExtensions
{
    public static byte[] Serialize<T>(this T objectToWrite)
    {
        using (MemoryStream stream = new MemoryStream())
        {
            BinaryFormatter binaryFormatter = new BinaryFormatter();
            binaryFormatter.Serialize(stream, objectToWrite);

            return stream.GetBuffer();
        }
    }

    public static async Task<T> _Deserialize<T>(this byte[] arr)
    {
        using (MemoryStream stream = new MemoryStream())
        {
            BinaryFormatter binaryFormatter = new BinaryFormatter();
            await stream.WriteAsync(arr, 0, arr.Length);
            stream.Position = 0;

            return (T)binaryFormatter.Deserialize(stream);
        }
    }

    public static async Task<object> Deserialize(this byte[] arr)
    {
        object obj = await arr._Deserialize<object>();
        return obj;
    }
}

Con esto puedes serializar todo lo que sea BinaryFormattercompatible, en lugar de solo cadenas.

Editar:

En caso de que necesite cuidarlo Encoding, puede usar Convert.ToBase64String (byte []) ...

¡Eche un vistazo a esta respuesta si necesita un ejemplo!

z3nth10n
fuente
Debe restablecer la posición de Stream antes de deserializar, editó su muestra. Además, sus comentarios XML no están relacionados.
Magnus Johansson
Vale la pena señalar que esto funciona, pero solo para cosas basadas en UTF8. Si agrega, digamos, caracteres suecos como åäö al valor de cadena que está serializando / deserializando, fallará una prueba de ida y vuelta: /
bc3tech
En este caso podrías usar Convert.ToBase64String(byte[]). Por favor, vea esta respuesta ( stackoverflow.com/a/23908465/3286975 ). ¡Espero eso ayude!
z3nth10n
6

Para aquellos que todavía obtienen El número mágico en el encabezado GZip no es correcto. Asegúrate de pasar una transmisión GZip. ERROR y si su cadena se comprimió usando php , deberá hacer algo como:

       public static string decodeDecompress(string originalReceivedSrc) {
        byte[] bytes = Convert.FromBase64String(originalReceivedSrc);

        using (var mem = new MemoryStream()) {
            //the trick is here
            mem.Write(new byte[] { 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00 }, 0, 8);
            mem.Write(bytes, 0, bytes.Length);

            mem.Position = 0;

            using (var gzip = new GZipStream(mem, CompressionMode.Decompress))
            using (var reader = new StreamReader(gzip)) {
                return reader.ReadToEnd();
                }
            }
        }
Choletski
fuente
Recibo esta excepción: Excepción lanzada: 'System.IO.InvalidDataException' en System.dll Información adicional: El CRC en el pie de página de GZip no coincide con el CRC calculado a partir de los datos descomprimidos.
Dainius Kreivys