Stringa di compressione / decompressione con C #


144

Sono un novizio in .net. Sto facendo la stringa di compressione e decompressione in C #. C'è un XML e sto convertendo in stringa e dopo sto facendo compressione e decompressione. Non c'è nessun errore di compilazione nel mio codice, tranne quando decomprimo il mio codice e restituisco la mia stringa, restituendo solo metà dell'XML.

Di seguito è riportato il mio codice, per favore correggimi dove sbaglio.

Codice:

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

La mia dimensione XML è di 63 KB.


1
Sospetto che il problema si "risolverà da solo" se si utilizza UTF8Encoding (o UTF16 o whatnot) e GetBytes / GetString. Inoltre semplificherà notevolmente il codice. Consiglio anche di usare using.

Non puoi convertire char in byte e viceversa come fai tu (usando un semplice cast). È necessario utilizzare una codifica e la stessa codifica per la compressione / decompressione. Vedi la risposta di xanatos di seguito.
Simon Mourier,

@pst no non lo farà; staresti usando Encodingnel modo sbagliato. Hai bisogno di base 64 qui, secondo la risposta di
xanatos

@Marc Gravell Vero, mancata quella parte della firma / intento. Sicuramente non è la mia prima scelta di firme.

Risposte:


257

Il codice per comprimere / decomprimere una stringa

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

Ricorda che Ziprestituisce a byte[], mentre Unziprestituisce a string. Se vuoi una stringa da Zipte, Base64 può codificarla (ad esempio usando Convert.ToBase64String(r1)) (il risultato Zipè MOLTO binario! Non è qualcosa che puoi stampare sullo schermo o scrivere direttamente in un XML)

La versione suggerita è per .NET 2.0, per .NET 4.0 utilizzare il MemoryStream.CopyTo.

IMPORTANTE: i contenuti compressi non possono essere scritti nel flusso di output fino a quando non GZipStreamsanno che ha tutti gli input (ovvero, per comprimere efficacemente sono necessari tutti i dati). È necessario assicurarsi che Dispose()la GZipStreamprima di controllare il flusso di uscita (ad esempio, mso.ToArray()). Questo viene fatto con il using() { }blocco sopra. Si noti che GZipStreamè il blocco più interno e ai contenuti si accede al di fuori di esso. Lo stesso vale per la decompressione: Dispose()dei GZipStreamprima di tentare di accedere ai dati.


Grazie per la risposta. Quando uso il codice, mi viene visualizzato un errore di compilazione. "CopyTo () non ha spazio dei nomi o riferimenti di assembly". Dopo di che ho cercato su Google e ho trovato quella parte di CopyTo () di .NET 4 Framework. Ma sto lavorando su .net 2.0 e 3.5 framework. Per favore, suggeriscimi. :)
Mohit Kumar,

Voglio solo sottolineare che GZipStream deve essere eliminato prima di chiamare ToArray () sul flusso di output. Ho ignorato quel poco, ma fa la differenza!
Wet Noodles,

1
è questo il modo più efficace di zippare su .net 4.5?
MonsterMMORPG,

1
Si noti che ciò non riesce (unzipped-string! = Original) in caso di stringa contenente coppie surrogate, ad es string s = "X\uD800Y". Ho notato che funziona se cambiamo la codifica in UTF7 ... ma con UTF7 siamo sicuri che tutti i caratteri possano essere rappresentati?
digEmAll

@digEmAll Dirò che non funziona se ci sono coppie surrogate INVALID (come nel tuo caso). La conversione GetByes UTF8 sostituisce silenziosamente la coppia di surrogati non valida con 0xFFFD.
xanatos,

103

secondo questo frammento uso questo codice e funziona benissimo:

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

2
Volevo solo ringraziarti per aver pubblicato questo codice. L'ho inserito nel mio progetto e ha funzionato subito senza problemi.
BoltBait,

3
Sì, lavorando fuori dagli schemi! Mi è piaciuta anche l'idea di aggiungere lunghezza come i primi quattro byte
JustADev

2
Questa è la risposta migliore Questo dovrebbe essere contrassegnato come la risposta!
Eriawan Kusumawardhono,

1
@Matt che è come comprimere un file .zip - .png è già un contenuto compresso
fubo

2
La risposta contrassegnata come risposta non è stabile. Questa è la risposta migliore.
Sari,

38

Con l'avvento di .NET 4.0 (e versioni successive) con i metodi Stream.CopyTo (), ho pensato di pubblicare un approccio aggiornato.

Penso anche che la versione seguente sia utile come chiaro esempio di una classe autonoma per comprimere stringhe regolari in stringhe con codifica Base64 e 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);
    }

Ecco un altro approccio che utilizza la tecnica dei metodi di estensione per estendere la classe String per aggiungere compressione e decompressione delle stringhe. È possibile rilasciare la classe seguente in un progetto esistente e quindi utilizzare in questo modo:

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

e

var decompressedString = compressedString.Decompress();

Per dire:

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

2
Jace: Penso che manchi le usingdichiarazioni per le istanze di MemoryStream. E per gli sviluppatori F # là fuori: astenersi useToArray()
dall'usare

1
Sarà meglio usare GZipStream in quanto aggiunge qualche ulteriore convalida? Classe GZipStream o DeflateStream?
Michael Freidgeim,

2
@Michael Freidgeim Non la penso così per comprimere e decomprimere i flussi di memoria. Per file o trasporti inaffidabili ha senso. Dirò che nel mio particolare caso d'uso l'alta velocità è molto desiderabile, quindi qualsiasi sovraccarico che posso evitare è tanto meglio.
Jace,

Solido. Ho portato la mia stringa di JSON da 20 MB a 4,5 MB. 🎉
James Esh,

1
Funziona alla grande, ma dovresti eliminare il memorystream dopo l'uso o mettere tutti gli stream in uso come suggerito da @knocte
Sebastian

8

Questa è una versione aggiornata per .NET 4.5 e successive utilizzando 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 questo puoi serializzare tutto ciò che BinaryFormattersupporta, anziché solo le stringhe.

Modificare:

Nel caso, devi prenderti cura di Encodingte, potresti semplicemente usare Convert.ToBase64String (byte []) ...

Dai un'occhiata a questa risposta se hai bisogno di un esempio!


È necessario ripristinare la posizione dello Stream prima di DeSerializing, modificato il campione. Inoltre, i tuoi commenti XML non sono correlati.
Magnus Johansson,

Vale la pena notare che funziona, ma solo per cose basate su UTF8. Se aggiungi, diciamo, caratteri svedesi come åäö al valore di stringa che stai serializzando / deserializzando fallirà un test di andata e ritorno: /
bc3tech

In questo caso potresti usare Convert.ToBase64String(byte[]). Per favore, vedi questa risposta ( stackoverflow.com/a/23908465/3286975 ). Spero che sia d'aiuto!
z3nth10n,

6

Per coloro che continuano a ottenere Il numero magico nell'intestazione GZip non è corretto. Assicurati di passare in un flusso GZip. ERRORE e se la tua stringa è stata zippata usando php dovrai fare qualcosa del tipo:

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

Ottengo questa eccezione: Eccezione generata: 'System.IO.InvalidDataException' in System.dll Ulteriori informazioni: Il CRC nel piè di pagina di GZip non corrisponde al CRC calcolato dai dati decompressi.
Dainius Kreivys,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.