来自 MemoryStream 的 GZipStream 只返回几百个字节



我正在尝试下载一个几百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。

写入命令行的是压缩文档的第一行。它们不是乱码。我不知道如何得到其余的。

.NETGZipStream解压缩纯文本的前 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"}}}

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


只是为了完整...

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

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

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

两者都应该在不减慢您速度的情况下工作。 由于缓冲发生在解压缩代码中,因此您必须在每个段之后向后查找流,因此读取一些额外的字节不会减慢太多速度。

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

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

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

最新更新