Строка сжатия / декомпрессии с 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);
            }
        }
    }
}
Fubo
источник
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 []) ...

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

z3nth10n
источник
Вы должны сбросить позицию потока до того, как 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();
                }
            }
        }
Choletski
источник
Я получаю это исключение: Возникло исключение: «System.IO.InvalidDataException» в System.dll Дополнительная информация: CRC в нижнем колонтитуле GZip не соответствует CRC, вычисленному из распакованных данных.
Дайний Крейвис,