【问题标题】:GZipStream from MemoryStream only returns a few hundred bytesMemoryStream 中的 GZipStream 只返回几百个字节
【发布时间】:2017-12-10 21:47:29
【问题描述】:

我正在尝试下载一个几百 MB 的 .gz 文件,并在 C# 中将其转换为一个很长的字符串。

using (var memstream = new MemoryStream(new WebClient().DownloadData(url)))
using (GZipStream gs = new GZipStream(memstream, CompressionMode.Decompress))
using (var outmemstream = new MemoryStream())
{
    gs.CopyTo(outmemstream);
    string t = Encoding.UTF8.GetString(outmemstream.ToArray());
    Console.WriteLine(t);
}

我的测试网址:https://commoncrawl.s3.amazonaws.com/crawl-data/CC-MAIN-2017-47/segments/1510934803848.60/wat/CC-MAIN-20171117170336-20171117190336-00002.warc.wat.gz

memstream 的长度为 283063949。程序在它初始化的那一行停留了大约 15 秒,而我的网络在此期间出现了问题,这是有道理的。

outmemstream 的长度只有 548。

写入命令行的是压缩文档的第一行。他们没有乱码。我不确定如何获得其余部分。

【问题讨论】:

  • 您是否确认这适用于较小的文件?说,一兆字节左右?您确定原始的未压缩文件是 UTF-8 编码的吗?您是否将outmemstream 的内容写入文件以查看其中的内容?你确定你没有内存不足?该代码看起来会占用超过 2 GB 的 RAM(memstream 为 280 MB,未压缩数据可能会增加一倍,而您正在创建的字符串则再次增加一倍)。
  • @JimMischel 实际上比这更糟。压缩数据似乎是 ASCII 文本。 1.2GB 用于未压缩数据,是字符串表示的两倍。
  • 这里是 dotnet/corefx 的拉取请求:“添加对级联 GZip 流的支持。#30442”github.com/dotnet/corefx/pull/30442
  • @StanislavPrusac 拉取请求 #30442 已合并。但我仍然看到仅在 .net 4.6.1 中使用上述代码提取的几个字节。有什么建议吗?

标签: c# gzip memorystream


【解决方案1】:

.NET GZipStream 解压缩纯文本的前 548 个字节,即文件中的所有第一条记录。 7Zip 将整个文件提取为一个 1.2GB 的输出文件,但它是纯文本(大约 130 万行),没有记录分隔符,当我在 7Zip 中测试该文件时,它报告了 1,441 个字节。

我检查了一些东西,但找不到一个可以直接解压这个东西的压缩库。

在文件中进行了一些转换后,我发现 1,441 字节是 ISIZE 的值,这通常是 gzip 文件的最后 4 个字节,是附加到压缩文件的 8 字节页脚记录的一部分数据块。

事实证明,您拥有的是一大组串联在一起的 .gz 文件。虽然这完全是一件令人头疼的事情,但有几种方法可以解决这个问题。

首先是扫描压缩文件中的 gzip 标头签名字节:0x1F0x8B。当您找到这些时,您将(通常)在流中拥有每个 .gz 文件的开头。您可以在文件中构建一个偏移量列表,然后提取文件的每个块并解压缩。

另一种选择是使用一个库,该库将报告从输入流中消耗的字节数。由于几乎所有的解压器都使用某种缓冲,你会发现输入流的移动比消耗的字节数要远得多,因此很难直接猜测。但是,DotNetZip 流将为您提供实际消耗的输入字节,您可以使用它来计算下一个起始位置。这将允许您将文件作为流处理并单独提取每个文件。

不管怎样,都不快。

这是第二个选项的方法,使用 DotNetZip 库:

public static IEnumerable<byte[]> UnpackCompositeFile(string filename)
{
    using (var fstream = File.OpenRead(filename))
    {
        long offset = 0;
        while (offset < fstream.Length)
        {
            fstream.Position = offset;
            byte[] bytes = null;
            using (var ms = new MemoryStream())
            using (var unpack = new Ionic.Zlib.GZipStream(fstream, Ionic.Zlib.CompressionMode.Decompress, true))
            {
                unpack.CopyTo(ms);
                bytes = ms.ToArray();
                // Total compressed bytes read, plus 10 for GZip header, plus 8 for GZip footer
                offset += unpack.TotalIn + 18;
            }
            yield return bytes;
        }
    }
}

它很丑而且速度不快(我花了大约 48 秒来解压整个文件),但它似乎可以工作。每个byte[] 输出代表流中的一个压缩文件。这些可以用System.Text.Encoding.UTF8.GetString(...)转成字符串,然后解析提取含义。

文件中的最后一项如下所示:

