Chaîne de compression / décompression avec C #

144

Je suis novice en .net. Je fais des chaînes de compression et de décompression en C #. Il y a un XML et je convertis en chaîne et après cela je fais de la compression et de la décompression.Il n'y a pas d'erreur de compilation dans mon code sauf lorsque je décompresse mon code et renvoie ma chaîne, il ne renvoie que la moitié du XML.

Ci-dessous mon code, veuillez me corriger là où je me trompe.

Code:

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

Ma taille XML est de 63 Ko.

Mohit Kumar
la source
1
Je soupçonne que le problème "se résoudra " si vous utilisez UTF8Encoding (ou UTF16 ou autre) et GetBytes / GetString. Cela simplifiera également grandement le code. Recommander également d'utiliser using.
Vous ne pouvez pas convertir char en octet et l'inverse comme vous le faites (en utilisant une simple conversion). Vous devez utiliser un encodage et le même encodage pour la compression / décompression. Voir la réponse xanatos ci-dessous.
Simon Mourier
@pst non, ce ne sera pas le cas; vous utiliseriez Encodingle mauvais sens. Vous avez besoin de la base 64 ici, selon la réponse de xanatos
Marc Gravell
@Marc Gravell True, a manqué cette partie de la signature / intention. Certainement pas mon premier choix de signatures.

Réponses:

257

Le code pour compresser / décompresser une chaîne

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

N'oubliez pas que Ziprenvoie a byte[], tandis que Unziprenvoie a string. Si vous voulez une chaîne, Zipvous pouvez l'encoder en Base64 (par exemple en utilisant Convert.ToBase64String(r1)) (le résultat de Zipest TRÈS binaire! Ce n'est pas quelque chose que vous pouvez imprimer à l'écran ou écrire directement dans un XML)

La version suggérée est pour .NET 2.0, pour .NET 4.0 utilisez le MemoryStream.CopyTo.

IMPORTANT: Le contenu compressé ne peut pas être écrit dans le flux de sortie tant que le ne GZipStreamsait pas qu'il a toutes les entrées (c'est-à-dire que pour compresser efficacement, il a besoin de toutes les données). Vous devez vous assurer que vous Dispose()le faites GZipStreamavant d'inspecter le flux de sortie (par exemple, mso.ToArray()). Ceci est fait avec le using() { }bloc ci-dessus. Notez que le GZipStreamest le bloc le plus interne et que le contenu est accessible en dehors de celui-ci. La même chose vaut pour décomprimer: Dispose()des GZipStreamavant de tenter d'accéder aux données.

xanatos
la source
Merci pour la réponse. Lorsque j'utilise votre code, cela me donne une erreur de compilation. "CopyTo () n'a pas d'espace de noms ou de référence d'assembly.". Après cela, j'ai cherché sur Google et je l'ai trouvé que CopyTo () fait partie de .NET 4 Framework. Mais je travaille sur le framework .net 2.0 et 3.5. S'il vous plaît suggérez-moi. :)
Mohit Kumar
Je veux juste souligner que le GZipStream doit être éliminé avant d'appeler ToArray () sur le flux de sortie. J'ai ignoré cela, mais cela fait une différence!
Wet Noodles
1
est ce moyen le plus efficace de zipper à .net 4.5?
MonsterMMORPG
1
Notez que cela échoue (chaîne décompressée! = Original) dans le cas d'une chaîne contenant des paires de substitution, par exemple string s = "X\uD800Y". J'ai remarqué que cela fonctionne si nous changeons l'encodage en UTF7 ... mais avec UTF7 sommes-nous sûrs que tous les caractères peuvent être représentés?
digEmAll
@digEmAll Je dirai que cela ne fonctionne pas s'il y a des paires de substitution INVALID (comme dans votre cas). La conversion UTF8 GetByes remplace silencieusement la paire de substitution non valide par 0xFFFD.
xanatos
103

