Рядок стиснення / декомпресії за допомогою C #


144

Я новачок в .net. Я роблю рядок стиснення та декомпресії в C #. Є XML, і я перетворюю рядок, після чого я роблю стиснення та декомпресію. У моєму коді немає помилки компіляції, за винятком випадків, коли я декомпресую свій код і повертаю свій рядок, він повертає лише половину XML.

Нижче мій код, будь ласка, виправте мене, де я помиляюся.

Код:

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

Мій розмір XML - 63 КБ.


1
Я підозрюю, що проблема "виправить себе", якщо використовувати UTF8Encoding (або UTF16 чи що-небудь) та GetBytes / GetString. Це також значно спростить код. Також рекомендую використовувати using.

Ви не можете перетворити char в байт і реверс, як ви робите (використовуючи простий ролик). Для стиснення / декомпресії потрібно використовувати кодування і те саме кодування. Дивіться відповідь xanatos нижче.
— Саймон Мур’є

@pst ні, це не буде; ви б використовували Encodingнеправильний шлях. Тут вам потрібна база-64, відповідно до відповіді xanatos
— Марк Гравелл

@Marc Gravell True, пропустив цю частину підпису / наміру. Однозначно не перший мій вибір підписів.

Відповіді:


257

Код для стискання / розпакування рядка

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

Пам'ятайте, що Zipповертає a byte[], а Unzipповертає a string. Якщо вам потрібен рядок, Zipви можете Base64 кодувати його (наприклад, використовуючи Convert.ToBase64String(r1)) (результат Zip- ДУЖЕ двійковий! Це не те, що можна надрукувати на екрані або записати безпосередньо в XML)

Пропонована версія призначена для .NET 2.0, для .NET 4.0 використовуйте MemoryStream.CopyTo.

ВАЖЛИВО: Стислий вміст не може бути записаний у вихідний потік до тих пір, поки не GZipStreamдізнається, що в ньому є весь вхід (тобто для ефективного стиснення йому потрібні всі дані). Ви повинні переконатися , що ви Dispose()з GZipStreamперед оглядом вихідного потоку (наприклад, mso.ToArray()). Це робиться з using() { }блоком вище. Зауважте, що GZipStreamблок є найпотаємнішим блоком, а вміст доступний поза ним. Те ж саме відноситься і до розпакуванні: Dispose()з GZipStreamраніше спроб доступу до даних.


Дякую за відповідь. Коли я використовую ваш код, він дає мені помилку компіляції. "CopyTo () не має простору імен або посилання на збірку." Після цього я здійснив пошук у Google і визначив, що це CopyTo () частина .NET 4 Framework. Але я працюю над .net 2.0 та 3.5 Framework. Підкажіть, будь ласка. :)
— Мохіт Кумар

Я просто хочу підкреслити, що GZipStream необхідно утилізувати перед викликом ToArray () у вихідному потоці. Я проігнорував цей біт, але це має значення!
— Мокра локшина

1
це найефективніший спосіб блискавки на .net 4.5?
— MonsterMMORPG

1
Зауважте, що це не вдається (unzipped-string! = Original) у випадку рядка, що містить сурогатні пари, наприклад string s = "X\uD800Y". Я помітив, що це працює, якщо ми змінимо Encoding на UTF7 ... але з UTF7 ми впевнені, що всі символи можуть бути представлені?
— digEmAll

@digEmAll Скажу, що це не працює, якщо є сурогатні пари INVALID (як у вашому випадку). Перетворення GetByes UTF8 мовчки замінює недійсну сурогатну пару на 0xFFFD.
— xanatos

103

відповідно до цього фрагмента, я використовую цей код, і він працює добре:

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
Я просто хотів подякувати вам за публікацію цього коду. Я кинув його у свій проект, і він працював прямо з коробки без проблем.
— BoltBait

3
Так, я працюю з коробки! Також мені сподобалась ідея додавання довжини як перші чотири байти
— JustADev

2
Це найкраща відповідь. Цей має бути позначений як відповідь!
— Eriawan Kusumawardhono

1
@Matt - це як стиснення .zip-файлу - .png - це вже стислий вміст
— fubo

2
Відповідь, яка позначена як відповідь, не є стабільною. Цей найкращий варіант відповіді.
— Сарі

38

З появою .NET 4.0 (і вище) методами Stream.CopyTo (), я думав, що опублікую оновлений підхід.

Я також думаю, що наведена нижче версія є корисною як наочний приклад автономного класу для стиснення регулярних рядків до кодованих рядків Base64, і навпаки:

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

Ось ще один підхід з використанням методики методів розширення для розширення класу String для додавання рядкового стиснення та декомпресії. Ви можете перенести клас нижче в існуючий проект, а потім використати таким чином:

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

і

var decompressedString = compressedString.Decompress();

А саме:

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
Джейс: Я думаю, вам не вистачає usingтверджень для екземплярів MemoryStream. А розробникам F # там: утримайтеся від використання ключового слова useдля екземпляра компресорStream / decompressorStream, оскільки їх потрібно розпоряджатись вручну, перш ніж ToArray()викликати
— knocte

1
Чи буде краще використовувати GZipStream, оскільки це додасть додаткову перевірку? Клас GZipStream або DeflateStream?
— Майкл Фрейджім

2
@Michael Freidgeim Я б не вважав це для стиснення та декомпресії потоків пам'яті. Для файлів або ненадійних перевезень це має сенс. Я скажу, що в моєму конкретному випадку користування високою швидкістю дуже бажано, тому будь-які накладні витрати, які я можу уникнути, тим краще.
— Джейс

Суцільний. Звів мій струм JSON 20 Мб до 4,5 Мб. 🎉
— Джеймс Еш

1
Працює чудово, але вам слід розпоряджатися потоком пам’яті після використання або застосовувати кожен потік у використанні, як запропонував @knocte
— Себастьян

8

Це оновлена ​​версія для .NET 4.5 та новішої версії, яка використовує async / await та 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;
    }
}

За допомогою цього ви можете серіалізувати все BinaryFormatterпідтримку, а не лише рядки.

Редагувати:

У випадку, якщо вам потрібно подбати про це Encoding, ви можете просто скористатися Convert.ToBase64String (байт []) ...

Подивіться на цю відповідь, якщо вам потрібен приклад!


Ви повинні скинути позицію Stream перед тим, як DeSerialization та редагував зразок. Також ваші коментарі XML не пов'язані.
— Магнус Йоханссон

Варто зазначити, що це працює, але лише для речей, що базуються на UTF8. Якщо ви додасте, скажімо, шведські символи на зразок ääö до значення рядка, яке ви серіалізуєте / десеріалізуєте, це не вдасться до тесту в обидва кінці: /
— bc3tech

У цьому випадку ви могли б скористатися Convert.ToBase64String(byte[]). Будь ласка, дивіться цю відповідь ( stackoverflow.com/a/23908465/3286975 ). Сподіваюся, це допомагає!
— z3nth10n

6

Для тих, хто все ще отримує Чарівне число в заголовку GZip невірно. Переконайтеся, що ви проходите в потоці GZip. ПОМИЛКА, і якщо ваш рядок було зафіксовано за допомогою php, вам потрібно зробити щось на кшталт:

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

Я отримую цей виняток: Виключення: "System.IO.InvalidDataException" в System.dll Додаткова інформація: CRC у нижньому колонтитулі GZip не відповідає CRC, обчисленому з декомпресованих даних.
— Дайній Крейвіс
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.