续上一节内容, 本节主要讲解一下 web 压缩数据的处理方法.
在 HTTP 协议中指出, 可以通过对内容压缩来减少网络流量, 从而提高网络传输的性能.
那么问题来了, 在 HTTP 中, 采用的是什么样的压缩格式和机制呢?
首先呢, 先说压缩格式, 主要有三种:
DEFLATE, 是一种使用 Lempel-Ziv 压缩算法 (LZ77) 和哈夫曼编码的数据压缩格式. 定义于 RFC 1951 : DEFLATE Compressed Data Format Specification http://tools.ietf.org/html/rfc1951 ;
ZLIB, 是一种使用 DEFLATE 的数据压缩格式. 定义于 RFC 1950 : ZLIB Compressed Data Format Specification http://tools.ietf.org/html/rfc1950 ;
GZIP, 是一种使用 DEFLATE 的文件格式. 定义于 RFC 1952 : GZIP file format specification http://tools.ietf.org/html/rfc1952 ;
我们这里就不细琢磨了, 格式里面又有算法, 又有规则什么的, 我也搞不清楚, 说多了, 挨骂...... 理解上, 就相当于我们常用的 Zip,7Zip,RAR 等压缩格式;
但是需要注意的是, ZLIB 和 GZIP 都是使用的 DEFLATE, 这就有点儿意思了, 后面再说:)
说完压缩格式, 再来说机制, 分为两条路子(请求, 回复):
请求: 在 request header 中指定 Accept-Encoding. 例如: Accept-Encoding: gzip, deflate, compress, br;Accept-Encoding 在 Headers 中是可选的, 可以不指定; 当然, 其中还有一些规则, 后面我们结合回复一起给出;
回复: 在 response header 中指定 Content-Encoding. 例如: Content-Encoding: gzip;Content-Encoding 在 Headers 中也是可选的, 可以不指定; 不过现在大多数站点都会对内容进行压缩, 不过通常不会对图片及视频等已经经过压缩的资源进行压缩, 因为得不偿失啊;
来解释一下, 首先客户端 (比如说浏览器) 发出请求, 我们在使用浏览器的过程中, 一般就只是输入一个网址或点击某个连接, 不会刻意去填写一下 Accept-Encoding, 但是浏览器会为我们添加; 这个 Accept-Encoding, 就是告诉网站服务器端, 我 (浏览器) 可以解释这几种压缩格式 (一个列表), 你(网站服务器) 要是压缩, 就给我这几种格式, 否则, 就不要压缩了; 网站服务器端收到请求后, 进行解析, 看看有没有自己能够使用的压缩格式, 如果有, 那么就进行压缩, 如果有多个可以使用, 那就要看优先级, 选择优先级最高的格式进行压缩 (后面列出规则), 并将使用的压缩格式填入 Content-Encoding 中发送回客户端; 客户端(浏览器) 收到回复以后, 就看 Content-Encoding 有没有值, 如果有并且自己也认识, 那么就可以正常解压, 显示在界面上了.
这个就是压缩的机制了, 一切看起来那么的和谐, 但在互联网的世界, 总是不缺乏 "惊喜", 即使客户端不指定任何 Accept-Encoding, 服务器端也会根据情况返回 Content-Encoding, 这就迫使浏览器, 还必须得有两把刷子, 否则就傻眼了.
HTTP Header 中 Accept-Encoding 是浏览器发给服务器, 声明浏览器支持的编码类型[1]
常见的有
- Accept-Encoding: compress, gzip // 支持 compress 和 gzip 类型
- Accept-Encoding: // 默认是 identity
- Accept-Encoding: *// 支持所有类型
- Accept-Encoding: compress;q=0.5, gzip;q=1.0// 按顺序支持 gzip , compress
- Accept-Encoding: gzip;q=1.0, identity; q=0.5, *;q=0 // 按顺序支持 gzip , identity
服务器返回的对应的类型编码 header 是 content-encoding. 服务器处理 accept-encoding 的规则如下所示:
1. 如果服务器可以返回定义在 Accept-Encoding 中的任何一种 Encoding 类型, 那么处理成功(除非 q 的值等于 0, 等于 0 代表不可接受)
2. * 代表任意一种 Encoding 类型 (除了在 Accept-Encoding 中显示定义的类型)
3. 如果有多个 Encoding 同时匹配, 按照 q 值顺序排列
4. identity 总是可被接受的 encoding 类型(除非明确的标记这个类型 q=0)
如果 Accept-Encoding 的值是空, 那么只有 identity 是会被接受的类型
如果 Accept-Encoding 中的所有类型服务器都没法返回, 那么应该返回 406 错误给客户端
如果 request 中没有 Accept-Encoding 那么服务器会假设所有的 Encoding 都是可以被接受的.
如果 Accept-Encoding 中有 identity 那么应该优先返回 identity (除非有 q 值的定义, 或者你认为另外一种类型是更有意义的)
注意:
如果服务器不支持 identity 并且浏览器没有发送 Accept-Encoding, 那么服务器应该倾向于使用 HTTP1.0 中的 "gzip" and "compress" , 服务器可能按照客户端类型发送更适合的 encoding 类型
大部分 HTTP1.0 的客户端无法处理 q 值
Accept-Encoding 与 Content-Encoding 的规则
Accept-Encoding 与 Content-Encoding 的对应规则
另外, 需要额外说明的是, 在 Accept-Encoding 中指定的 delfate, 可不一定是 DEFLATE 压缩格式, 按照官方的说法:
gzip, 一种由文件压缩程序「Gzip,GUN zip」产生的编码格式, 描述于 RFC 1952. 这种编码格式是一种具有 32 位 CRC 的 Lempel-Ziv 编码(LZ77);
deflate, 由定义于 RFC 1950 的「ZLIB」编码格式与 RFC 1951 中描述的「DEFLATE」压缩机制组合而成的产物;
也就是说, deflate 其实对应的应该是 ZLIB 压缩格式, 而它的名字, 又与 DEFLATE 格式重名(估计这位同仁会被祭天了吧), 导致很多浏览器厂商不知道究竟该用哪种格式来解释 Content-Encoding: deflate, 因为不论你选择哪种, 都会有例外发生, 这就尴尬了. 所以, 尽管 deflate 的压缩效果要比 gzip 好, 但还是会被不少 Web-Server 放弃或者降低优先级. 这也就是为什么我们会经常看到 Content-Encoding: gzip 而很少能看到 Content-Encoding: deflate 的原因; 所以, 我们在做爬虫的时候, 也应该尽量避免使用 deflate, 减少不必要的麻烦.
话锋一转, 回到我们的爬虫, 也会遇到上面浏览器遇到的尴尬场面, 所以, 就必须得事先准备好常用的解压缩方式, 要不然, 数据抓下来了, 读不出来, 你说气不气~
本节中, 我们就来继续改造我们的爬虫框架, 让它也有两把刷子:)
[Code 2.3.1]
- public static byte[] DecompressStreamData(Stream sourceStream, String contentEncoding)
- {
- var _stream = sourceStream;
- switch ((contentEncoding ?? string.Empty).ToLower())
- {
- case "gzip":
- _stream = new GZipStream(sourceStream, CompressionMode.Decompress);
- break;
- case "deflate":
- _stream = new DeflateStream(sourceStream, CompressionMode.Decompress);
- break;
- default:
- break;
- }
- using (var memory = new MemoryStream())
- {
- int length = 256;
- Byte[] buffer = new Byte[length];
- int bytesRead = _stream.Read(buffer, 0, length);
- while (bytesRead> 0)
- {
- memory.Write(buffer, 0, bytesRead);
- bytesRead = _stream.Read(buffer, 0, length);
- }
- return memory.ToArray();
- }
- }
DecompressStreamData 静态方法
这是一个公共静态方法, 其目的就是将原数据流中的数据转换为 byte[]数组, 其中, 如果指定了压缩格式, 就会使用适当的方法进行解压. 这里只提供了最常见的 gzip 和不推荐的 deflate 两种格式, 可以自行扩展.
接下来, 就是对工蚁 (WorkerAnt) 进行改造了.
[Code 2.3.2]
- private void GetResponse(JobContext context)
- {
- context.Request.BeginGetResponse(new AsyncCallback(acGetResponse =>
- {
- var contextGetResponse = acGetResponse.AsyncState as JobContext;
- using (contextGetResponse.Response = contextGetResponse.Request.EndGetResponse(acGetResponse))
- using (contextGetResponse.ResponseStream = contextGetResponse.Response.GetResponseStream())
- using (contextGetResponse.Memory = new MemoryStream())
- {
- // 此处省略 N 行......
- if (TaskStatus.Running == contextGetResponse.JobStatus)
- {
- if (!String.IsNullOrEmpty(contextGetResponse.Response.Headers["Content-Encoding"]))
- {
- contextGetResponse.Memory.Seek(0, SeekOrigin.Begin);
- contextGetResponse.Buffer = DecompressStreamData(contextGetResponse.Memory
- , contextGetResponse.Response.Headers["Content-Encoding"]);
- //contextGetResponse.Buffer = contextGetResponse.Memory.ToArray();
- }
- else
- contextGetResponse.Buffer = contextGetResponse.Memory.ToArray();
- contextGetResponse.JobStatus = TaskStatus.RanToCompletion;
- NotifyStatusChanged(new JobEventArgs { Context = context, EventAnt = this, });
- }
- contextGetResponse.Buffer = null;
- }
- }), context);
- }
改造 WorkerAnt 的 GetResponse 方法
注释中是原来使用的方法, 现在用上面的 DecompressStreamData 替换掉了.
这样我们在收到采集完成事件通知时, 就可以得到解压缩后的数据了:
[Code 2.3.3]
- switch (args.Context.JobStatus)
- {
- // 此处省略 N 行......
- case TaskStatus.RanToCompletion:
- if (null != args.Context.Buffer && 0 <args.Context.Buffer.Length)
- {
- Task.Factory.StartNew(oBuffer =>
- {
- var content = new UTF8Encoding(false).GetString((byte[])oBuffer);
- richOutput.EndInvoke(richOutput.BeginInvoke(new MethodInvoker(() => { richOutput.Text = content; })));
- }, args.Context.Buffer, TaskCreationOptions.LongRunning);
- }
- if (null != args.Context.Watch)
- Console.WriteLine("/* ********************** using {0}ms / request ******************** */"
- + Environment.NewLine + Environment.NewLine, (args.Context.Watch.Elapsed.TotalMilliseconds / 100).ToString("000.00"));
- break;
- // 此处省略 N 行......
- default:/* Do nothing on this even. */
- break;
- }
改造应用中对事件的处理
至于为何在 Complete 事件的位置处理解压缩, 而不在 Running 事件的位置, 这是 gzip 的限制, 它具有 CRC 校验位, CRC 的算法, 大家可以在网上搜索, 大体上说, 就是遍历一遍所有数据, 进行与或计算, 最终得到一个校验位, 来保证数据的完整性与正确性. 这也导致我们无法对中间数据进行解压, 因为没有校验位, 对末尾数据解压, 又因数据不全, CRC 计算结果也不会对.
至此, 我们就完成了对 HTTP 协议内容部分已压缩数据的处理, 抛砖引玉, 可以实现更多种压缩格式的处理;
节外生枝:
来源: https://www.cnblogs.com/mikecheers/p/12210904.html