《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年出生,内蒙古辽阔的大草原是我的故乡. 没有高学历,没有侃侃而谈的高谈阔论,拥有的就是那一份对技术的执著,对自我价值的追求. ...
随机推荐
- P1099 双连击
题目描述 我们假设一个二位整数 \(N(10 \le N \le 99)\) ,它的十位上的数字是 \(A\) ,个位上的数字是 \(B\) ,如果 \(A\) 和 \(B\) 的比例关系满足 \(A ...
- 在spring security3中使用自定义的MD5和salt进行加密
首先看代码: <authentication-manager alias="authenticationManager"> <authentication-pro ...
- springboot中使用spring-session实现共享会话到redis(二)
上篇文章介绍了springboot中集成spring-session实现了将session分布式存到redis中.这篇在深入介绍一些spring-session的细节. 1.session超时: 在t ...
- 2019-10-10-优雅调试-REST-API-的工具
title author date CreateTime categories 优雅调试 REST API 的工具 lindexi 2019-10-10 20:9:33 +0800 2019-10-1 ...
- linux scull 的设计
编写驱动的第一步是定义驱动将要提供给用户程序的能力(机制).因为我们的"设备"是计算 机内存的一部分, 我们可自由做我们想做的事情. 它可以是一个顺序的或者随机存取的设 备, 一个 ...
- mysql导入文件出现Data truncated for column 'xxx' at row 1的原因
mysql导入文件的时候很容易出现"Data truncated for column 'xxx' at row x",其中字符串里的xxx和x是指具体的列和行数. 有时候,这是因 ...
- codeforces gym100801 Problem G. Graph
传送门:https://codeforces.com/gym/100801 题意: 给你一个DAG图,你最多可以进行k次操作,每次操作可以连一条有向边,问你经过连边操作后最小拓扑序的最大值是多少 题解 ...
- python数据分析经常使用的库
这个列表包含数据分析经常使用的Python库,供大家使用.1. 网络通用urllib -网络库(stdlib).requests -网络库.grab – 网络库(基于pycurl).pycurl – ...
- POJ 1166 The Clocks [BFS] [位运算]
1.题意:有一组3*3的只有时针的挂钟阵列,每个时钟只有0,3,6,9三种状态:对时针阵列有9种操作,每种操作只对特点的几个时钟拨一次针,即将时针顺时针波动90度,现在试求从初试状态到阵列全部指向0的 ...
- C++引用计数设计与分析(解决垃圾回收问题)
1.引言 上一篇博文讲到https://www.cnblogs.com/zhaoyixiang/p/12116203.html 我们了解到我们在浅拷贝时对带指针的对象进行拷贝会出现内存泄漏,那C++是 ...