String kompresi / Dekompresi dengan C #

144

Saya pemula di .net. Saya melakukan kompresi dan dekompresi string dalam C #. Ada XML dan saya mengkonversi dalam string dan setelah itu saya melakukan kompresi dan dekompresi. Tidak ada kesalahan kompilasi dalam kode saya kecuali ketika saya mendekompresi kode saya dan mengembalikan string saya, mengembalikan hanya setengah dari XML.

Di bawah ini adalah kode saya, tolong perbaiki saya di mana saya salah.

Kode:

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

Ukuran XML saya adalah 63KB.

Mohit Kumar
sumber
1
Saya menduga masalah akan "memperbaiki sendiri" jika menggunakan UTF8Encoding (atau UTF16 atau yang lainnya) dan GetBytes / GetString. Ini juga akan sangat menyederhanakan kode. Juga merekomendasikan penggunaan using.
Anda tidak dapat mengubah char menjadi byte dan sebaliknya seperti yang Anda lakukan (menggunakan gips sederhana). Anda perlu menggunakan penyandian, dan penyandian yang sama untuk kompresi / dekompresi. Lihat jawaban xanatos di bawah ini.
Simon Mourier
@pst tidak itu tidak akan; Anda akan menggunakan Encodingjalan yang salah. Anda memerlukan base-64 di sini, sesuai jawaban xanatos
Marc Gravell
@Marc Gravell Benar, melewatkan bagian tanda tangan / niat itu. Jelas bukan pilihan tanda tangan pertama saya.

Jawaban:

257

Kode untuk mengompresi / mendekompresi 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);
}

Ingat bahwa Zipmengembalikan a byte[], sementara Unzipmengembalikan a string. Jika Anda menginginkan sebuah string dari ZipAnda, Anda dapat menyalinnya dari Base64 (misalnya dengan menggunakan Convert.ToBase64String(r1)) (hasilnya ZipSANGAT biner! Ini bukan sesuatu yang dapat Anda cetak ke layar atau menulis langsung dalam XML)

Versi yang disarankan adalah untuk .NET 2.0, untuk .NET 4.0 gunakan MemoryStream.CopyTo.

PENTING: Konten terkompresi tidak dapat ditulis ke aliran output sampai GZipStreamtahu bahwa ia memiliki semua input (yaitu, untuk secara efektif kompres perlu semua data). Anda perlu memastikan bahwa Anda Dispose()dari GZipStreamsebelum memeriksa aliran output (misalnya, mso.ToArray()). Ini dilakukan dengan using() { }blok di atas. Perhatikan bahwa GZipStreamini adalah blok terdalam dan konten diakses di luarnya. Hal yang sama berlaku untuk dekompresi: Dispose()dari GZipStreamsebelum mencoba untuk mengakses data.

xanatos
sumber
Terima kasih atas jawabannya. Ketika saya menggunakan kode Anda, itu memberi saya kesalahan kompilasi. "CopyTo () tidak memiliki referensi namespace atau assembly." Setelah itu saya mencari di Google dan menemukan bahwa CopyTo () bagian dari .NET 4 Framework. Tapi saya sedang mengerjakan framework .net 2.0 dan 3.5. Tolong sarankan saya. :)
Mohit Kumar
Saya hanya ingin menekankan bahwa GZipStream harus dibuang sebelum memanggil ToArray () pada aliran keluaran. Saya mengabaikan sedikit itu, tetapi itu membuat perbedaan!
Mie Basah
1
Apakah ini cara paling efektif untuk zip di .net 4.5?
MonsterMMORPG
1
Perhatikan bahwa ini gagal (unzipped-string! = Asli) dalam kasus string yang mengandung pasangan pengganti misalnya string s = "X\uD800Y". Saya perhatikan bahwa ini berfungsi jika kita mengubah Pengkodean ke UTF7 ... tetapi dengan UTF7 apakah kami yakin semua karakter dapat diwakili?
digEmAll
@ DigEmAll Saya akan mengatakan bahwa itu tidak berfungsi jika ada pasangan pengganti INVALID (seperti dalam kasus Anda). Konversi UTF8 GetByes secara diam-diam menggantikan pasangan pengganti yang tidak valid dengan 0xFFFD.
xanatos
103

