String de compactação / descompactação com C #

144

Eu sou novato em .net. Estou fazendo a seqüência de compactação e descompactação em c #. Existe um XML e estou convertendo em string e, depois disso, estou fazendo compressão e descompressão. Não há erro de compilação no meu código, exceto quando descompacto meu código e retorno minha string, retornando apenas metade do XML.

Abaixo está o meu código, corrija-me onde estiver errado.

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);
    }
} 

Meu tamanho XML é 63 KB.

Mohit Kumar
fonte
1
Eu suspeito que o problema "se conserte" se estiver usando UTF8Encoding (ou UTF16 ou outros enfeites) e GetBytes / GetString. Isso também simplificará bastante o código. Também recomendo usar using.
Você não pode converter char em byte e o inverso como você faz (usando uma conversão simples). Você precisa usar uma codificação e a mesma codificação para compactação / descompactação. Veja a resposta de xanatos abaixo.
Simon Mourier
@ pst não, não; você usaria Encodingo caminho errado. Você precisa da base 64 aqui, de acordo com a resposta de xanatos
Marc Gravell
@ Marc Gravell True, perdeu essa parte da assinatura / intenção. Definitivamente não é minha primeira escolha de assinaturas.

Respostas:

257

O código para compactar / descompactar uma string

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);
}

Lembre-se que Zipretorna a byte[], enquanto Unzipretorna a string. Se você quer uma string, Zippode codificá-la pelo Base64 (por exemplo, usando Convert.ToBase64String(r1)) (o resultado Zipé MUITO binário! Não é algo que você possa imprimir na tela ou escrever diretamente em um XML)

A versão sugerida é para o .NET 2.0, para o .NET 4.0 use o MemoryStream.CopyTo.

IMPORTANTE: O conteúdo compactado não pode ser gravado no fluxo de saída até GZipStreamque você saiba que possui toda a entrada (ou seja, para compactar efetivamente ele precisa de todos os dados). Você precisa ter certeza de que você Dispose()do GZipStreamantes de inspecionar o fluxo de saída (por exemplo, mso.ToArray()). Isso é feito com o using() { }bloco acima. Observe que este GZipStreamé o bloco mais interno e o conteúdo é acessado fora dele. O mesmo vale para descompactar: Dispose()do GZipStreamantes de tentar acessar os dados.

xanatos
fonte
Obrigado pela resposta. Quando eu uso seu código, ele está me dando um erro de compilação. "CopyTo () não possui referência de namespace ou assembly.". Depois disso, pesquisei no Google e montei essa parte CopyTo () do .NET 4 Framework. Mas estou trabalhando no .net 2.0 e 3.5 framework. Por favor, sugira-me. :)
Mohit Kumar
Eu só quero enfatizar que o GZipStream deve ser descartado antes de chamar ToArray () no fluxo de saída. Eu ignorei isso, mas faz a diferença!
Wet Noodles
1
esta é a maneira mais eficaz de compactar em .net 4.5?
MonsterMMORPG
1
Observe que isso falha (string descompactada! = Original) no caso de string contendo pares substitutos, por exemplo string s = "X\uD800Y". Notei que funciona se mudarmos a codificação para UTF7 ... mas com UTF7 temos certeza de que todos os caracteres podem ser representados?
precisa saber é o seguinte
@digEmAll Vou dizer que não funciona se houver pares substitutos INVÁLIDOS (como no seu caso). A conversão UTF8 GetByes substitui silenciosamente o par substituto inválido por 0xFFFD.
Xanatos
103

de acordo com este trecho, eu uso esse código e está funcionando bem:

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
fonte
2
Eu só queria agradecer por postar este código. Coloquei-o no meu projeto e ele funcionou imediatamente, sem problemas.
BoltBait
3
Sim trabalhando fora da caixa! Eu também gostei da idéia de comprimento acrescentando como primeiros quatro bytes
JustADev
2
Esta é a melhor resposta. Este deve ser marcado como a resposta!
Eriawan Kusumawardhono
1
@ Mat que é como compactar um arquivo .zip - .png já é um conteúdo compactado
fubo
2
A resposta que está marcada como resposta não é estável. Essa é a melhor resposta.
Sari
38

Com o advento do .NET 4.0 (e superior) com os métodos Stream.CopyTo (), pensei em publicar uma abordagem atualizada.

Também acho que a versão abaixo é útil como um exemplo claro de uma classe independente para compactar cadeias regulares para cadeias codificadas em Base64 e vice-versa:

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);
    }

Aqui está outra abordagem usando a técnica de métodos de extensão para estender a classe String para adicionar compactação e descompactação de string. Você pode soltar a classe abaixo em um projeto existente e, em seguida, usar o seguinte:

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

e

var decompressedString = compressedString.Decompress();

A saber:

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
fonte
2
Jace: Acho que falta usinginstruções para as instâncias do MemoryStream. E para os desenvolvedores F # lá fora: se abster de utilizar a palavra-chave usepara a instância compressorStream / decompressorStream, porque eles precisam ser eliminados manualmente antes ToArray()é chamado
knocte
1
Será melhor usar o GZipStream, pois adiciona alguma validação extra? Classe GZipStream ou DeflateStream?
Michael Freidgeim
2
@ Michael Freidgeim Eu não pensaria assim para compactar e descomprimir fluxos de memória. Para arquivos ou transportes não confiáveis, faz sentido. Eu direi que, no meu caso de uso específico, a alta velocidade é muito desejável; portanto, qualquer sobrecarga que eu possa evitar é melhor.
Jace
Sólido. Levei minha string de 20 MB de JSON para 4,5 MB.
James
1
Funciona muito bem, mas você deve descartar o memorystream após o uso, ou colocar cada operação no utilizando como sugerido por @knocte
Sebastian
8

Esta é uma versão atualizada do .NET 4.5 e mais recente usando async / waitit 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;
    }
}

Com isso, você pode serializar tudo o que BinaryFormattersuporta, em vez de apenas strings.

Editar:

Caso você precise se cuidar Encoding, basta usar Convert.ToBase64String (byte []) ...

Dê uma olhada nesta resposta se precisar de um exemplo!

z3nth10n
fonte
Você deve redefinir a posição do fluxo antes de desserializar, editar sua amostra. Além disso, seus comentários XML não são relacionados.
Magnus Johansson
Vale a pena notar que isso funciona, mas apenas para itens baseados em UTF8. Se você adicionar, digamos, em caracteres suecos como AAO para o valor da cadeia que você está serialize / desserializar ele irá falhar um teste de ida e volta: /
bc3tech
Nesse caso, você pode usar Convert.ToBase64String(byte[]). Por favor, consulte esta resposta ( stackoverflow.com/a/23908465/3286975 ). Espero que ajude!
z3nth10n 22/08/19
6

Para quem ainda está recebendo O número mágico no cabeçalho do GZip não está correto. Verifique se você está passando em um fluxo GZip. ERRO e se sua string foi compactada usando php, você precisará fazer 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
fonte
Recebo esta exceção: Exceção lançada: 'System.IO.InvalidDataException' no System.dll Informações adicionais: O CRC no rodapé do GZip não corresponde ao CRC calculado a partir dos dados descompactados.
Dainius Kreivys 02/09