《C# 爬虫 破境之道》:第一境 爬虫原理 — 第五节:数据流处理的那些事儿

为什么说到数据流了呢,因为上一节中介绍了一下异步发送请求。同样,在数据流的处理上,C#也为我们提供几个有用的异步处理方法。而且,爬虫这生物,处理数据流是基础本能,比较重要。本着这个原则,就聊一聊吧。

我们经常使用到的流有文件流、内存流、网络流,爬虫与这三种流都有着密不可分的联系,可以联想以下这些场景:

  • 当我们采集的数据,是一个压缩包或者照片,那么要存储它们到硬盘上,就需要使用到文件流了;
  • 当我们采集的数据,是经过GZip等压缩算法压缩过的,那么要解压它,就需要使用到内存流了;
  • 当我们的爬虫运行起来,就需要用到网络了,使用网络流是必然不可缺少的了;

所以,对流的操作,也是一个必要重要的环节;除了上面列举的几个场景之外,还有很多场景会涉及到流的处理,就不一一列举了,数不胜数;但每种流的处理,都对应其相应的I/O操作。所以,在DotNetFramework中,封装了System.IO.Stream这个基础流,在其基础之上,派生出很多有用的流;

我们在这里结合上一节中第一种异步请求方式的案例,来讲述爬虫中的网络流处理,其他类型的流处理,也是触类旁通的,文件流、内存流,在后续章节中,都会有所涉及,只是不会当作专题来讲解了。

在爬虫中,我们主要面临的网络流,有两个:

  • RequestStream:请求流
  • ResponseStream:回复流

当然,这里说的爬虫,还很小,只是基于WebRequest、WebResponse的,等后面我们再继续下沉,让它再成长成长,到Socket层面,我们要处理的网络流主要就是System.Net.Sockets.NetworkStream了,不过先不急,以小见大,也是很好的事情:)

至于为什么要使用流,上一节中已经举例说明了,这里就不再赘述。

第一部分:同步方式处理数据流

[Code 5.1.1]

 1 {
 2     Stopwatch watch = new Stopwatch();
 3     Console.WriteLine("/* ********** 异步请求方式 * BeginGetResponse() & EndGetResponse() **********/");
 4     watch.Start();
 5     {
 6         var request = WebRequest.Create(@"https://tool.runoob.com/compile.php");
 7         request.Method = WebRequestMethods.Http.Post;
 8         request.ContentType = @"application/x-www-form-urlencoded; charset=UTF-8";
 9
10         var requestDataBuilder = new StringBuilder();
11         requestDataBuilder.AppendLine("using System;");
12         requestDataBuilder.AppendLine("namespace HelloWorldApplication");
13         requestDataBuilder.AppendLine("{");
14         requestDataBuilder.AppendLine("    class HelloWorld");
15         requestDataBuilder.AppendLine("    {");
16         requestDataBuilder.AppendLine("        static void Main(string[] args)");
17         requestDataBuilder.AppendLine("        {");
18         requestDataBuilder.AppendLine("            Console.WriteLine(\"《C# 爬虫 破境之道》\");");
19         requestDataBuilder.AppendLine("        }");
20         requestDataBuilder.AppendLine("    }");
21         requestDataBuilder.AppendLine("}");
22
23         var requestData = Encoding.UTF8.GetBytes(@"code=" + System.Web.HttpUtility.UrlEncode(requestDataBuilder.ToString())
24             + @"&token=4381fe197827ec87cbac9552f14ec62a&language=10&fileext=cs");
25         requestDataBuilder.Clear();
26         request.ContentLength = requestData.Length;
27         var requestStream = request.GetRequestStream();
28         requestStream.Write(requestData, 0, requestData.Length);
29         request.BeginGetResponse(new AsyncCallback(ar =>
30         {
31             using (var response = (ar.AsyncState as WebRequest).EndGetResponse(ar))
32             {
33                 using (var stream = response.GetResponseStream())
34                 {
35                     using (var reader = new StreamReader(stream, new UTF8Encoding(false)))
36                     {
37                         var content = reader.ReadToEnd();
38                         Console.WriteLine(content.Length > 100 ? content.Substring(0, 90) + "..." : content);
39                     }
40                 }
41                 response.Close();
42             }
43
44             watch.Stop();
45             Console.WriteLine("/* ********************** using {0}ms / request  ******************** */"
46                 + Environment.NewLine + Environment.NewLine, (watch.Elapsed.TotalMilliseconds / 100).ToString("000.00"));
47         }), request);
48     }
49 }

