Komprimierungs- / Dekomprimierungszeichenfolge mit C #

144

Ich bin ein Neuling in .net. Ich mache eine Komprimierungs- und Dekomprimierungszeichenfolge in C #. Es gibt ein XML und ich konvertiere in eine Zeichenfolge und danach mache ich eine Komprimierung und Dekomprimierung. Es gibt keinen Kompilierungsfehler in meinem Code, außer wenn ich meinen Code dekomprimiere und meine Zeichenfolge zurückgebe, gibt es nur die Hälfte des XML zurück.

Unten ist mein Code, bitte korrigieren Sie mich, wo ich falsch liege.

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

Meine XML-Größe beträgt 63 KB.

Mohit Kumar
quelle
1
Ich vermute, dass sich das Problem "selbst beheben" wird, wenn UTF8Encoding (oder UTF16 oder so weiter) und GetBytes / GetString verwendet werden. Es wird auch den Code stark vereinfachen. Auch empfehlen mit using.
Sie können char nicht wie Sie in Byte und umgekehrt konvertieren (mit einer einfachen Umwandlung). Sie müssen eine Codierung und dieselbe Codierung für die Komprimierung / Dekomprimierung verwenden. Siehe Xanatos Antwort unten.
Simon Mourier
@pst nein wird es nicht; Sie würden Encodingden falschen Weg verwenden. Sie benötigen hier Base-64 gemäß der Antwort von
Xanatos
@Marc Gravell True, habe diesen Teil der Signatur / Absicht verpasst. Auf keinen Fall meine erste Wahl für Unterschriften.

Antworten:

257

Der Code zum Komprimieren / Dekomprimieren eines Strings

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

Denken Sie daran, dass Zipa zurückgegeben wird byte[], während Unzipa zurückgegeben wird string. Wenn Sie einen String von möchten Zip, können Sie ihn von Base64 codieren (z. B. mit Convert.ToBase64String(r1)) (das Ergebnis Zipist SEHR binär! Sie können ihn nicht auf den Bildschirm drucken oder direkt in eine XML schreiben).

Die vorgeschlagene Version ist für .NET 2.0, für .NET 4.0 verwenden Sie die MemoryStream.CopyTo.

WICHTIG: Der komprimierte Inhalt kann erst in den Ausgabestream geschrieben werden, wenn er GZipStreamweiß, dass er über alle Eingaben verfügt (dh um effektiv zu komprimieren, werden alle Daten benötigt). Sie müssen sicherstellen, dass Sie Dispose()von der, GZipStreambevor Sie den Ausgabestream überprüfen (z mso.ToArray(). B. ). Dies geschieht mit dem using() { }obigen Block. Beachten Sie, dass dies der GZipStreaminnerste Block ist und auf den Inhalt außerhalb zugegriffen wird. Gleiches gilt für die Dekomprimierung: Dispose()von GZipStreamvor dem Versuch, auf die Daten zuzugreifen.

Xanatos
quelle
Vielen Dank für die Antwort. Wenn ich Ihren Code verwende, wird mir ein Kompilierungsfehler angezeigt. "CopyTo () hat keinen Namespace oder keine Assemblyreferenz." Danach habe ich bei Google gesucht und festgestellt, dass CopyTo () Teil von .NET 4 Framework ist. Aber ich arbeite an .net 2.0 und 3.5 Framework. Bitte schlagen Sie mich vor. :)
Mohit Kumar
Ich möchte nur betonen, dass der GZipStream entsorgt werden muss, bevor ToArray () im Ausgabestream aufgerufen wird. Ich habe dieses bisschen ignoriert, aber es macht einen Unterschied!
Wet Noodles
1
Ist dies die effektivste Art des Zippens bei .net 4.5?
MonsterMMORPG
1
Beachten Sie, dass dies fehlschlägt (entpackte Zeichenfolge! = Original), wenn eine Zeichenfolge Ersatzpaare enthält, z string s = "X\uD800Y". Mir ist aufgefallen, dass es funktioniert, wenn wir die Codierung auf UTF7 ändern ... aber sind wir mit UTF7 sicher, dass alle Zeichen dargestellt werden können?
digEmAll
@digEmAll Ich werde sagen, dass es nicht funktioniert, wenn es UNGÜLTIGE Ersatzpaare gibt (wie in Ihrem Fall). Die UTF8-GetByes-Konvertierung ersetzt stillschweigend das ungültige Ersatzpaar durch 0xFFFD.
Xanatos
103

