Строка сжатия / декомпрессии с 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 ниже.
— Саймон Мурье

@ нет, не будет; вы будете использовать Encodingнеправильный путь. Вам нужен base-64 здесь, согласно ответу ксанатоса
— Марк Гравелл

@Marc Gravell Правда, пропустил ту часть подписи / намерения. Определенно не мой первый выбор подписей.

Ответы:


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". Я заметил, что это работает, если мы изменим кодировку на UTF7 ... но с UTF7 мы уверены, что все символы могут быть представлены?
— digEmAll

@ DigEmAll Я скажу, что это не будет работать, если есть НЕПРАВИЛЬНЫЕ суррогатные пары (как в вашем случае). Преобразование GetByes UTF8 молча заменяет недопустимую суррогатную пару на 0xFFFD.
— Ксанатос

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
Это лучший ответ. Этот должен быть отмечен как ответ!
— Эриаван Кусумавардхоно

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для экземпляра compressorStream / decompressorStream, потому что они должны быть расположены вручную , прежде чем ToArray()будет вызвана
— knocte

1
Будет ли лучше использовать GZipStream, так как он добавляет дополнительную проверку? GZipStream или класс DeflateStream?
— Майкл Фрейдгейм

2
@Michael Freidgeim Я бы так не думал о сжатии и распаковке потоков памяти. Для файлов или ненадежных транспортов это имеет смысл. Я скажу, что в моем конкретном случае высокая скорость очень желательна, поэтому любые издержки, которых я могу избежать, тем лучше.
— Джейс

Твердое вещество. Снял мою строку JSON размером 20 МБ до 4,5 МБ. James
— Джеймс Эш

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 (byte []) ...

Посмотрите на этот ответ, если вам нужен пример!


Вы должны сбросить позицию потока до того, как DeSerializing отредактировал ваш образец. Кроме того, ваши комментарии 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.