selon cet extrait, j'utilise ce code et cela fonctionne 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
la source
2
Je voulais juste vous remercier d'avoir publié ce code. Je l'ai inséré dans mon projet et cela a fonctionné dès la sortie de la boîte sans aucun problème.
BoltBait
3
Yup travaille hors de la boîte! J'ai aussi aimé l'idée d'ajouter de la longueur aux quatre premiers octets
JustADev
2
C'est la meilleure réponse. Celui-ci devrait être marqué comme la réponse!
Eriawan Kusumawardhono
1
@Matt c'est comme compresser un fichier .zip - .png est déjà un contenu compressé
fubo
2
La réponse marquée comme réponse n'est pas stable. Celui-ci est la meilleure réponse.
Sari
38

Avec l'avènement de .NET 4.0 (et supérieur) avec les méthodes Stream.CopyTo (), j'ai pensé publier une approche mise à jour.

Je pense également que la version ci-dessous est utile comme exemple clair de classe autonome pour compresser des chaînes régulières en chaînes encodées en Base64, et 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);
    }

Voici une autre approche utilisant la technique des méthodes d'extension pour étendre la classe String pour ajouter une compression et une décompression de chaîne. Vous pouvez déposer la classe ci-dessous dans un projet existant, puis utiliser ainsi:

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

et

var decompressedString = compressedString.Decompress();

En être témoin:

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
la source
2
Jace: Je pense que vous manquez des usingdéclarations pour les instances MemoryStream. Et aux développeurs F # là-bas: évitez d'utiliser le mot-clé usepour l'instance compresseurStream / decompressorStream, car ils doivent être supprimés manuellement avant d' ToArray()être appelés
knocte
1
Sera-t-il préférable d'utiliser GZipStream car il ajoute une validation supplémentaire? Classe GZipStream ou DeflateStream?
Michael Freidgeim
2
@Michael Freidgeim Je ne le pense pas pour la compression et la décompression des flux mémoire. Pour les fichiers ou les transports peu fiables, cela a du sens. Je dirai que dans mon cas d'utilisation particulier, la vitesse élevée est très souhaitable, donc toute surcharge que je peux éviter est d'autant mieux.
Jace
Solide. J'ai réduit ma chaîne de 20 Mo de JSON à 4,5 Mo. 🎉
James Esh
1
Fonctionne très bien, mais vous devez supprimer le flux de mémoire après utilisation, ou mettre chaque flux en utilisant comme suggéré par @knocte
Sebastian
8

Il s'agit d'une version mise à jour pour .NET 4.5 et plus récent utilisant async / await et 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;
    }
}

Avec cela, vous pouvez sérialiser tout ce qui BinaryFormatterprend en charge, au lieu uniquement des chaînes.

Éditer:

Au cas où vous auriez besoin de vous en occuper Encoding, vous pouvez simplement utiliser Convert.ToBase64String (byte []) ...

Jetez un œil à cette réponse si vous avez besoin d'un exemple!

z3nth10n
la source
Vous devez réinitialiser la position du flux avant de désérialiser, modifier votre échantillon. De plus, vos commentaires XML ne sont pas liés.
Magnus Johansson
Il convient de noter que cela fonctionne mais uniquement pour les objets basés sur UTF8. Si vous ajoutez, par exemple, des caractères suédois comme åäö à la valeur de chaîne que vous sérialisez / désérialisez, cela échouera à un test aller-retour: /
bc3tech
Dans ce cas, vous pouvez utiliser Convert.ToBase64String(byte[]). Veuillez consulter cette réponse ( stackoverflow.com/a/23908465/3286975 ). J'espère que ça aide!
z3nth10n
6

Pour ceux qui obtiennent toujours Le nombre magique dans l'en-tête GZip n'est pas correct. Assurez-vous que vous passez dans un flux GZip. ERREUR et si votre chaîne a été compressée avec php, vous devrez faire quelque chose comme:

       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
la source
J'obtiens cette exception: Exception levée: «System.IO.InvalidDataException» dans System.dll Informations supplémentaires: Le CRC dans le pied de page GZip ne correspond pas au CRC calculé à partir des données décompressées.
Dainius Kreivys