menurut cuplikan ini saya menggunakan kode ini dan berfungsi dengan baik:

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
sumber
2
Saya hanya ingin mengucapkan terima kasih karena memposting kode ini. Saya memasukkannya ke dalam proyek saya dan itu berhasil keluar dari kotak tanpa masalah sama sekali.
BoltBait
3
Yup bekerja di luar kotak! Saya juga menyukai gagasan untuk menambahkan panjang sebagai empat byte pertama
JustADev
2
Ini jawaban terbaik. Yang ini harus ditandai sebagai jawabannya!
Eriawan Kusumawardhono
1
@Matt itu seperti zip file .zip - .png sudah menjadi konten terkompresi
fubo
2
Jawaban yang ditandai sebagai jawaban tidak stabil. Yang ini adalah jawaban terbaik.
Sari
38

Dengan munculnya .NET 4.0 (dan lebih tinggi) dengan metode Stream.CopyTo (), saya pikir saya akan memposting pendekatan yang diperbarui.

Saya juga berpikir versi di bawah ini berguna sebagai contoh yang jelas dari kelas mandiri untuk memampatkan string biasa ke string yang disandikan Base64, dan sebaliknya:

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

Berikut ini pendekatan lain menggunakan teknik metode ekstensi untuk memperluas kelas String untuk menambahkan kompresi string dan dekompresi. Anda dapat menjatuhkan kelas di bawah ini ke proyek yang sudah ada dan kemudian menggunakannya:

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

dan

var decompressedString = compressedString.Decompress();

Yakni:

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
sumber
2
Jace: Saya pikir Anda kehilangan usingpernyataan untuk instance MemoryStream. Dan untuk para pengembang F # di luar sana: jangan menggunakan kata kunci useuntuk instance compressorStream / decompressorStream, karena mereka harus dibuang secara manual sebelum ToArray()dipanggil
knocte
1
Akankah lebih baik menggunakan GZipStream karena menambahkan beberapa validasi tambahan? Kelas GZipStream atau DeflateStream?
Michael Freidgeim
2
@Michael Freidgeim Saya tidak akan berpikir demikian untuk mengompresi dan mendekompresi aliran memori. Untuk file, atau transport yang tidak dapat diandalkan, masuk akal. Saya akan mengatakan bahwa dalam kasus penggunaan khusus saya, kecepatan tinggi sangat diinginkan sehingga semua overhead yang dapat saya hindari adalah yang terbaik.
Jace
Padat. Mengambil string 20MB JSON saya ke 4,5MB. 🎉
James Esh
1
Berfungsi bagus, tetapi Anda harus membuang memorystream setelah penggunaan, atau menggunakan setiap aliran menggunakan seperti yang disarankan oleh @knocte
Sebastian
8

Ini adalah versi yang diperbarui untuk .NET 4.5 dan yang lebih baru menggunakan async / menunggu dan 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;
    }
}

Dengan ini, Anda dapat membuat serialisasi semua BinaryFormatterdukungan, bukan hanya string.

Edit:

Jika Anda perlu berhati-hati Encoding, Anda bisa menggunakan Convert.ToBase64String (byte []) ...

Lihatlah jawaban ini jika Anda membutuhkan contoh!

z3nth10n
sumber
Anda harus mengatur ulang posisi Stream sebelum DeSerializing, mengedit sampel Anda. Juga, komentar XML Anda tidak terkait.
Magnus Johansson
Layak dicatat ini bekerja tetapi hanya untuk hal-hal berbasis UTF8. Jika Anda menambahkan, katakanlah, karakter Swedia seperti åäö ke nilai string yang Anda serialkan / deserialisasi akan gagal dalam tes
bolak
Dalam hal ini Anda bisa menggunakan Convert.ToBase64String(byte[]). Silakan, lihat jawaban ini ( stackoverflow.com/a/23908465/3286975 ). Semoga ini bisa membantu!
z3nth10n
6

Bagi mereka yang masih mendapatkan Angka ajaib di header GZip tidak benar. Pastikan Anda melewati aliran GZip. GALAT dan jika string Anda di-zip menggunakan php, Anda harus melakukan sesuatu seperti:

       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
sumber
Saya mendapatkan pengecualian ini: Pengecualian dilemparkan: 'System.IO.InvalidDataException' di System.dll Informasi tambahan: CRC di GZip footer tidak cocok dengan CRC yang dihitung dari data yang didekompresi.
Dainius Kreivys