如何从包含多个Gzip流的文件中读取数据

5

我有一个使用代码创建的文件,内容如下:

        using (var fs=File.OpenWrite("tmp"))
        {
            using (GZipStream gs=new GZipStream(fs,CompressionMode.Compress,true))
            {
                using (StreamWriter sw=new StreamWriter(gs))
                {
                    sw.WriteLine("hello ");
                }
            }

            using (GZipStream gs = new GZipStream(fs, CompressionMode.Compress, true))
            {
                using (StreamWriter sw = new StreamWriter(gs))
                {
                    sw.WriteLine("world");
                }
            }
        }

现在我正在尝试使用以下代码从此文件中读取数据:
        string txt;

        using (var fs=File.OpenRead("tmp"))
        {
            using (GZipStream gs=new GZipStream(fs,CompressionMode.Decompress,true))
            {
                using (var rdr = new StreamReader(gs))
                {
                    txt = rdr.ReadToEnd();
                }
            }

            using (GZipStream gs = new GZipStream(fs, CompressionMode.Decompress, true))
            {
                using (StreamReader sr = new StreamReader(gs))
                {
                    txt+=sr.ReadToEnd();
                }
            }
        }

第一个流读取正常,但第二个流无法读取。
如何读取第二个流?

请澄清“doesn't read”。您是否收到异常? - Peter Ritchie
偶尔我会出现(无效的校验和)错误,但在这个特定的例子中,第二个字符串是空的,在读取了第一个字符串后,文件系统指示它已经到达了末尾。 - Arsen Zahray
你还有这个问题的解决方案吗? - Martin
使用gzmulti可能是操作多成员gzip文件的另一种选择。 - Mahmoud Mubarak
4个回答

6
这是关于GzipStream处理具有多个gzip条目的gzip文件的问题。它只读取第一个条目,并将其后续的所有条目视为垃圾(有趣的是,像gzip和winzip这样的工具会正确地将它们提取到一个文件中)。 有几种解决方法,或者您可以使用第三方实用程序,例如DotNetZip(http://dotnetzip.codeplex.com/)。
也许最简单的方法是扫描所有gzip头信息,并手动将流移动到每个头信息并解压内容。这可以通过查找原始文件字节中的ID1、ID2和0x8(Deflate压缩方法,请参见规范:http://www.gzip.org/zlib/rfc-gzip.html)来完成。这并不总是足以保证您正在查看gzip头,因此您需要读取剩余的头信息(或至少前10个字节)进行验证:
    const int Id1 = 0x1F;
    const int Id2 = 0x8B;
    const int DeflateCompression = 0x8;
    const int GzipFooterLength = 8;
    const int MaxGzipFlag = 32; 

    /// <summary>
    /// Returns true if the stream could be a valid gzip header at the current position.
    /// </summary>
    /// <param name="stream">The stream to check.</param>
    /// <returns>Returns true if the stream could be a valid gzip header at the current position.</returns>
    public static bool IsHeaderCandidate(Stream stream)
    {
        // Read the first ten bytes of the stream
        byte[] header = new byte[10];

        int bytesRead = stream.Read(header, 0, header.Length);
        stream.Seek(-bytesRead, SeekOrigin.Current);

        if (bytesRead < header.Length)
        {
            return false;
        }

        // Check the id tokens and compression algorithm
        if (header[0] != Id1 || header[1] != Id2 || header[2] != DeflateCompression)
        {
            return false;
        }

        // Extract the GZIP flags, of which only 5 are allowed (2 pow. 5 = 32)
        if (header[3] > MaxGzipFlag)
        {
            return false;
        }

        // Check the extra compression flags, which is either 2 or 4 with the Deflate algorithm
        if (header[8] != 0x0 && header[8] != 0x2 && header[8] != 0x4)
        {
            return false;
        }

        return true;
    }

注意,如果您直接使用文件流,GzipStream可能会将流移动到文件末尾。您可能需要将每个部分读入MemoryStream中,然后在内存中逐个解压缩每个部分。
另一种方法是修改gzip头以指定内容的长度,这样您就不必扫描头文件(您可以编程确定每个偏移量),这需要深入了解gzip规范。

5

现在 .NET Core 已经实现了多部分 gzip 处理。这种讨论仍然适用于.NET Framework。


这是 GzipStream 中的一个错误。根据gzip 格式的 RFC 1952 规范

2.2. 文件格式

gzip 文件由一系列 “成员”(压缩数据集)组成。每个成员的格式在下面的部分中指定。成员只是简单地按顺序出现在文件中,它们之间没有其他信息。

因此,符合规范的解压缩器需要在前一个 gzip 成员之后立即寻找另一个 gzip 成员。

您应该能够简单地使用有缺陷的 GzipStream 循环读取一个 gzip 成员,然后再次使用 GzipStream 从上次使用 GzipStream 没有使用过的第一个输入字节开始读取下一个 gzip 成员。这将是完全可靠的,而不是尝试搜索 gzip 成员开头的其他建议。

压缩数据可以具有任何字节模式,因此有可能被欺骗认为已经找到了 gzip 头,而实际上它是一个 gzip 成员的压缩数据的一部分。事实上,其中一种 deflate 方法是不进行压缩存储数据,在这种情况下,gzip 流中的 gzip 成员可能会被存储(因为大部分数据都已经被压缩,因此非常可能无法进一步压缩),因此会在 gzip 成员的压缩数据中呈现出一个完全有效的伪造 gzip 头。

使用 DotNetZip 的建议非常好。 GzipStream 中存在许多错误,其中一些在 NET 4.5 中得到修复,而其他明显没有。 Microsoft 可能需要几年时间才能弄清楚如何正确编写该类。 DotNetZip 就可以工作。


关于使用多个 GzipStream 读取整个 gzip 文件,我尝试过这种方法,但并没有成功,猜测可能是由于缓存的原因。我认为使用第三方库是解决问题的途径。 - Sam
使用gzmulti可能是操作多成员gzip文件的另一种选择。 - Mahmoud Mubarak

2

我曾经遇到过DeflateStream的类似问题。

一个简单的方法是将底层流包装在一个流实现中,当调用Read(byte[] buffer, int offset, int count)时,它只返回一个字节。这会阻止DeflateStream/GZipStream的缓冲,使得当第一个流结束时,底层流处于正确的位置。当然,由于增加了对Read的调用次数,这里显然存在效率问题,但根据你的应用程序而言,这可能不是一个问题。

通过深入研究DeflateStream的内部工作原理,也许可以使用反射来重置内部Inflater实例。


如果您能在此处放置一个可以实现此功能的代码,我将测试它与当前解决方案进行比较。如果它更好(应该是这样),您将得到正确的答案。 - Arsen Zahray

1

我已经确认SharpZipLib 0.86.0.518可以读取多成员gzip文件:

using (var fileStream = File.OpenRead(filePath))
using (var gz = new GZipInputStream(fileStream))
{
    //Read from gz here
}

您可以使用NuGet获取它。

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