如何从包含多个GzipStreams的文件中读取

本文关键字:文件 读取 GzipStreams 包含多 | 更新日期: 2023-09-27 18:02:53

我已经用代码创建了一个文件,看起来像这样:

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

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

如何读取第二个流?

如何从包含多个GzipStreams的文件中读取

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

多部分gzip处理现在似乎在。net Core中实现了。此讨论对于。net框架仍然有效。


这是GzipStream中的一个bug。根据RFC 1952对gzip格式的规范:

2.2。文件格式

gzip文件由一系列"成员"组成。(压缩数据集)。每个成员的格式如下所示部分。成员只是一个接一个地出现在文件中,在它们之前、之间或之后没有任何附加信息。

所以一个兼容的解压缩程序需要在前一个gzip成员之后立即查找另一个gzip成员。

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

压缩数据可以有任何字节模式,所以有可能被愚弄,认为你已经找到了一个gzip头,当它实际上是gzip成员的压缩数据的一部分。事实上,deflate方法之一是存储不压缩的数据,在这种情况下,可能会存储在gzip成员中压缩的gzip流(因为大部分数据都被压缩了,因此很可能无法进一步压缩),因此会在gzip成员的压缩数据中间呈现一个完全有效的伪gzip头。

使用DotNetZip的建议是一个很好的建议。GzipStream中有很多bug,其中一些在。NET 4.5中得到了修复,还有一些显然没有修复。微软可能还需要几年时间才能弄清楚如何正确编写这个类。DotNetZip只是工作

我在使用DeflateStream时也遇到过类似的问题。

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

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

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

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

您可以使用NuGet获取