同步方式处理网络流

相对于异步发送的案例,代码的变动主要在第7行到第28行。

首先7、8行,为request的两个属性赋值发生了变化,我们要操作RequestStream,一定要指定合适的Method,POST或PUT等,其他的Method并不支持对流操作,就会出错;另外就是然使用流了,流里的数据到底是个什么,服务器端应该如何解释,可以通过ContentType来指定,有时候服务器端并不是那么严谨,可能稀里糊涂的也就过去了;

接下来,第10~21行,我构建了一个字符串,作为要提交的主体数据,在第23行,将字符串转换为字节数组;对流操作,字节数组和编码都是跑不掉的,时而绕晕,时而迷糊,也是很正常的:)

第26行,指定填充到数据流的数据长度;说到这个长度,再啰嗦一下HTTP协议,用Wireshark随便抓个包当个栗子

[Code 5.1.2]

 1 HTTP/1.1 200 OK
 2 Date: Fri, 10 Jan 2020 08:10:02 GMT
 3 Server: Apache/2.2.9 (APMServ) PHP/5.2.6
 4 Last-Modified: Sun, 05 Jan 2020 10:29:06 GMT
 5 ETag: "29000000008812-616-59b620334ab88"
 6 Accept-Ranges: bytes
 7 Content-Length: 1558  <-----------这里不对,是因为我把下面xml精简了一下,要不太长。
 8 Content-Type: application/xml
 9
10 <?xml version="1.0" encoding="gb2312"?>
11 <root>
12 <FileList>
13     <FileName version="20181122">sound/FaceSuccess.wav</FileName>
14 </FileList>
15 </root>

某请求的回复报文

第1行,请求和回复不太一样,具体就不说了,大体就是HTTP协议的版本、URI地址、状态等;

第2~8行,就是协议头,一行一对,对应我们WebRequest和WebResponse里的Headers;

第9行,是一个空行(\r\n),不要以为是我为了美观加的,这也是协议的一部分;它的作用就是来分隔协议头和协议体的;

从第10行到第15行,就是协议体,也就是我们流中的内容了。

再回到我们刚才说的ContentLength属性,这个属性的值,其实就是协议体(报文中第10到15行)的字节长度;

WebRequest和WebResponse都有这个属性,这样,就给我们一个制作进度条的可能性,比如下载一个AV,可以显示已经下载了多少了,占比是多少,之类的。但为什么说是可能性呢,因为这个属性,无论是Request还是Response的时候,都可以不指定,它有默认值:-1;也就是说,当ContentLength==-1的时候,数据的长度将以实际发送或收到的数据长度为准,这就对数据的完整性校验和传送进度的统计产生了困难。所以,我们最好在刚开始学习的时候,就养成为它们赋值的好习惯;就啰嗦这么多吧。

再回到[Code 5.1.1] 中,继续第27行,这里就是以同步的方式来获取请求流了,线程将在这里阻塞。同样,第33行获取回复流,也是同样的道理。

其余就是写流、读流的操作,没什么好说的了~

第二部分:异步方式处理数据流

首先,我们定义一个结构体[WebAsyncContext],用来存储上下文中使用的变量;

[Code 5.2.1]

1 public class WebAsyncContext
2 {
3     public System.Net.WebRequest Request { get; set; }
4     public System.Net.WebResponse Response { get; set; }
5     public System.IO.Stream RequestStream { get; set; }
6     public System.IO.Stream ResponseStream { get; set; }
7     public System.IO.MemoryStream Memory { get; set; }
8     public byte[] Buffer { get; set; }
9 }

