Python:实现Inflate和Deflate

71

我需要与一个要求发送至其数据使用Deflate算法(Huffman编码+LZ77)进行压缩并且发送我需要解压缩的数据的服务器进行交互。

我知道Python包含Zlib,并且Zlib中的C库支持调用解压缩压缩,但这些显然不由Python的Zlib模块提供。它提供压缩解压缩,但是当我执行以下调用时:

result_data = zlib.decompress( base64_decoded_compressed_string )

我收到了以下错误:

Error -3 while decompressing data: incorrect header check

Gzip并没有提高性能;当执行以下调用时:

result_data = gzip.GzipFile( fileobj = StringIO.StringIO( base64_decoded_compressed_string ) ).read()

我收到了以下错误:

IOError: Not a gzipped file

由于数据是一个 Deflated 文件而不是真正的 Gzipped 文件,这是有道理的。

现在我知道有一个可用的 Deflate 实现(Pyflate),但我不知道有一个 Inflate 实现。

似乎有几个选项:

  1. 在 Python 中查找现有的 InflateDeflate 实现(最理想)
  2. 编写自己的 Python 扩展,使用 zlib c 库包括 InflateDeflate
  3. 调用其他可以从命令行执行的东西(例如 Ruby 脚本,因为在 Ruby 中完全包装了 zlib 中的 Inflate/Deflate 调用)
  4. ?

我正在寻找一个解决方案,但如果没有解决方案,我将感谢见解、建设性意见和想法。

附加信息: 对字符串执行压缩(和编码)的结果应该与以下 C# 代码片段的结果相同,其中输入参数是与要压缩的数据对应的 UTF 字节数组:

public static string DeflateAndEncodeBase64(byte[] data)
{
    if (null == data || data.Length < 1) return null;
    string compressedBase64 = "";

    //write into a new memory stream wrapped by a deflate stream
    using (MemoryStream ms = new MemoryStream())
    {
        using (DeflateStream deflateStream = new DeflateStream(ms, CompressionMode.Compress, true))
        {
            //write byte buffer into memorystream
            deflateStream.Write(data, 0, data.Length);
            deflateStream.Close();

            //rewind memory stream and write to base 64 string
            byte[] compressedBytes = new byte[ms.Length];
            ms.Seek(0, SeekOrigin.Begin);
            ms.Read(compressedBytes, 0, (int)ms.Length);
            compressedBase64 = Convert.ToBase64String(compressedBytes);
        }
    }
    return compressedBase64;
}

将这段 .NET 代码运行于字符串 "deflate and encode me",结果为:

7b0HYBxJliUmL23Ke39K9UrX4HShCIBgEyTYkEAQ7MGIzeaS7B1pRyMpqyqBymVWZV1mFkDM7Z28995777333nvvvfe6O51OJ/ff/z9cZmQBbPbOStrJniGAqsgfP358Hz8iZvl5mbV5mi1nab6cVrM8XeT/Dw==

通过Python Zlib.compress()压缩再进行Base64编码的"deflate and encode me"结果为"eJxLSU3LSSxJVUjMS1FIzUvOT0lVyE0FAFXHB6k="。很明显,zlib.compress()不是与标准的Deflate算法实现相同的算法。

更多信息:

在b64解码后,.NET deflate数据(“7b0HY…”)的前2个字节为0xEDBD,这与Gzip数据(0x1f8b)、BZip2(0x425A)数据或Zlib(0x789C)数据不符。

在b64解码后,Python压缩数据的前两个字节(“eJxLS…”)为0x789C。这是Zlib标头。

已解决

要处理未包含标头和校验和的原始Deflate和Inflate,请执行以下操作:

进行压缩/压缩时:剥去前两个字节(标头)和最后四个字节(校验和)。

在解压缩/解压缩时:有一个第二个参数用于窗口大小。如果该值为负,则会抑制标头。以下是我目前的方法,包括Base64编码/解码,并正常工作:

import zlib
import base64

def decode_base64_and_inflate( b64string ):
    decoded_data = base64.b64decode( b64string )
    return zlib.decompress( decoded_data , -15)

def deflate_and_base64_encode( string_val ):
    zlibbed_str = zlib.compress( string_val )
    compressed_string = zlibbed_str[2:-4]
    return base64.b64encode( compressed_string )
2个回答

35

您仍然可以使用zlib模块来压缩/解压数据。gzip模块在内部使用它,但添加了一个文件头部,将其转换为gzip文件。查看gzip.py文件,类似以下代码可能会起作用:

import zlib

def deflate(data, compresslevel=9):
    compress = zlib.compressobj(
            compresslevel,        # level: 0-9
            zlib.DEFLATED,        # method: must be DEFLATED
            -zlib.MAX_WBITS,      # window size in bits:
                                  #   -15..-8: negate, suppress header
                                  #   8..15: normal
                                  #   16..30: subtract 16, gzip header
            zlib.DEF_MEM_LEVEL,   # mem level: 1..8/9
            0                     # strategy:
                                  #   0 = Z_DEFAULT_STRATEGY
                                  #   1 = Z_FILTERED
                                  #   2 = Z_HUFFMAN_ONLY
                                  #   3 = Z_RLE
                                  #   4 = Z_FIXED
    )
    deflated = compress.compress(data)
    deflated += compress.flush()
    return deflated

