使用C#进行字符串压缩/解压缩

189

我是 .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文件大小为63KB。


2
如果使用UTF8Encoding(或UTF16或其他编码方式)和GetBytes/GetString,我怀疑问题会“自行解决”。这也将极大地简化代码。同时建议使用using - user166390
1
你不能像使用简单强制类型转换那样将char转换为byte,反之亦然。你需要使用编码,并且压缩/解压缩需要使用相同的编码。请参见下面的xanatos答案。 - Simon Mourier
@pst 不会的;你使用 Encoding 的方式是错误的。你需要在这里使用 base-64,就像 xanatos 的回答一样。 - Marc Gravell
@Marc Gravell 确实,我错过了签名/意图的那一部分。绝对不是我首选的签名。 - user166390
8个回答

327

压缩/解压字符串的代码

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返回一个byte[],而Unzip返回一个string。如果你想从Zip获取一个字符串,你可以对其进行Base64编码(例如使用Convert.ToBase64String(r1))。Zip的结果是二进制的!你不能直接将其打印到屏幕上或直接写入XML文件中。

推荐的版本适用于.NET 2.0,对于.NET 4.0,请使用MemoryStream.CopyTo

重要提示:GZipStream知道已经拥有了所有输入内容之前(即为了有效地压缩它需要所有数据),压缩后的内容不能写入输出流。您需要在检查输出流(例如mso.ToArray())之前确保GZipStreamDispose()。这是通过以上的using() { }块完成的。请注意,GZipStream是最内部的块,内容在其外部访问。解压缩也是一样:Dispose()在尝试访问数据之前处理好GZipStream


我只想强调一下,在调用输出流的ToArray()方法之前,必须先释放GZipStream对象。我忽略了这点,但这是很重要的! - Wet Noodles
1
这是在 .NET 4.5 中压缩的最有效方式吗? - Furkan Gözükara
1
请注意,如果字符串包含代理对(例如 string s = "X\uD800Y"),则此方法会失败(未解压的字符串与原始字符串不相等)。我注意到,如果我们将编码更改为UTF7,则它可以正常工作...但是使用UTF7,我们是否确信所有字符都可以表示? - digEmAll
1
@Pan.student 我已经检查过了,似乎可以与我生成的一个gz文件一起使用。有可能文件并不是真正的gz文件。请注意,gz文件不是rar文件,也不是zip文件,也不是bz2文件。它们都是不兼容的格式。如果你能够从Windows打开它,我建议你在SO上发布问题,并附上你正在使用的代码。 - xanatos
1
答案中的代码将导致异常:“GZip头中的魔数不正确。请确保您正在传递GZip流。”,正如@rluks所说。 - Eriawan Kusumawardhono
显示剩余15条评论

138

根据这个代码片段,我使用了这段代码,而且它运行良好:

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

3
我只想感谢您发布这段代码。我把它放到我的项目中,它立即运行,没有任何问题。 - BoltBait
4
是的,直接使用就可以了!我也喜欢在前四个字节中添加长度的想法。 - JustADev
2
这是最好的答案。这个应该标记为答案! - Eriawan Kusumawardhono
1
@Matt 这就像压缩一个 .zip 文件一样 - .png 已经是压缩过的内容了。 - fubo
4
标记为答案的回答不够稳定,这个是最佳答案。 - NuminousName
显示剩余7条评论

