《C# 爬虫 破境之道》:第一境 爬虫原理 — 第五节:数据流处理的那些事儿
为什么说到数据流了呢,因为上一节中介绍了一下异步发送请求。同样,在数据流的处理上,C#也为我们提供几个有用的异步处理方法。而且,爬虫这生物,处理数据流是基础本能,比较重要。本着这个原则,就聊一聊吧。
我们经常使用到的流有文件流、内存流、网络流,爬虫与这三种流都有着密不可分的联系,可以联想以下这些场景:
- 当我们采集的数据,是一个压缩包或者照片,那么要存储它们到硬盘上,就需要使用到文件流了;
- 当我们采集的数据,是经过GZip等压缩算法压缩过的,那么要解压它,就需要使用到内存流了;
- 当我们的爬虫运行起来,就需要用到网络了,使用网络流是必然不可缺少的了;
所以,对流的操作,也是一个必要重要的环节;除了上面列举的几个场景之外,还有很多场景会涉及到流的处理,就不一一列举了,数不胜数;但每种流的处理,都对应其相应的I/O操作。所以,在DotNetFramework中,封装了System.IO.Stream这个基础流,在其基础之上,派生出很多有用的流;
我们在这里结合上一节中第一种异步请求方式的案例,来讲述爬虫中的网络流处理,其他类型的流处理,也是触类旁通的,文件流、内存流,在后续章节中,都会有所涉及,只是不会当作专题来讲解了。
在爬虫中,我们主要面临的网络流,有两个:
- RequestStream:请求流
- ResponseStream:回复流
当然,这里说的爬虫,还很小,只是基于WebRequest、WebResponse的,等后面我们再继续下沉,让它再成长成长,到Socket层面,我们要处理的网络流主要就是System.Net.Sockets.NetworkStream了,不过先不急,以小见大,也是很好的事情:)
至于为什么要使用流,上一节中已经举例说明了,这里就不再赘述。
第一部分:同步方式处理数据流
[Code 5.1.1]
{
Stopwatch watch = new Stopwatch();
Console.WriteLine("/* ********** 异步请求方式 * BeginGetResponse() & EndGetResponse() **********/");
watch.Start();
{
var request = WebRequest.Create(@"https://tool.runoob.com/compile.php");
request.Method = WebRequestMethods.Http.Post;
request.ContentType = @"application/x-www-form-urlencoded; charset=UTF-8";
var requestDataBuilder = new StringBuilder();
requestDataBuilder.AppendLine("using System;");
requestDataBuilder.AppendLine("namespace HelloWorldApplication");
requestDataBuilder.AppendLine("{");
requestDataBuilder.AppendLine(" class HelloWorld");
requestDataBuilder.AppendLine(" {");
requestDataBuilder.AppendLine(" static void Main(string[] args)");
requestDataBuilder.AppendLine(" {");
requestDataBuilder.AppendLine(" Console.WriteLine(\"《C# 爬虫 破境之道》\");");
requestDataBuilder.AppendLine(" }");
requestDataBuilder.AppendLine(" }");
requestDataBuilder.AppendLine("}");
var requestData = Encoding.UTF8.GetBytes(@"code=" + System.Web.HttpUtility.UrlEncode(requestDataBuilder.ToString())
+ @"&token=4381fe197827ec87cbac9552f14ec62a&language=10&fileext=cs");
requestDataBuilder.Clear();
request.ContentLength = requestData.Length;
var requestStream = request.GetRequestStream();
requestStream.Write(requestData, , requestData.Length);
request.BeginGetResponse(new AsyncCallback(ar =>
{
using (var response = (ar.AsyncState as WebRequest).EndGetResponse(ar))
{
using (var stream = response.GetResponseStream())
{
using (var reader = new StreamReader(stream, new UTF8Encoding(false)))
{
var content = reader.ReadToEnd();
Console.WriteLine(content.Length > ? content.Substring(, ) + "..." : content);
}
}
response.Close();
}
watch.Stop();
Console.WriteLine("/* ********************** using {0}ms / request ******************** */"
+ Environment.NewLine + Environment.NewLine, (watch.Elapsed.TotalMilliseconds / ).ToString("000.00"));
}), request);
}
}
同步方式处理网络流
相对于异步发送的案例,代码的变动主要在第7行到第28行。
首先7、8行,为request的两个属性赋值发生了变化,我们要操作RequestStream,一定要指定合适的Method,POST或PUT等,其他的Method并不支持对流操作,就会出错;另外就是然使用流了,流里的数据到底是个什么,服务器端应该如何解释,可以通过ContentType来指定,有时候服务器端并不是那么严谨,可能稀里糊涂的也就过去了;
接下来,第10~21行,我构建了一个字符串,作为要提交的主体数据,在第23行,将字符串转换为字节数组;对流操作,字节数组和编码都是跑不掉的,时而绕晕,时而迷糊,也是很正常的:)
第26行,指定填充到数据流的数据长度;说到这个长度,再啰嗦一下HTTP协议,用Wireshark随便抓个包当个栗子
[Code 5.1.2]
HTTP/1.1 200 OK
Date: Fri, 10 Jan 2020 08:10:02 GMT
Server: Apache/2.2.9 (APMServ) PHP/5.2.6
Last-Modified: Sun, 05 Jan 2020 10:29:06 GMT
ETag: "29000000008812-616-59b620334ab88"
Accept-Ranges: bytes
Content-Length: 1558 <-----------这里不对,是因为我把下面xml精简了一下,要不太长。
Content-Type: application/xml <?xml version="1.0" encoding="gb2312"?>
<root>
<FileList>
<FileName version="20181122">sound/FaceSuccess.wav</FileName>
</FileList>
</root>
某请求的回复报文
第1行,请求和回复不太一样,具体就不说了,大体就是HTTP协议的版本、URI地址、状态等;
第2~8行,一行一对儿,对应我们WebRequest和WebResponse里的Headers;
上面1~8行就是协议头;
第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]
public class WebAsyncContext
{
public System.Net.WebRequest Request { get; set; }
public System.Net.WebResponse Response { get; set; }
public System.IO.Stream RequestStream { get; set; }
public System.IO.Stream ResponseStream { get; set; }
public System.IO.MemoryStream Memory { get; set; }
public byte[] Buffer { get; set; }
}
WebAsyncContext
比较简单,不做解释了,接下来,就是一票子异步操作了,别眨眼~
[Code 5.2.2]
{
Stopwatch watch = new Stopwatch();
Console.WriteLine("/* ********** 异步请求方式 * 异步方式处理数据流 **********/");
watch.Start();
{
var requestDataBuilder = new StringBuilder();
requestDataBuilder.AppendLine("using System;");
requestDataBuilder.AppendLine("namespace HelloWorldApplication");
requestDataBuilder.AppendLine("{");
requestDataBuilder.AppendLine(" class HelloWorld");
requestDataBuilder.AppendLine(" {");
requestDataBuilder.AppendLine(" static void Main(string[] args)");
requestDataBuilder.AppendLine(" {");
requestDataBuilder.AppendLine(" Console.WriteLine(\"《C# 爬虫 破境之道》\");");
requestDataBuilder.AppendLine(" }");
requestDataBuilder.AppendLine(" }");
requestDataBuilder.AppendLine("}");
var requestData = Encoding.UTF8.GetBytes(@"code=" + System.Web.HttpUtility.UrlEncode(requestDataBuilder.ToString())
+ @"&token=4381fe197827ec87cbac9552f14ec62a&language=10&fileext=cs");
var context = new WebAsyncContext { Request = WebRequest.Create(@"https://tool.runoob.com/compile.php"), Buffer = requestData };
requestData = null;
requestDataBuilder.Clear();
context.Request.ContentLength = context.Buffer.Length;
context.Request.Method = WebRequestMethods.Http.Post;
context.Request.ContentType = @"application/x-www-form-urlencoded; charset=UTF-8";
context.Request.Proxy = null;
context.Request.BeginGetRequestStream(acGetRequestStream =>
{
var contextGetRequestStream = acGetRequestStream.AsyncState as WebAsyncContext;
contextGetRequestStream.RequestStream = contextGetRequestStream.Request.EndGetRequestStream(acGetRequestStream);
contextGetRequestStream.RequestStream.BeginWrite(contextGetRequestStream.Buffer, , contextGetRequestStream.Buffer.Length, acWriteStream =>
{
var contextWriteRequestStream = acWriteStream.AsyncState as WebAsyncContext;
contextWriteRequestStream.RequestStream.EndWrite(acWriteStream);
contextWriteRequestStream.Request.BeginGetResponse(new AsyncCallback(acGetResponse =>
{
var contextGetResponse = acGetResponse.AsyncState as WebAsyncContext;
using (contextGetResponse.Response = contextGetResponse.Request.EndGetResponse(acGetResponse))
using (contextGetResponse.ResponseStream = contextGetResponse.Response.GetResponseStream())
using (contextGetResponse.Memory = new MemoryStream())
{
contextGetResponse.Buffer = new Byte[];
var readCount = ;
IAsyncResult ar = null;
do
{
if ( < readCount) contextGetResponse.Memory.Write(contextGetResponse.Buffer, , readCount);
ar = contextGetResponse.ResponseStream.BeginRead(
contextGetResponse.Buffer, , contextGetResponse.Buffer.Length, null, contextGetResponse);
Console.WriteLine($"Totally {contextGetResponse.Memory.Length} downloaded.");
} while ( < (readCount = contextGetResponse.ResponseStream.EndRead(ar)));
contextGetResponse.RequestStream.Close();
contextGetResponse.Request.Abort();
contextGetResponse.Response.Close();
contextGetResponse.Buffer = null;
var content = new UTF8Encoding(false).GetString(contextGetResponse.Memory.ToArray());
Console.WriteLine(content.Length > ? content.Substring(, ) + "..." : content);
watch.Stop();
Console.WriteLine("/* ********************** using {0}ms / request ******************** */"
+ Environment.NewLine + Environment.NewLine, (watch.Elapsed.TotalMilliseconds / ).ToString("000.00"));
}
}), contextWriteRequestStream);
}, contextGetRequestStream);
}, context);
}
}
精彩部分来啦~ 修正一个小Bug~
代码眨一看,挺吓人,而且网页显示出来,也不那么美观,还是拷贝到VS中看吧;
个人还是比较喜欢这种风格,比较符合人的阅读习惯,从上往下看,就是正常的逻辑处理流程,感觉总比在一个又一个方法之间来回跳跃阅读要好得多;
所以,耐心一点看,还是可以看得明白的:)
归纳一下,基本上,异步操作就是以BeginXXX开始(不阻塞线程),以EndXXX结束(阻塞线程);
这里的特例就是MemoryStream的读写,没有使用异步方法,因为在其内部,异步方法和同步方法是一样的实现,所以,就没有必要搞那么麻烦了。
另外就是,我们看到用了BeginGetRequestStream(),却没有提供对应的BeginGetResponseStream()方法,这是为什么呢,我猜测是因为在EndGetResponse()的时候,就已经拿到了ResponseStream的句柄,所以没有必要再异步拿一次了。
网上还有同学问,既然已经BenginGetResponse()了,还要使用BeginRead()来异步读取呢,有这个必要吗?其实还是有必要的,如果传输的数据量很大,或者网络状态不好,Read()可能可能会阻塞很久,完全可以通过BeginRead()来解放CPU,多干点儿其他的事情。
本来还想写一写使用XXXAsync()的范例,不过实在太困了,以后有机会再写吧:(
喜欢本系列丛书的朋友,可以点击链接加入QQ交流群(994761602)【C# 破境之道】
方便各位在有疑问的时候可以及时给我个反馈。同时,也算是给各位志同道合的朋友提供一个交流的平台。
需要源码的童鞋,也可以在群文件中获取最新源代码。
《C# 爬虫 破境之道》:第一境 爬虫原理 — 第五节:数据流处理的那些事儿的更多相关文章
- 网络爬虫入门:你的第一个爬虫项目(requests库)
0.采用requests库 虽然urllib库应用也很广泛,而且作为Python自带的库无需安装,但是大部分的现在python爬虫都应用requests库来处理复杂的http请求.requests库语 ...
- Python爬虫实践 -- 记录我的第一只爬虫
一.环境配置 1. 下载安装 python3 .(或者安装 Anaconda) 2. 安装requests和lxml 进入到 pip 目录,CMD --> C:\Python\Scripts,输 ...
- 《C# 爬虫 破境之道》:第二境 爬虫应用 — 第一节:HTTP协议数据采集
首先欢迎您来到本书的第二境,本境,我们将全力打造一个实际生产环境可用的爬虫应用了.虽然只是刚开始,虽然路漫漫其修远,不过还是有点小鸡冻:P 本境打算针对几大派生类做进一步深耕,包括与应用的结合.对比它 ...
- 《C# 爬虫 破境之道》:第一境 爬虫原理 — 第六节:第一境尾声
在第一境中,我们主要了解了爬虫的一些基本原理,说原理也行,说基础知识也罢,结果就是已经知道一个小爬虫是如何诞生的了~那么现在,请默默回想一下,在第一境中,您都掌握了哪些内容?哪些还比较模糊?如果还有什 ...
- 《C# 爬虫 破境之道》:第一境 爬虫原理 — 第二节:WebRequest
本节主要来介绍一下,在C#中制造爬虫,最为常见.常用.实用的基础类 ------ WebRequest.WebResponse. 先来看一个示例 [1.2.1]: using System; usin ...
- 《C# 爬虫 破境之道》:第一境 爬虫原理 — 第一节:整体思路
在构建本章节内容的时候,笔者也在想一个问题,究竟什么样的采集器框架,才能算得上是一个“全能”的呢?就我自己以往项目经历而言,可以归纳以下几个大的分类: 根据通讯协议:HTTP的.HTTPS的.TCP的 ...
- 《C# 爬虫 破境之道》:第一境 爬虫原理 — 第四节:同步与异步请求方式
前两节,我们对WebRequest和WebResponse这两个类做了介绍,但两者还相对独立.本节,我们来说说如何将两者结合起来,方式有哪些,有什么不同. 1.4.1 说结合,无非就是我们如何发送一个 ...
- 《C# 爬虫 破境之道》:第一境 爬虫原理 — 第三节:WebResponse
第二节中,我们介绍了WebRequest,它可以帮助我们发送一个请求,不过正所谓“来而不往非礼也”,对方收到我们的请求,不给点回复,貌似不太合适(不过,还真有脸皮厚的:P). 接下来,就重点研究一下, ...
- 《C# 爬虫 破境之道》:概述
第一节:写作本书的目的 关于笔者 张晓亭(Mike Cheers),1982年出生,内蒙古辽阔的大草原是我的故乡. 没有高学历,没有侃侃而谈的高谈阔论,拥有的就是那一份对技术的执著,对自我价值的追求. ...
随机推荐
- LA 5031 Graph and Queries —— Treap名次树
离线做法,逆序执行操作,那么原本的删除边的操作变为加入边的操作,用名次树维护每一个连通分量的名次,加边操作即是连通分量合并操作,每次将结点数小的子树向结点数大的子树合并,那么单次合并复杂度O(n1lo ...
- 【t013】无聊的军官
Time Limit: 1 second Memory Limit: 32 MB [问题描述] 每个学年的开始,高一新生们都要进行传统的军训.今年有一个军训教官十分奇怪,他为了测试学员们的反应能力,每 ...
- svn 删除、移动和改名
删除.移动和改名 Subversion allows renaming and moving of files and folders. So there are menu entries for d ...
- H3C 基本的局域网间路由
- P1028 过河问题
题目描述 为了躲避黑暗大魔王的追杀,zifeiy与他的伙伴们共N人连夜逃出了黑暗城堡,他们走到一条河的东岸边,想要过河到西岸.而东岸边有一条小船. 船太小了,一次只能乘坐两人.每个人都有一个渡河时间T ...
- dotnet Framework 源代码 类库的意思
本文告诉大家 dotnet framework 的源代码类库的意思 下面列出来 dotnet framework 源代码的各个类库的作用. System System 命名空间包含基本类和基类,这些类 ...
- JavaScript实现版本号比较
/* * JavaScript实现版本号比较 * 传入两个字符串,当前版本号:curV:比较版本号:reqV * 调用方法举例:Version('5.12.3','5.12.2'),将返回true * ...
- 【Docker Compose】简介与安装
1.简介 Compose 是一个用户定义和运行多个容器的 Docker 应用程序.在 Compose 中你可以使用 YAML 文件来配置你的应用服务.然后,只需要一个简单的命令,就可以创建并启动你配置 ...
- Visual Studio 2019 编译.Net Core Console项目出现【MSB4018 The "CreateAppHost" task failed unexpectedly】 错误
需要测试一个小东东,使用Visual Studio 2019新建了一个.Net Core的Console程序,但是在编译的时候一直报错,死活编译不通过. 错误信息: Severity Code Des ...
- vue新增属性响应式更新的问题
根据官方文档定义: 如果在实例创建之后添加新的属性到实例上,它不会触发视图更新. 受现代 JavaScript 的限制 (以及废弃 Object.observe),Vue 不能检测到对象属性的添加或删 ...