Ciąg kompresji / dekompresji w języku C #

144

Jestem nowicjuszem w .net. Robię ciąg kompresji i dekompresji w C #. Istnieje XML i konwertuję na ciąg, a następnie wykonuję kompresję i dekompresję.Nie ma błędu kompilacji w moim kodzie, z wyjątkiem sytuacji, gdy dekompresuję mój kod i zwracam ciąg, zwracając tylko połowę XML.

Poniżej znajduje się mój kod, popraw mnie tam, gdzie się mylę.

Kod:

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

Mój rozmiar XML to 63 KB.

Mohit Kumar
źródło
1
Podejrzewam, że problem sam się „naprawi”, jeśli użyjesz UTF8Encoding (lub UTF16 lub czegoś innego) i GetBytes / GetString. Znacznie uprości też kod. Polecam również użycie using.
Nie możesz zamienić znaku na bajt i na odwrót, tak jak robisz (używając prostego rzutowania). Musisz użyć kodowania i tego samego kodowania do kompresji / dekompresji. Zobacz odpowiedź Xanatos poniżej.
Simon Mourier,
@pst nie, nie będzie; używałbyś Encodingniewłaściwego sposobu. Potrzebujesz tutaj bazy 64, zgodnie z odpowiedzią xanatosa
Marc Gravell
@Marc Gravell True, przegapiłem tę część podpisu / intencji. Zdecydowanie nie jest to mój pierwszy wybór podpisów.

Odpowiedzi:

257

Kod do kompresji / dekompresji ciągu

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

Pamiętaj, że Zipzwraca a byte[], while Unzipzwraca a string. Jeśli chcesz napisać od Zipsiebie, możesz zakodować go w Base64 (na przykład za pomocą Convert.ToBase64String(r1)) (wynik Zipjest BARDZO binarny! Nie jest to coś, co możesz wydrukować na ekranie lub napisać bezpośrednio w XML)

Sugerowana wersja jest przeznaczona dla .NET 2.0, dla .NET 4.0 użyj rozszerzenia MemoryStream.CopyTo.

WAŻNE: Skompresowana zawartość nie może zostać zapisana w strumieniu wyjściowym, dopóki nie będzie GZipStreamwiadomo, że ma wszystkie dane wejściowe (tj. Aby skutecznie skompresować, potrzebuje wszystkich danych). Musisz się upewnić, że jesteś Dispose()w GZipStreamprzed sprawdzeniem strumienia wyjściowego (np mso.ToArray().). Odbywa się to za pomocą using() { }powyższego bloku. Zauważ, że GZipStreamjest to najbardziej wewnętrzny blok, a dostęp do zawartości jest poza nim. To samo odnosi się do dekompresji: Dispose()zGZipStream przed próbą dostępu do danych.

xanatos
źródło
Dziękuję za odpowiedź. Kiedy używam Twojego kodu, powoduje to błąd kompilacji. „CopyTo () nie ma przestrzeni nazw ani odwołania do zestawu.”. Potem wyszukałem w Google i znalazłem to, że CopyTo () jest częścią .NET 4 Framework. Ale pracuję na frameworku .net 2.0 i 3.5. Proszę, zasugeruj mi. :)
Mohit Kumar
Chcę tylko podkreślić, że GZipStream musi zostać usunięty przed wywołaniem ToArray () w strumieniu wyjściowym. Zignorowałem to, ale to robi różnicę!
Wet Noodles
1
czy jest to najskuteczniejszy sposób pakowania w .net 4.5?
MonsterMMORPG
1
Zauważ, że to się nie powiedzie (unzipped-string! = Original) w przypadku łańcucha zawierającego pary zastępcze, np string s = "X\uD800Y". Zauważyłem, że działa, jeśli zmienimy kodowanie na UTF7 ... ale czy w przypadku UTF7 jesteśmy pewni, że wszystkie znaki mogą być reprezentowane?
digEmAll
@digEmAll Powiem, że to nie działa, jeśli istnieją NIEPRAWIDŁOWE pary zastępcze (jak w twoim przypadku). Konwersja UTF8 GetByes dyskretnie zastępuje nieprawidłową parę zastępczą wartością 0xFFFD.
xanatos
103

