zlib压缩字节数组?

15

我有一个未压缩的字节数组:

0E 7C BD 03 6E 65 67 6C 65 63 74 00 00 00 00 00 00 00 00 00 42 52 00 00 01 02 01
00 BB 14 8D 37 0A 00 00 01 00 00 00 00 05 E9 05 E9 00 00 00 00 00 00 00 00 00 00
00 00 00 00 01 00 00 00 00 00 81 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 05 00 00 01 00 00 00

我需要使用deflate算法(由zlib实现)对其进行压缩,根据我的搜索,在C#中相当于使用GZipStream,但是我无法完全匹配压缩结果。

以下是压缩代码:

public byte[] compress(byte[] input)
{
    using (MemoryStream ms = new MemoryStream())
    {
        using (GZipStream deflateStream = new GZipStream(ms, CompressionMode.Compress))
        {
            deflateStream.Write(input, 0, input.Length);
        }
        return ms.ToArray();
    }
}

这是以上压缩代码的结果:

1F 8B 08 00 00 00 00 00 04 00 ED BD 07 60 1C 49 96 25 26 2F 6D CA 7B 7F 4A F5 4A
D7 E0 74 A1 08 80 60 13 24 D8 90 40 10 EC C1 88 CD E6 92 EC 1D 69 47 23 29 AB 2A
81 CA 65 56 65 5D 66 16 40 CC ED 9D BC F7 DE 7B EF BD F7 DE 7B EF BD F7 BA 3B 9D
4E 27 F7 DF FF 3F 5C 66 64 01 6C F6 CE 4A DA C9 9E 21 80 AA C8 1F 3F 7E 7C 1F 3F
22 7E 93 9F F9 FB 7F ED 65 7E 51 E6 D3 F6 D7 30 CF 93 57 BF C6 AF F1 6B FE 5A BF
E6 AF F1 F7 FE 56 7F FC 03 F3 D9 AF FB 5F DB AF 83 E7 0F FE 35 23 1F FE BA F4 FE
AF F1 6B FC 1A FF 0F 26 EC 38 82 5C 00 00 00

这是我期望的结果:

78 9C E3 AB D9 CB 9C 97 9A 9E 93 9A 5C C2 00 03 4E 41 0C 0C 8C 4C 8C 0C BB 45 7A
CD B9 80 4C 90 18 EB 4B D6 97 0C 28 00 2C CC D0 C8 C8 80 09 58 21 B2 00 65 6B 08
C8

我做错了什么,有人可以帮帮我吗?


为什么您期望不同的实现会产生相同的输出呢?有许多方法可以压缩某些内容,这些内容可以使用相同的解压器进行解压缩。但在您的情况下,zip流似乎会输出某种头部信息。 - CodesInChaos
1
GZipStream 的结果不仅不同,而且比未压缩的输入还要大! - user47589
@Inuyasha,我已经理解了这一点,因此我正在寻找如何通过查找我的错误来使它们相等,正如我所提到的,我需要在C#中使用zlib的deflate实现。@CodeInChaos,我不知道它是不同的实现,我在SO上搜索并发现一些回复称GZip是它的等效物,但当我开始测试时,我确实发现它不是。 - Guapo
除了增加的大小之外,我认为还有另一个程序在解压缩这个文件。那是如何进行的? - H H
2个回答

35

首先,一些信息:DEFLATE是压缩算法,定义在RFC 1951中。 DEFLATE用于ZLIB和GZIP格式,在RFC 19501952中定义,它们本质上是DEFLATE字节流的薄包装器。这些包装器提供元数据,例如文件名、时间戳、CRC或Adler等。

.NET的基类库实现了一个DeflateStream,用于压缩时生成原始的DEFLATE字节流。在解压缩时,它会消耗原始的DEFLATE字节流。.NET还提供了一个GZipStream,它只是基于该基类的GZIP包装器。.NET基类库中没有ZlibStream——没有任何产生或消耗ZLIB的内容。有一些技巧可以做到这一点,你可以搜索一下。

.NET中的压缩逻辑存在行为异常,先前已经压缩过的数据在“压缩”时实际上可以被显著地解压缩。这是与Microsoft提出的Connect bug有关,并且在SO上已经讨论过。就节省空间而言,这可能是您看到的无效压缩情况。Microsoft已经拒绝了此错误,因为虽然它不能节省空间,但压缩流并不无效,换句话说,任何兼容的DEFLATE引擎都可以“解压缩”它。
无论如何,正如其他人所发表的,不同压缩器产生的压缩字节流可能并不相同。这取决于它们的默认设置和用于压缩器的应用程序指定设置。即使压缩后的字节流不同,它们仍然可能解压缩为相同的原始字节流。另一方面,您用于压缩的东西是GZIP,而似乎您想要的是ZLIB。虽然它们是相关的,但它们并不相同;您不能使用GZipStream来生成ZLIB字节流。这是您看到的差异的主要来源。

我认为你需要一个ZLIB流。

DotNetZip项目中的免费托管的Zlib实现了三种格式(DEFLATE,ZLIB,GZIP)的压缩流。DeflateStream和GZipStream的工作方式与.NET内置类相同,那里有一个ZlibStream类,它可以做你想做的事情。这些类中没有一个展示了我上面描述的异常行为。