WebAsyncContext

比较简单,不做解释了,接下来,就是一票子异步操作了,别眨眼~

[Code 5.2.2]

 1 {
 2     Stopwatch watch = new Stopwatch();
 3     Console.WriteLine("/* ********** 异步请求方式 * 异步方式处理数据流 **********/");
 4     watch.Start();
 5     {
 6         var requestDataBuilder = new StringBuilder();
 7         requestDataBuilder.AppendLine("using System;");
 8         requestDataBuilder.AppendLine("namespace HelloWorldApplication");
 9         requestDataBuilder.AppendLine("{");
10         requestDataBuilder.AppendLine("    class HelloWorld");
11         requestDataBuilder.AppendLine("    {");
12         requestDataBuilder.AppendLine("        static void Main(string[] args)");
13         requestDataBuilder.AppendLine("        {");
14         requestDataBuilder.AppendLine("            Console.WriteLine(\"《C# 爬虫 破境之道》\");");
15         requestDataBuilder.AppendLine("        }");
16         requestDataBuilder.AppendLine("    }");
17         requestDataBuilder.AppendLine("}");
18
19         var requestData = Encoding.UTF8.GetBytes(@"code=" + System.Web.HttpUtility.UrlEncode(requestDataBuilder.ToString())
20             + @"&token=4381fe197827ec87cbac9552f14ec62a&language=10&fileext=cs");
21
22         var context = new WebAsyncContext { Request = WebRequest.Create(@"https://tool.runoob.com/compile.php"), Buffer = requestData };
23
24         requestData = null;
25         requestDataBuilder.Clear();
26
27         context.Request.ContentLength = context.Buffer.Length;
28         context.Request.Method = WebRequestMethods.Http.Post;
29         context.Request.ContentType = @"application/x-www-form-urlencoded; charset=UTF-8";
30         context.Request.BeginGetRequestStream(acGetRequestStream =>
31         {
32             var contextGetRequestStream = acGetRequestStream.AsyncState as WebAsyncContext;
33             contextGetRequestStream.RequestStream = contextGetRequestStream.Request.EndGetRequestStream(acGetRequestStream);
34             contextGetRequestStream.RequestStream.BeginWrite(contextGetRequestStream.Buffer, 0, contextGetRequestStream.Buffer.Length, acWriteStream =>
35             {
36                 var contextWriteRequestStream = acWriteStream.AsyncState as WebAsyncContext;
37                 contextWriteRequestStream.RequestStream.EndWrite(acWriteStream);
38                 contextWriteRequestStream.Request.BeginGetResponse(new AsyncCallback(acGetResponse =>
39                 {
40                     var contextGetResponse = acGetResponse.AsyncState as WebAsyncContext;
41                     using (contextGetResponse.Response = contextGetResponse.Request.EndGetResponse(acGetResponse))
42                     using (contextGetResponse.ResponseStream = contextGetResponse.Response.GetResponseStream())
43                     using (contextGetResponse.Memory = new MemoryStream())
44                     {
45                         contextGetResponse.Buffer = new Byte[512];
46                         var readCount = 0;
47                         do
48                         {
49                             var acReadStreamResult = contextGetResponse.ResponseStream.BeginRead(contextGetResponse.Buffer, 0, contextGetResponse.Buffer.Length, acReadStream =>
50                             {
51                                 readCount = (acReadStream.AsyncState as WebAsyncContext).ResponseStream.EndRead(acReadStream);
52                                 contextGetResponse.Memory.Write(contextGetResponse.Buffer, 0, readCount);
53                             }, contextGetResponse);
54
55                             acReadStreamResult.AsyncWaitHandle.WaitOne();
56                         } while (readCount > 0);
57
58                         contextGetResponse.RequestStream.Close();
59                         contextGetResponse.Request.Abort();
60                         contextGetResponse.Response.Close();
61                         contextGetResponse.Buffer = null;
62
63                         var content = new UTF8Encoding(false).GetString(contextGetResponse.Memory.ToArray());
64                         Console.WriteLine(content.Length > 100 ? content.Substring(0, 90) + "..." : content);
65
66                         watch.Stop();
67                         Console.WriteLine("/* ********************** using {0}ms / request  ******************** */"
68                             + Environment.NewLine + Environment.NewLine, (watch.Elapsed.TotalMilliseconds / 100).ToString("000.00"));
69                     }
70                 }), contextWriteRequestStream);
71             }, contextGetRequestStream);
72         }, context);
73     }
74 }