WARC/1.0
WARC-Type: metadata
WARC-Target-URI: https://zverek-shop.ru/dljasobak/ruletka_sobaki/ruletka-tros_standard_5_m_dlya_sobak_do_20_kg
WARC-Date: 2017-11-25T14:16:01Z
WARC-Record-ID: <urn:uuid:e19ef645-b057-4305-819f-7be2687c3f19>
WARC-Refers-To: <urn:uuid:df5de410-d4af-45ce-b545-c699e535765f>
Content-Type: application/json
Content-Length: 1075

{"Container":{"Filename":"CC-MAIN-20171117170336-20171117190336-00002.warc.gz","Compressed":true,"Offset":"904209205","Gzip-Metadata":{"Inflated-Length":"463","Footer-Length":"8","Inflated-CRC":"1610542914","Deflate-Length":"335","Header-Length":"10"}},"Envelope":{"Format":"WARC","WARC-Header-Length":"438","Actual-Content-Length":"21","WARC-Header-Metadata":{"WARC-Target-URI":"https://zverek-shop.ru/dljasobak/ruletka_sobaki/ruletka-tros_standard_5_m_dlya_sobak_do_20_kg","WARC-Warcinfo-ID":"<urn:uuid:283e4862-166e-424c-b8fd-023bfb4f18f2>","WARC-Concurrent-To":"<urn:uuid:ca594c00-269b-4690-b514-f2bfc39c2d69>","WARC-Date":"2017-11-17T17:43:04Z","Content-Length":"21","WARC-Record-ID":"<urn:uuid:df5de410-d4af-45ce-b545-c699e535765f>","WARC-Type":"metadata","Content-Type":"application/warc-fields"},"Block-Digest":"sha1:4SKCIFKJX5QWLVICLR5Y2BYE6IBVMO3Z","Payload-Metadata":{"Actual-Content-Type":"application/metadata-fields","WARC-Metadata-Metadata":{"Metadata-Records":[{"Value":"1140","Name":"fetchTimeMs"}]},"Actual-Content-Length":"21","Trailing-Slop-Length":"0"}}}

这是占用1441字节的记录,包括它后面的两个空行。


只是为了完整性...

TotalIn 属性返回读取的压缩字节数,不包括 GZip 页眉和页脚。在上面的代码中,我使用 18 字节的常量作为页眉和页脚大小,这是 GZip 的最小大小。虽然这适用于该文件,但处理串联 GZip 文件的其他任何人可能会发现标头中有额外的数据使其变大,这将阻止上述工作。

在这种情况下,您有两个选择:

  • 直接解析GZip header,使用DeflateStream解压。
  • 扫描从 TotalIn + 18 字节开始的 GZip 签名字节。

两者都应该工作而不会太慢。由于缓冲发生在解压缩代码中,您将不得不在每个段之后向后寻找流,因此读取一些额外的字节不会让您减慢太多。

【讨论】:

  • 我没有提到的一件事:它可能不是一个有效的 gzip 文件。那里的调查很好。
  • @JimMischel 谢谢……这是一个有趣的谜题。 7Zip 提取整个内容的事实有点令人困惑。该死的那些人效率高:P
  • Multipart gzip 处理似乎现在在 .NET Core 中实现(在我的机器上使用 .NET 5):github.com/dotnet/runtime/issues/25105
  • @Jacob 是的,底层的原生库支持它,所以他们修复了流包装器中的 gzip 处理。他们还在即将发布的版本中添加了适当的 zlib 流封装。
  • @dcarl661 嗯,是的,那应该是fstream.Position = offset; 我的错。这已经超过 4 年了,而您是第一个发现它的人。干得好:P
【解决方案2】:

这是一个有效的 gzip 流,可通过 gzip 解压缩。根据标准 (RFC 1952),有效 gzip 流的串联也是有效的 gzip 流。您的文件是 118,644 (!) 个原子 gzip 流的串联。第一个原子 gzip 流长 382 个字节,产生 548 个未压缩字节。这就是你得到的全部。

显然GzipStream 类有一个错误,它在完成第一个解压缩后不寻找另一个原子 gzip 流,因此不遵守 RFC 1952。您可以自己在循环,直到到达输入文件的末尾。

附带说明,文件中每个 gzip 流的小尺寸是相当低效的。压缩机需要更多的数据才能滚动。如果将该数据压缩为单个原子 gzip 流,它将压缩为 195,606,385 字节而不是 283,063,949 字节。即使有很多块,它也会压缩到大致相同的大小,只要这些块的大小更像 1 兆字节或更大,而不是每块有数百到平均 10K 字节。

【讨论】:

  • 嗨,马克。错过了你的答案,但有人刚刚 ping 我并再次引起了我的注意。我确实尝试了一些不同的压缩库和存档程序,只有 7Zip 存档器让我按原样提取文件。似乎几乎每个人都认为每个文件中只存在一个成员。至少在 Windows 上 ;)
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-08-11
  • 2012-10-10
  • 2018-11-16
  • 1970-01-01
相关资源
最近更新 更多