Рядок стиснення / декомпресії за допомогою 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.