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

第一个流读取正常,但第二个流没有读取。

如何阅读第二个流?

4

4 回答 4

5

这是 GzipStream 处理具有多个 gzip 条目的 gzip 文件的方式的问题。它读取第一个条目,并将所有后续条目视为垃圾(有趣的是,gzip 和 winzip 等实用程序通过将它们全部提取到一个文件中来正确处理它)。有几种解决方法,或者您可以使用第三方实用程序,如DotNetZip ( http://dotnetzip.codeplex.com/ )。

也许最简单的方法是扫描文件中的所有 gzip 标头,然后手动将流移动到每个标头并解压缩内容。这可以通过在原始文件字节中查找 ID1、ID2 和 0x8 来完成(Deflate 压缩方法,参见规范:http ://www.gzip.org/zlib/rfc-gzip.html )。这并不总是足以保证您正在查看 gzip 标头,因此您需要读取标头的其余部分(或至少前十个字节)以验证:

    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 规范。

于 2013-03-09T08:59:26.550 回答
5

多部分 gzip 处理似乎现在在 .NET Core 中实现。此讨论对于 .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 中修复,而一些显然没有。微软可能需要几年时间才能弄清楚如何正确编写该类。DotNetZip 可以正常工作。

于 2013-03-09T18:42:26.317 回答
2

DeflateStream 也有类似的问题。

一种简单的方法是将底层 Stream 包装在 Stream 实现中,当调用 Read(byte[] buffer, int offset, int count) 时,该实现只返回一个字节。这会阻碍 DeflateStream/GZipStream 的缓冲,当到达第一个流的末尾时,将底层流留在正确的位置。当然,由于对 Read 的调用次数增加,这里显然效率低下,但这可能不是问题,具体取决于您的应用程序。

深入了解 DeflateStream 的内部结构,可以使用反射来重置内部 Inflater 实例。

于 2014-02-27T08:25:36.107 回答
1

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

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

您可以使用 NuGet 获取它。

于 2017-03-16T00:09:11.930 回答