精彩的部分来了~

代码眨一看,挺吓人,而且网页显示出来,也不那么美观,还是拷贝到VS中看吧;

个人还是比较喜欢这种风格,比较符合人的阅读习惯,从上往下看,就是正常的逻辑处理流程,感觉总比在一个又一个方法之间来回跳跃阅读要好得多;

所以,耐心一点看,还是可以看得明白的:)

归纳一下,基本上,异步操作就是以BeginXXX开始(不阻塞线程),以EndXXX结束(阻塞线程);

这里的特例就是MemoryStream的读写,没有使用异步方法,因为在其内部,异步方法和同步方法是一样的实现,所以,就没有必要搞那么麻烦了。

另外就是,我们看到用了BeginGetRequestStream(),却没有提供对应的BeginGetResponseStream()方法,这是为什么呢,我猜测是因为在EndGetResponse()的时候,就已经拿到了ResponseStream的句柄,所以没有必要再异步拿一次了。

网上还有同学问,既然已经BenginGetResponse()了,还要使用BeginRead()来异步读取呢,有这个必要吗?其实还是有必要的,如果传输的数据量很大,或者网络状态不好,Read()可能可能会阻塞很久,完全可以通过BeginRead()来解放CPU,多干点儿其他的事情。

本来还想写一写使用XXXAsync()的范例,不过实在太困了,以后有机会再写吧:(

原文地址:https://www.cnblogs.com/mikecheers/p/12178398.html

时间: 2024-11-10 11:29:52

《C# 爬虫 破境之道》:第一境 爬虫原理 — 第五节:数据流处理的那些事儿的相关文章

网络爬虫入门:你的第一个爬虫项目(requests库)

0.采用requests库 虽然urllib库应用也很广泛,而且作为Python自带的库无需安装,但是大部分的现在python爬虫都应用requests库来处理复杂的http请求.requests库语法上简洁明了,使用上简单易懂,而且正逐步成为大多数网络爬取的标准. 1. requests库的安装采用pip安装方式,在cmd界面输入: pip install requests 小编推荐一个学python的学习qun 491308659 验证码:南烛无论你是大牛还是小白,是想转行还是想入行都可以来

《C# 爬虫 破境之道》:第二境 爬虫应用 — 第一节:HTTP协议数据采集

首先欢迎您来到本书的第二境,本境,我们将全力打造一个实际生产环境可用的爬虫应用了.虽然只是刚开始,虽然路漫漫其修远,不过还是有点小鸡冻:P 本境打算针对几大派生类做进一步深耕,包括与应用的结合.对比它们之间的区别.综合共性.封装.One-By-One. System.IO.Packaging.PackWebRequest System.Net.FileWebRequest System.Net.FtpWebRequest System.Net.HttpWebRequest 第一节,我们先来说说最

《C# 爬虫 破境之道》:第一境 爬虫原理 — 第一节:整体思路

在构建本章节内容的时候,笔者也在想一个问题,究竟什么样的采集器框架,才能算得上是一个“全能”的呢?就我自己以往项目经历而言,可以归纳以下几个大的分类: 根据通讯协议:HTTP的.HTTPS的.TCP的.UDP的: 根据数据类型:纯文本的.json的.压缩包的.图片的.视频的: 根据更新周期:不定期更新的.定期更新的.增量更新的: 根据数据来源:单一数据源.多重数据源.多重数据源混合: 根据采集点分布:单机的,集群的: 根据反爬虫策略:控制频率的,使用代理的,使用特定UA的: 根据配置:可配置的,

《C# 爬虫 破境之道》:第一境 爬虫原理 — 第二节:WebRequest

本节主要来介绍一下,在C#中制造爬虫,最为常见.常用.实用的基础类 ------ WebRequest.WebResponse. 先来看一个示例 [1.2.1]: 1 using System; 2 using System.IO; 3 using System.Net; 4 using System.Text; 5 6 class Program 7 { 8 static void Main(string[] args) 9 { 10 var request = WebRequest.Crea

《C# 爬虫 破境之道》:概述

第一节:写作本书的目的 关于笔者 张晓亭(Mike Cheers),1982年出生,内蒙古辽阔的大草原是我的故乡. 没有高学历,没有侃侃而谈的高谈阔论,拥有的就是那一份对技术的执著,对自我价值的追求. 我是谁,其实并不重要,我是高级开发.我是架构师.我是技术经理,这些都是我,跟各位没有半毛钱关系.最重要的是,我能给读者带来什么.接下来的日子里,就看看本书能给各位带来什么惊喜,也许到最后,你都不会记得我的名字,没有关系,相信我,那并不重要. 关于本书 本书是<破境之道>系列技术丛书中的一部分,将

《C# GDI+ 破境之道》:第一境 GDI+基础 —— 第三节:画圆形

有了上一节画矩形的基础,画圆形就不要太轻松+EZ:)所以,本节在画边线及填充上,就不做过多的讲解了,关注一下画“随机椭圆”.“正圆”.“路径填充”的具体实现就好.与画矩形相比较,画椭圆与之完全一致,没有任何特别之处. 在画矩形时,我们使用: System.Drawing.Graphics.DrawRectangle(Brush brush, Rectangle rect); System.Drawing.Graphics.FillRectangle(Brush brush, Rectangle

《ASP.NET MVC 5 破境之道》:第一境 ASP.Net MVC5项目初探 &mdash; 第二节:MVC5项目结构

第一境 ASP.Net MVC5项目初探 第一节:运行第一个MVC5项目 第二节:MVC5项目结构 第三节:View层简单改造 第四节:打造首页面 第二节:MVC5项目结构 接下来,我们来看看,VS为我们自动创建的项目,是什么样子的? 可以通过菜单中[View]->[Solution Explorer]项来打开解决方案资源管理器.这是一个树形结构的视图,根节点是解决方案,解决方案节点下,就是一个一个的项目了,目前,我们的解决方案里只有一个项目(HonorShop.Web). 接下来,展开(Hon

二维码已死?谁将是互联网+下的破局之道

原文标题:二维码已死?谁将是互联网+下的破局之道 随着科技的进步,互联网+和工业4.0的进程,增强现实和虚拟现实将会得到前所未有爆发性的增长,并将引领下一代互联网的走向,甚至取代目前人人离不开的手机.这已成为业内人士的共识,然而VR/AR的发展是否真如想象中的美呢? 黑科技大热!你到底抓住几分发展重心? 以智能手机的快速普及过程来看,硬件.内容和网络这三大基石成为信息革命的关键点,同样AR/VR亦是.随着第五代移动通信技术(5G)频繁进入大众视野并布局,其超过4G千倍的网速将能提供更大的容量,同

程序员修炼之道第一章读后感

首先我读了序言,明白了这本书可能现在还是读不懂,但是书中的有些知识可能会帮助我以后开发软件少走点弯路, 所以,可能我现在还不太懂,但是我一定会好好阅读,尽量多理解书中的一些经验和内容,这是我对自己读这本书的要求. 求. 接着我读了第一章的内容,虽然第一章篇幅不多,但整体读下来还是收获很多的.在第一章的一开始,就教会了我做一 个做一个程序员的原则,那就是诚实和负责任.如果你做错了某些事,承认它,并给出补救的选择,不要把责任推卸给别人. 更不要找各种各样的借口.不要说事情做不到,而要说能够做什么来挽