续上一节内容,本节主要讲解一下Web压缩数据的处理方法。

在HTTP协议中指出,可以通过对内容压缩来减少网络流量,从而提高网络传输的性能。

那么问题来了,在HTTP中,采用的是什么样的压缩格式和机制呢?

首先呢,先说压缩格式,主要有三种:

我们这里就不细琢磨了,格式里面又有算法,又有规则什么的,我也搞不清楚,说多了,挨骂……理解上,就相当于我们常用的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 = ;
Byte[] buffer = new Byte[length];
int bytesRead = _stream.Read(buffer, , length);
while (bytesRead > )
{
memory.Write(buffer, , bytesRead);
bytesRead = _stream.Read(buffer, , 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(, 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 && < 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 / ).ToString("000.00"));
break;
// 此处省略N行……
default:/* Do nothing on this even. */
break;
}

改造应用中对事件的处理

至于为何在Complete事件的位置处理解压缩,而不在Running事件的位置,这是gzip的限制,它具有CRC校验位,CRC的算法,大家可以在网上搜索,大体上说,就是遍历一遍所有数据,进行与或计算,最终得到一个校验位,来保证数据的完整性与正确性。这也导致我们无法对中间数据进行解压,因为没有校验位,对末尾数据解压,又因数据不全,CRC计算结果也不会对。

至此,我们就完成了对HTTP协议内容部分已压缩数据的处理,抛砖引玉,可以实现更多种压缩格式的处理;

节外生枝:

  • 本节讲述的数据压缩,指的是HTTP协议中,对协议内容部分的压缩,在HTTP 2.x的版本中,增加了对协议头部的压缩(更确切的说是缓存)的机制,用空间换时间,由于2.x版本Schema为HTTPS,处理起来,另有蹊跷,本节先不做深入介绍了,可作为延伸内容,有兴趣的童鞋可以搜索相关主题;
  • 为了方便以后的做更多更好的案例,源码中增加了一个WinForm项目,这样在切换Uri的时候,就更方便一些;

喜欢本系列丛书的朋友,可以点击链接加入QQ交流群(994761602)【C# 破境之道】
方便各位在有疑问的时候可以及时给我个反馈。同时,也算是给各位志同道合的朋友提供一个交流的平台。
需要源码的童鞋,也可以在群文件中获取最新源代码。

最新文章

  1. 列表屏幕(List Screen)
  2. Oracle 索引&lt;七&gt;
  3. 百科编辑器ueditor应用笔记
  4. 批量修改照片名称的shell脚本
  5. Codeforces Round #358 (Div. 2) D. Alyona and Strings 字符串dp
  6. mysql 数据库连接(远程和本地原理同样)
  7. Service初步了解
  8. Swift编程语言学习9—— 存储属性和计算属性
  9. WPF换肤之七:异步
  10. springmvc实现long-pulling技术
  11. 匹配替换第n个字符串
  12. deeplearning.ai 卷积神经网络 Week 2 深度卷积网络:实例研究 听课笔记
  13. [Luogu 2816]宋荣子搭积木
  14. (转)JAVA HashSet 去除重复值原理
  15. Python数据描述与分析
  16. Python交互图表可视化Bokeh:1. 可视交互化原理| 基本设置
  17. js字符串方法汇总
  18. perl 信号
  19. hadoop之 reduce个数控制
  20. JavaScript面向对象编程指南(第2版)》读书笔记

热门文章

  1. 模板——BigInteger
  2. window 系统下修改`CMD`的编码格式的方法,`CHCP` 的 使用
  3. springdata jpa使用Example快速实现动态查询
  4. Total Commander 显示文件包含文件名扩展
  5. 2018-2-13-win10-uwp-图标制作器
  6. vue-learning:8-template-v-on-and-modifier
  7. 【2016常州一中夏令营Day4】
  8. P4556 [Vani有约会]雨天的尾巴 (线段树合并)
  9. codeforces 1183F 离散化枚举 约数定理
  10. java 如何重写equals