c# – 来自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.

写入命令行是压缩文档的第一行.它们没有乱码.我不知道怎么做其余的.

解决方法:

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

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

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

事实证明,你拥有的是一大堆连接在一起的.gz文件.虽然这是一个完全痛苦的屁股,但有几种方法可以解决这个问题.

第一种是扫描压缩文件中的gzip头签名字节:0x1F和0x8B.当您找到这些时,您将(通常)在流中包含每个.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 = p;
            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签名字节.

两者都应该工作而不会减慢你的速度.由于在解压缩代码中发生缓冲,因此您必须在每个段之后向后搜索流,因此读取一些额外的字节不会减慢您的速度.

上一篇:c# – 使用BinaryFormatter反序列化序列化数据时的异常


下一篇:[GO] go-fly客服系统0.2.2打包编译版下载