65
随着.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
Jace:我认为你缺少了对MemoryStream实例的“using”语句。而对于那些F#开发人员,请避免使用关键字“use”来处理compressorStream/decompressorStream实例,因为在调用“ToArray()”之前需要手动释放它们。 - knocte
1
使用GZipStream是否更好,因为它添加了一些额外的验证?[GZipStream或DeflateStream类?] (//stackoverflow.com/q/2599080) - Michael Freidgeim
2
@Michael Freidgeim,对于压缩和解压内存流,我认为不需要。对于文件或不可靠的传输,这是有意义的。但我要说,在我的特定用例中,高速度非常重要,因此我可以避免任何开销就更好了。 - Jace
1
工作得很好,但是你应该在使用后处理memorystream,或者像@knocte建议的那样将每个流放入using中。 - Sebastian
1
@Sebastian 阅读文档:https://learn.microsoft.com/en-us/dotnet/api/system.io.compression.deflatestream.-ctor?view=netframework-4.8#System_IO_Compression_DeflateStream__ctor_System_IO_Stream_System_IO_Compression_CompressionLevel_System_Boolean_“默认情况下,DeflateStream 拥有底层流,因此关闭流也会关闭底层流。” - Jace
显示剩余6条评论

24

我最喜欢@fubo的回答,但我认为这个更加优雅。

这种方法更兼容,因为它不需要提前手动存储长度。

另外,我还公开了扩展,用于支持字符串到字符串、字节数组到字节数组以及流到流的压缩。

public static class ZipExtensions
{
    public static string CompressToBase64(this string data)
    {
        return Convert.ToBase64String(Encoding.UTF8.GetBytes(data).Compress());
    }

    public static string DecompressFromBase64(this string data)
    {
        return Encoding.UTF8.GetString(Convert.FromBase64String(data).Decompress());
    }
    
    public static byte[] Compress(this byte[] data)
    {
        using (var sourceStream = new MemoryStream(data))
        using (var destinationStream = new MemoryStream())
        {
            sourceStream.CompressTo(destinationStream);
            return destinationStream.ToArray();
        }
    }

    public static byte[] Decompress(this byte[] data)
    {
        using (var sourceStream = new MemoryStream(data))
        using (var destinationStream = new MemoryStream())
        {
            sourceStream.DecompressTo(destinationStream);
            return destinationStream.ToArray();
        }
    }
    
    public static void CompressTo(this Stream stream, Stream outputStream)
    {
        using (var gZipStream = new GZipStream(outputStream, CompressionMode.Compress))
        {
            stream.CopyTo(gZipStream);
            gZipStream.Flush();
        }
    }

    public static void DecompressTo(this Stream stream, Stream outputStream)
    {
        using (var gZipStream = new GZipStream(stream, CompressionMode.Decompress))
        {
            gZipStream.CopyTo(outputStream);
        }
    }
}

我使用了一个52 MB的文件来测试这个解决方案和Jace的解决方案。 Jace的解决方案将其压缩到2.9 MB,并花费361毫秒进行压缩和解压缩。 这个解决方案将其压缩到1.66 MB,但花费788毫秒进行压缩和解压缩。 两者使用的内存大致相同。 - James in Indy

13

这是使用async/await和IEnumerables更新的.NET 4.5及更高版本:

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 支持的一切,而不仅仅是字符串。
编辑:
如果你需要考虑编码,你可以使用 Convert.ToBase64String(byte[])...
如果你需要示例,请看看这个答案!

在反序列化之前,您必须重置流位置,并编辑您的示例。此外,您的XML注释与此无关。 - Magnus Johansson
值得注意的是,这只适用于基于UTF8的内容。如果您将瑞典字符(如åäö)添加到要序列化/反序列化的字符串值中,则无法通过往返测试:/ - bc3tech
在这种情况下,您可以使用Convert.ToBase64String(byte[])。请参见此答案(https://dev59.com/9oDaa4cB1Zd3GeqP_xmC#23908465)。希望能帮到您! - z3nth10n

7
对于那些仍然遇到魔术数字在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.dll中抛出的“System.IO.InvalidDataException”附加信息:GZip页脚中的CRC与从解压缩数据计算的CRC不匹配。 - Dainius Kreivys
我相信有人会遇到同样的问题。为了在压缩字符串中拥有那个神奇的头部,你需要使用正确的PHP函数:"gzencode"而不是"gzcompress"。PHP中还有另一种压缩算法:"gzinflate",但我个人从未使用过。顺便说一下,你的代码有一个问题:你写了一个头部,然后通过给第二个Write()方法设置偏移量0来覆盖它的实际字节,因此结果是你在流中有相同的字节。 - Developer

5

使用SharpZipLib库在C#中进行.NET6跨平台字符串压缩/解压缩。 在Ubuntu(18.0.x)和Windows上进行测试。

#region helper

private byte[] Zip(string text)
{
    if (text == null)
        return null;

    byte[] ret;
    using (var outputMemory = new MemoryStream())
    {
        using (var gz = new GZipStream(outputMemory, CompressionLevel.Optimal))
        {
            using (var sw = new StreamWriter(gz, Encoding.UTF8))
            {
                sw.Write(text);
            }
        }
        ret = outputMemory.ToArray();
    }
    return ret;
}

private string Unzip(byte[] bytes)
{
    string ret = null;
    using (var inputMemory = new MemoryStream(bytes))
    {
        using (var gz = new GZipStream(inputMemory, CompressionMode.Decompress))
        {
            using (var sr = new StreamReader(gz, Encoding.UTF8))
            {
                ret = sr.ReadToEnd();
            }
        }
    }
    return ret;
}
#endregion

4

通过使用StreamReader和StreamWriter而不是手动将字符串转换为字节数组,我们可以减少代码复杂性。你只需要三个流:

    public static byte[] Zip(string uncompressed)
    {
        byte[] ret;
        using (var outputMemory = new MemoryStream())
        {
            using (var gz = new GZipStream(outputMemory, CompressionLevel.Optimal))
            {
                using (var sw = new StreamWriter(gz, Encoding.UTF8))
                {
                    sw.Write(uncompressed);
                }
            }
            ret = outputMemory.ToArray();
        }
        return ret;
    }

    public static string Unzip(byte[] compressed)
    {
        string ret = null;
        using (var inputMemory = new MemoryStream(compressed))
        {
            using (var gz = new GZipStream(inputMemory, CompressionMode.Decompress))
            {
                using (var sr = new StreamReader(gz, Encoding.UTF8))
                {
                    ret = sr.ReadToEnd();
                }
            }
        }
        return ret;
    }

我尝试过那个方法,但在某些情况下会导致问题。我甚至尝试了Convert.UTF8,但在某些情况下也存在问题。唯一100%有效的解决方案是使用for循环手动构建字符串并将字符串转换为字节。 - Developer

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接