Laut diesem Snippet verwende ich diesen Code und er funktioniert einwandfrei:

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
quelle
2
Ich wollte mich nur bei Ihnen für die Veröffentlichung dieses Codes bedanken. Ich habe es in mein Projekt eingefügt und es hat sofort ohne Probleme funktioniert.
BoltBait
3
Yup arbeiten aus der Box! Ich mochte auch die Idee, Länge als erste vier Bytes
hinzuzufügen
2
Dies ist die beste Antwort. Dieser sollte als Antwort markiert werden!
Eriawan Kusumawardhono
1
@Matt das ist wie das Zippen einer .zip-Datei - .png ist bereits ein komprimierter Inhalt
fubo
2
Die als Antwort gekennzeichnete Antwort ist nicht stabil. Dieser ist die beste Antwort.
Sari
38

Mit dem Aufkommen von .NET 4.0 (und höher) mit den Stream.CopyTo () -Methoden dachte ich, ich würde einen aktualisierten Ansatz veröffentlichen.

Ich denke auch, dass die folgende Version als klares Beispiel für eine in sich geschlossene Klasse zum Komprimieren regulärer Zeichenfolgen in Base64-codierte Zeichenfolgen nützlich ist und umgekehrt:

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

Hier ist ein weiterer Ansatz, bei dem die String-Klasse mithilfe der Erweiterungstechniken erweitert wird, um String-Komprimierung und -Dekomprimierung hinzuzufügen. Sie können die folgende Klasse in ein vorhandenes Projekt einfügen und dann folgendermaßen verwenden:

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

und

var decompressedString = compressedString.Decompress();

Nämlich:

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
quelle
2
Jace: Ich denke, Sie vermissen usingAnweisungen für die MemoryStream-Instanzen. Und zu den F # -Entwicklern da draußen: Verwenden Sie das Schlüsselwort usefür die Instanz compressorStream / decompressorStream nicht, da sie manuell entsorgt werden müssen, bevor sie ToArray()aufgerufen werden
knocte
1
Wird es besser sein, GZipStream zu verwenden, da es eine zusätzliche Validierung hinzufügt? GZipStream- oder DeflateStream-Klasse?
Michael Freidgeim
2
@ Michael Freidgeim Ich würde es nicht für das Komprimieren und Dekomprimieren von Speicherströmen denken. Für Dateien oder unzuverlässige Transporte ist dies sinnvoll. Ich werde sagen, dass in meinem speziellen Anwendungsfall eine hohe Geschwindigkeit sehr wünschenswert ist, so dass jeder Overhead, den ich vermeiden kann, umso besser ist.
Jace
Solide. Ich habe meine 20-MB-JSON-Zeichenfolge auf 4,5 MB reduziert. 🎉
James Esh
1
Funktioniert hervorragend, aber Sie sollten den Memorystream nach der Verwendung entsorgen oder jeden Stream wie von @knocte
Sebastian
8

Dies ist eine aktualisierte Version für .NET 4.5 und höher mit async / await und 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;
    }
}

Damit können Sie alles BinaryFormatterunterstützen, was unterstützt wird, anstatt nur Zeichenfolgen.

Bearbeiten:

Falls Sie sich darum kümmern müssen Encoding, können Sie einfach Convert.ToBase64String (byte []) verwenden ...

Schauen Sie sich diese Antwort an, wenn Sie ein Beispiel brauchen!

z3nth10n
quelle
Sie müssen die Stream-Position vor dem DeSerializing zurücksetzen und Ihr Sample bearbeiten. Außerdem haben Ihre XML-Kommentare nichts miteinander zu tun.
Magnus Johansson
Bemerkenswert ist, dass dies funktioniert, aber nur für UTF8-basierte Dinge. Wenn Sie beispielsweise schwedische Zeichen wie åäö zu dem Zeichenfolgenwert hinzufügen, den Sie serialisieren / deserialisieren,
schlägt ein Roundtrip
In diesem Fall könnten Sie verwenden Convert.ToBase64String(byte[]). Bitte lesen Sie diese Antwort ( stackoverflow.com/a/23908465/3286975 ). Ich hoffe es hilft!
z3nth10n
6

Für diejenigen, die noch bekommen magische Zahl im GZip-Header erhalten, ist nicht korrekt. Stellen Sie sicher, dass Sie einen GZip-Stream übergeben. FEHLER und wenn Ihre Zeichenfolge mit PHP gezippt wurde, müssen Sie Folgendes tun:

       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
quelle
Ich erhalte die folgende Ausnahme: Ausnahme ausgelöst: 'System.IO.InvalidDataException' in System.dll Zusätzliche Informationen: Die CRC in der GZip-Fußzeile stimmt nicht mit der aus den dekomprimierten Daten berechneten CRC überein.
Dainius Kreivys