def inflate(data):
    decompress = zlib.decompressobj(
            -zlib.MAX_WBITS  # see above
    )
    inflated = decompress.decompress(data)
    inflated += decompress.flush()
    return inflated

我不知道这是否完全符合您的服务器要求,但这两个函数可以很好地处理我尝试的任何数据。

这些参数直接映射到传递给zlib库函数的内容。

PythonC
zlib.compressobj(...)deflateInit(...)
compressobj.compress(...)deflate(...)
zlib.decompressobj(...)inflateInit(...)
decompressobj.decompress(...)inflate(...)

构造函数创建结构并使用默认值填充它,并将其传递给 init 函数。 compress/decompress 方法更新结构并将其传递给 inflate/deflate


我所需要的是访问Python Zlib模块封装的库的C级别Inflate和Deflate调用。看起来Decompress和Compress并不是同样的东西,而Python Zlib模块也没有暴露Inflate和Deflate。 - Demi
这不是有用的。请注意我在问题上方添加的附加信息。您提供的代码,当使用字符串“deflate and encode me”运行时,会产生“S0lNy0ksSVVIzEtRSM1Lzk9JVchNBQA =”,这甚至更短。正确的减缩结果应该看起来像我上面提到的(更长).NET生成的字符串。 - Demi
1
似乎.NET版本使用了不同但兼容的算法。你能否尝试使用.NET解码Python压缩字符串?如果可以,那么它们以不同方式编码相同的字符串就没有问题了。 - Markus Jarderot
1
@Adam:212字节?他的base64编码字符串长度为160字节,解码后为118字节。也许你进行了编码(160 * 4/3约等于212)。Deflate文件头?也许你指的是gzip文件头--看起来不像(http://www.gzip.org/zlib/rfc-gzip.html):不以0x1F 0x8B开头(除非C#使用非默认的base64字母表)。如果Demi提供(1)网站规范中提供的更多详细信息(2)C# DeflateStream()的参数文档,那就太好了。 - John Machin
您的解压功能对我来说完美无缺,至于压缩功能,我目前并不需要。谢谢! - Chad
显示剩余7条评论

25

这是对MizardX答案的补充,提供一些解释和背景。

请参见http://www.chiramattel.com/george/blog/2007/09/09/deflatestream-block-length-does-not-match.html

根据RFC 1950,以默认方式构建的zlib流由以下组成:

  • 2字节的标头(例如0x78 0x9C)
  • 一个deflate流--请参见RFC 1951
  • 未压缩数据的Adler-32校验和(4字节)

C#的DeflateStream适用于(你猜对了)deflate流。MizardX的代码告诉zlib模块数据是原始deflate流。

观察结果:(1)希望C#“压缩”方法只在输入较短时产生更长的字符串(2)使用没有Adler-32校验和的原始deflate流?有点冒险,除非用更好的东西替换。

更新

错误消息块长度与其补数不匹配

如果您正在使用C#的DeflateStream尝试解压缩一些压缩数据,并且您收到了这条消息,则很可能您正在提供一个zlib流,而不是一个deflate流。
请参阅如何在文件的一部分上使用DeflateStream? 还可以将错误消息复制/粘贴到Google搜索中,您将获得众多结果(包括此答案前面的一个)都表达了类似的意思。 Java Deflater ...被“网站”使用... C#DeflateStream“非常简单,并已针对Java实现进行了测试”。 网站使用以下哪个可能的Java Deflater构造函数?

public Deflater(int level, boolean nowrap)

使用指定的压缩级别创建新的压缩器。 如果“nowrap”为true,则不会使用ZLIB标题和校验和字段,以支持GZIP和PKZIP中使用的压缩格式。

public Deflater(int level)

使用指定的压缩级别创建新的压缩器。 压缩数据将以ZLIB格式生成。

public Deflater()

使用默认压缩级别创建新的压缩器。 压缩数据将以ZLIB格式生成。

一个一行的压缩程序在丢弃2字节的zlib标题和4字节的校验和后:
uncompressed_string.encode('zlib')[2:-4] # does not work in Python 3.x

或者

zlib.compress(uncompressed_string)[2:-4]

@John Machin:回复您的第一个观察点……结果只有在较短的字符串(标题?填充?)的情况下才会更长。当我输入161字节的数据进行压缩时,在进行base64编码之前,结果为126字节。 - Demi
@John Machin: 很棒的见解和信息。所使用的deflater的Java签名是带有两个参数,其中nowrap==true。我使用了您的一行deflater示例,并且在.NET和Java中可以很好地展开,尽管与使用这些语言中的库进行压缩时产生的值看起来不同。这太棒了。现在我正在处理inflate - 采取由Java或.NET生成的压缩数据,并添加adler32校验和zlib头文件,以查看Python是否能够很好地消耗它。我会告诉您进展如何的。 - Demi
@John Machin:问题已解决。请参见上文。感谢您的帮助。关键在于将负值传递给inflate的解压方法,以及您对压缩时头部和Adler校验和的剪裁。 - Demi

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