在代码中它看起来像这样:

    byte[] original = new byte[] {
        0x0E, 0x7C, 0xBD, 0x03, 0x6E, 0x65, 0x67, 0x6C,
        0x65, 0x63, 0x74, 0x00, 0x00, 0x00, 0x00, 0x00,
        0x00, 0x00, 0x00, 0x00, 0x42, 0x52, 0x00, 0x00,
        0x01, 0x02, 0x01, 0x00, 0xBB, 0x14, 0x8D, 0x37,
        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
        0x05, 0xE9, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
        0x81, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
        0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00,
        0x01, 0x00, 0x00, 0x00
    };

    var compressed = Ionic.Zlib.ZlibStream.CompressBuffer(original);

输出如下:
0000    78 DA E3 AB D9 CB 9C 97 9A 9E 93 9A 5C C2 00 03     x...........\...
0010    4E 41 0C 0C 8C 4C 8C 0C BB 45 7A CD 61 62 AC 2F     NA...L...Ez.ab./
0020    19 B0 82 46 46 2C 82 AC 40 FD 40 0A 00 35 25 07     ...FF,..@.@..5%.
0030    CE                                                  .

进行解压缩,

    var uncompressed = Ionic.Zlib.ZlibStream.UncompressBuffer(compressed);

你可以查看静态CompressBuffer方法的文档


编辑

问题是为什么DotNetZip生成的前两个字节是78 DA而不是78 9C?这种差异并不重要。78 DA表示“最大压缩”,而78 9C表示“默认压缩”。如您在数据中所见,对于此小样本,无论使用BEST还是DEFAULT,实际压缩字节完全相同。此外,在解压缩过程中不使用压缩级别信息。它对您的应用程序没有影响。

如果您不想要“最大”压缩,换句话说,如果您非常希望得到78 9C作为前两个字节,即使它并不重要,那么您不能使用CompressBuffer便捷函数,该函数在内部使用最佳压缩级别。您可以尝试以下方法:

  var compress = new Func<byte[], byte[]>( a => {
        using (var ms = new System.IO.MemoryStream())
        {
            using (var compressor =
                   new Ionic.Zlib.ZlibStream( ms, 
                                              CompressionMode.Compress,
                                              CompressionLevel.Default )) 
            {
                compressor.Write(a,0,a.Length);
            }

            return ms.ToArray();
        }
    });

  var original = new byte[] { .... };
  var compressed = compress(original);

结果是:
0000    78 9C E3 AB D9 CB 9C 97 9A 9E 93 9A 5C C2 00 03     x...........\...
0010    4E 41 0C 0C 8C 4C 8C 0C BB 45 7A CD 61 62 AC 2F     NA...L...Ez.ab./
0020    19 B0 82 46 46 2C 82 AC 40 FD 40 0A 00 35 25 07     ...FF,..@.@..5%.
0030    CE                                                  .

@Cheeso,我刚试了一下Merlyn回复中的ZLib.Net,它很好地压缩了我的数据并给出了我期望的结果,现在我只是不知道如何解压接收到的字节数组。 - Guapo
@Cheeso DotNetZip在压缩时,始终使用与ZLib.Net不同的字节"78 DA"而不是"78 9C"。当我使用ZLib.Net时,它可以正常工作并给我9C而不是DA,去掉这个字节后,解压缩也可以正常工作,但不确定为什么会将9C更改为DA... - Guapo
CompressBuffer 便捷方法指定“最佳压缩”,这就是为什么在输出缓冲区中它被编码为 78 DA - Cheeso
稍微补充一下背景:DEFLATE 也被用于 zip 文件格式(对于每个归档文件单独使用)。 - Paŭlo Ebermann
@Cheeso,我知道这是一个旧的帖子,但是你详细的回答鼓励我向你提出几个问题。当我尝试在C#中使用zlib进行内存(而不是文件)压缩时,使用小于500字节的byte[]时,我发现压缩比率相当不一致:125=>116,98=>90,115=>113(压缩前后的字节数)。这让我想问:所有压缩工具都会出现这种比率不一致的情况吗?或者,这与小的byte数组输入有关,随着输入变大而改善吗?非常乐意听取您的想法。非常感谢。 - Pradeep Puranik
显示剩余2条评论

0

简单来说,你得到的是一个GZip头部。你想要的是更简单的Zlib头部。Zlib有GZip头部、Zlib头部或无头部的选项。通常情况下,使用Zlib头部,除非数据与磁盘文件相关(这种情况下使用GZip头部)。显然,在.Net库中没有办法写入Zlib头部(尽管这是文件格式中最常用的头部)。试试http://dotnetzip.codeplex.com/

你可以使用HexEdit快速测试所有不同的Zlib选项(操作->压缩->设置)。参见http://www.hexedit.com。我只需将你的压缩字节粘贴到HexEdit中并解压,就花了我10分钟来检查你的数据。还尝试使用GZip和Zlib头部压缩你的原始字节进行双重检查。请注意,你可能需要调整设置以获得你期望的确切字节。


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