仅来自 MemoryStream 的 GZipStream returns 几百个字节

GZipStream from MemoryStream only returns a few hundred bytes

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

我的测试URL: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。

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

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

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

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

事实证明,您拥有的是一大堆 .gz 文件串联在一起。虽然这完全是个难题,但您可以通过几种方法来解决这个问题。

首先是扫描压缩文件中的 gzip header 签名字节: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 属性 returns 读取的压缩字节数,不包括GZip header 和页脚。在上面的代码中,我为 header 和页脚大小使用了一个常量 18 字节,这是 GZip 的最小大小。虽然这适用于此文件,但处理串联 GZip 文件的任何其他人可能会发现 header 中有其他数据使其变大,这将阻止上述工作。

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

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

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

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

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

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