我正在尝试下载几百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);
}
memstream的长度为283063949.程序在初始化的行上持续了大约15秒,并且我的网络在其中被覆盖,这是有道理的。
outmemstream的长度仅为548。
写入命令行是压缩文档的第一行。它们没有乱码。我不知道怎么做其余的。
答案 0 :(得分:4)
.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文件的任何其他人都可能会发现标题中有额外的数据使其变大,这将阻止上述工作。
在这种情况下,您有两个选择:
DeflateStream
进行解压缩。TotalIn + 18
字节开始的GZip签名字节。要么工作要么不要太慢你。由于在解压缩代码中发生缓冲,因此您必须在每个段之后向后搜索流,因此读取一些额外的字节不会减慢您的速度。
答案 1 :(得分:1)
这是一个有效的gzip流,可以通过gzip解压缩。根据标准(RFC 1952),有效gzip流的串联也是有效的gzip流。您的文件是118,644(!)个原子gzip流的串联。第一个原子gzip流长382个字节,产生548个未压缩字节。这就是你得到的全部。
显然GzipStream
类有一个错误,它在完成第一个解压缩之后不会寻找另一个原子gzip流,因此不遵守RFC 1952.你可以自己做在循环中,直到到达输入文件的末尾。
作为旁注,文件中每个gzip流的小尺寸效率很低。压缩机需要的数据多于滚动数据。如果该数据被压缩为单个原子gzip流,则压缩为195,606,385字节而不是283,063,949字节。它可以压缩到大约相同的大小,即使有很多碎片,只要碎片大小更像是一个兆字节或更多,而不是每个碎片的数百到平均10K字节。