zgodnie z tym fragmentem kodu używam tego kodu i działa dobrze:

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
źródło
2
Chciałem tylko podziękować za wysłanie tego kodu. Wrzuciłem to do mojego projektu i działało od razu po wyjęciu z pudełka bez żadnych problemów.
BoltBait
3
Tak działa po wyjęciu z pudełka! Podobał mi się również pomysł dodania długości jako pierwszych czterech bajtów
JustADev
2
To najlepsza odpowiedź. Ten powinien być oznaczony jako odpowiedź!
Eriawan Kusumawardhono
1
@Matt to jak kompresowanie pliku .zip - .png jest już skompresowaną zawartością
fubo
2
Odpowiedź oznaczona jako odpowiedź nie jest stabilna. To jest najlepsza odpowiedź.
Sari
38

Wraz z pojawieniem się .NET 4.0 (i wyższych) z metodami Stream.CopyTo () pomyślałem, że opublikuję zaktualizowane podejście.

Myślę również, że poniższa wersja jest przydatna jako wyraźny przykład niezależnej klasy do kompresji zwykłych ciągów do ciągów zakodowanych w Base64 i odwrotnie:

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

Oto inne podejście wykorzystujące technikę metod rozszerzających do rozszerzenia klasy String w celu dodania kompresji i dekompresji ciągów. Możesz upuścić poniższą klasę do istniejącego projektu, a następnie użyć w ten sposób:

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

i

var decompressedString = compressedString.Decompress();

Dowcip:

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
źródło
2
Jace: Myślę, że brakuje ci usinginstrukcji dla instancji MemoryStream. A do programistów F #: powstrzymaj się od używania słowa kluczowego usedla instancji compressStream / decompressorStream, ponieważ muszą zostać usunięte ręcznie, zanim zostaną ToArray()wywołane
knocte
1
Czy lepiej będzie użyć GZipStream, ponieważ dodaje on dodatkową walidację? Klasa GZipStream czy DeflateStream?
Michael Freidgeim
2
@Michael Freidgeim Nie sądziłbym, żeby tak było przy kompresji i dekompresji strumieni pamięci. W przypadku plików lub zawodnych transportów ma to sens. Powiem, że w moim konkretnym przypadku wysoka prędkość jest bardzo pożądana, więc każde obciążenie, którego mogę uniknąć, jest tym lepsze.
Jace
Solidny. Zrobiłem mój ciąg JSON z 20 MB do 4,5 MB. 🎉
James Esh
1
Działa świetnie, ale po użyciu należy pozbyć się strumienia wspomnień lub włączyć każdy strumień, zgodnie z sugestią @knocte
Sebastian
8

To jest zaktualizowana wersja dla .NET 4.5 i nowszych przy użyciu async / await i 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;
    }
}

Dzięki temu możesz serializować wszystko BinaryFormatter, co obsługuje, zamiast tylko ciągów.

Edytować:

Na wypadek, gdybyś musiał się tym zająć Encoding, możesz po prostu użyć Convert.ToBase64String (byte []) ...

Spójrz na tę odpowiedź, jeśli potrzebujesz przykładu!

z3nth10n
źródło
Musisz zresetować pozycję strumienia przed DeSerializing i edycją próbki. Twoje komentarze XML są również niepowiązane.
Magnus Johansson
Warto zauważyć, że to działa, ale tylko w przypadku rzeczy opartych na UTF8. Jeśli dodasz, powiedzmy, szwedzkie znaki, takie jak åäö do wartości ciągu, którą serializujesz / deserializujesz, zakończy się niepowodzeniem testu w obie strony: /
bc3tech
W takim przypadku możesz użyć Convert.ToBase64String(byte[]). Zapoznaj się z tą odpowiedzią ( stackoverflow.com/a/23908465/3286975 ). Mam nadzieję, że to pomoże!
z3nth10n
6

Dla tych, którzy nadal otrzymują Magiczna liczba w nagłówku GZip jest nieprawidłowa. Upewnij się, że przekazujesz strumień GZip. BŁĄD i jeśli twój ciąg został spakowany za pomocą php , musisz zrobić coś takiego:

       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
źródło
Otrzymuję ten wyjątek: Zgłoszony wyjątek: „System.IO.InvalidDataException” w System.dll Dodatkowe informacje: CRC w stopce GZip nie jest zgodne z CRC obliczonym na podstawie zdekompresowanych danych.
Dainius Kreivys