续上一节内容,对Web爬虫进行进一步封装,通过委托将爬虫自己的状态变化以及数据变化暴露给上层业务处理或应用程序。

为了方便以后的扩展,我先定义一个蚂蚁抽象类(Ant),并让WorkerAnt(工蚁)继承自它。

[Code 2.2.1]

 using System;

 public abstract class Ant
{
public UInt32 AntId { get; set; } public Action<Ant, JobEventArgs> OnJobStatusChanged { get; set; } protected virtual JobEventArgs NotifyStatusChanged(JobEventArgs args)
{
if (null != OnJobStatusChanged)
OnJobStatusChanged(args.EventAnt, args);
else
Console.WriteLine($"Worker { args.EventAnt.AntId } JobStatus: {args.Context.JobStatus}."); return args;
}
}

蚂蚁类(Ant)

蚂蚁类比较简单,定义了一个属性(AntId),作为每只小蚂蚁的编号;

定义了一个委托(OnJobStatusChanged),当任务状态发生变化时,用来发出状态变化通知;其中第二个参数JobEventArgs我们一会列出它的定义;

在有就是定义了一个虚方法NotifyStatusChanged,用来检查和触发委托事件;

[Code 2.2.2]

 using System.ComponentModel;

 public class JobEventArgs : CancelEventArgs
{
public Ant EventAnt { get; set; }
public JobContext Context { get; set; }
public String Message { get; set; }
}

委托参数类(JobEventArgs)

委托参数类也比较简单,

  • 定义了一个属性(EventAnt),指示事件的触发者,就是编程世界中很有名气的sender,通常是object类型,不过在我们的爬虫框架里,这个事件通常是有蚂蚁触发,所以我就暂定它的类型为蚂蚁了,先把坑占上,如果以后扩展需要外部触发的话,我们再升级;
  • 另一个属性(Context)就是上节中使用的JobContext,内涵与Job相关的属性、描述信息;
  • 还有一个属性Message,做简单的说明,比如失败的原因是什么;

[Code 2.2.3]

 using System;
using System.Diagnostics;
using System.IO;
using System.Net;
using System.Security.Cryptography.X509Certificates;
using System.Threading.Tasks; /// <summary>
/// 一个爬虫的最小任务单位,一只小工蚁。
/// </summary>
public class WorkerAnt : Ant
{
public void Work(JobContext context)
{
if (null == context)
{
context.JobStatus = TaskStatus.Faulted;
NotifyStatusChanged(new JobEventArgs
{
Context = context,
EventAnt = this,
Message = @"can not start a job with no context",
});
return;
} switch ((context.Method ?? string.Empty))
{
case WebRequestMethods.Http.Connect:
case WebRequestMethods.Http.Get:
case WebRequestMethods.Http.Head:
case WebRequestMethods.Http.MkCol:
case WebRequestMethods.Http.Post:
case WebRequestMethods.Http.Put:
break;
default:
context.JobStatus = TaskStatus.Faulted;
NotifyStatusChanged(new JobEventArgs
{
Context = context,
EventAnt = this,
Message = $"can not start a job with request method <{(context.Method ?? "no method")}> is unsupported",
});
return;
} if (null == context.Uri || !Uri.IsWellFormedUriString(context.Uri, UriKind.RelativeOrAbsolute))
{
context.JobStatus = TaskStatus.Faulted;
NotifyStatusChanged(new JobEventArgs
{
Context = context,
EventAnt = this,
Message = $"can not start a job with uri '{context.Uri}' is not well formed",
});
return;
} context.JobStatus = TaskStatus.Created;
if (NotifyStatusChanged(new JobEventArgs { Context = context, EventAnt = this, }).Cancel)
{
context.JobStatus = TaskStatus.Canceled;
NotifyStatusChanged(new JobEventArgs { Context = context, EventAnt = this, });
return;
} /* ........... 此处省略上万字 ......... */
} 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())
{
var readCount = ;
if (null == contextGetResponse.Buffer) contextGetResponse.Buffer = new byte[];
IAsyncResult ar = null;
do
{
if ( < readCount)
{
contextGetResponse.Memory.Write(contextGetResponse.Buffer, , readCount);
contextGetResponse.JobStatus = TaskStatus.Running;
if (NotifyStatusChanged(new JobEventArgs { Context = contextGetResponse, EventAnt = this, }).Cancel)
{
contextGetResponse.JobStatus = TaskStatus.Canceled;
NotifyStatusChanged(new JobEventArgs { Context = contextGetResponse, EventAnt = this, });
break;
}
}
ar = contextGetResponse.ResponseStream.BeginRead(
contextGetResponse.Buffer, , contextGetResponse.Buffer.Length, null, contextGetResponse);
} while ( < (readCount = contextGetResponse.ResponseStream.EndRead(ar))
&& TaskStatus.Running == contextGetResponse.JobStatus); // 与EndRead的顺序不能颠倒 contextGetResponse.Request.Abort();
contextGetResponse.Response.Close();
contextGetResponse.Watch.Stop(); if (TaskStatus.Running == contextGetResponse.JobStatus)
{
contextGetResponse.Buffer = contextGetResponse.Memory.ToArray(); contextGetResponse.JobStatus = TaskStatus.RanToCompletion;
NotifyStatusChanged(new JobEventArgs { Context = context, EventAnt = this, });
}
contextGetResponse.Buffer = null;
}
}), context);
}
}

工蚁(WorkerAnt)进行改造

工蚁类抹去了内部输出,采用状态变更通知方式向外界传递消息。

第15~57行,演示了如何处理参数异常,发出通知,并停止采集工作。

其中第27~45行,演示了如何验证一个Request Method是否有效,注意,Method需要全部大写,所以,验证方法是区分大小写的;

其中第47~57行,演示了如何验证一个Uri是否是合法的格式;

第60~65行以及82~98,演示了如何处理业务逻辑返回的'Cancel'指令,并停止采集工作;

其中第87~93行,演示了在数据下载过程中,发出状态通知,业务逻辑层或应用层可以借此机会对部分数据进行编码或更新进度条;如果下载的数据是压缩数据,也可以在此时进行解压缩工作;也可以对数据进行文件写入操作;这也将导致在业务层或应用层将收到不止一次JobStatus = TaskStatus.Runing的消息;

第104~110行,演示了如何发出的任务完成通知;

[Code 2.2.4]

 Console.WriteLine("/* ************** 第二境 * 第二节 * 以事件驱动状态、数据处理 ************** */");

 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"); for (int i = ; i < ; i++)
{
new WorkerAnt()
{
AntId = (uint)Math.Abs(DateTime.Now.ToString("yyyyMMddHHmmssfff").GetHashCode()),
OnJobStatusChanged = (sender, args) =>
{
Console.WriteLine($"{args.EventAnt.AntId} said: {args.Context.JobName} entered status '{args.Context.JobStatus}'.");
switch (args.Context.JobStatus)
{
case TaskStatus.Created:
if (string.IsNullOrEmpty(args.Context.JobName))
{
Console.WriteLine($"Can not execute a job with no name.");
args.Cancel = true;
}
else
Console.WriteLine($"{args.EventAnt.AntId} said: job {args.Context.JobName} created.");
break;
case TaskStatus.Running:
if (null != args.Context.Memory)
Console.WriteLine($"{args.EventAnt.AntId} said: {args.Context.JobName} already downloaded {args.Context.Memory.Length} bytes.");
break;
case TaskStatus.RanToCompletion:
if (null != args.Context.Buffer && < args.Context.Buffer.Length)
{
Task.Factory.StartNew(oBuffer =>
{
var content = new UTF8Encoding(false).GetString((byte[])oBuffer);
Console.WriteLine(content.Length > ? content.Substring(, ) + "..." : content);
}, new MemoryStream(args.Context.Buffer).ToArray(), 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;
case TaskStatus.Faulted:
Console.WriteLine($"{args.EventAnt.AntId} said: job {args.Context.JobName} faulted because {args.Message}.");
break;
case TaskStatus.WaitingToRun:
case TaskStatus.WaitingForChildrenToComplete:
case TaskStatus.Canceled:
case TaskStatus.WaitingForActivation:
default:
/* Do nothing on this even. */
break;
}
},
}.Work(new JobContext
{
JobName = "“以事件驱动状态、数据处理”",
Uri = @"https://tool.runoob.com/compile.php",
ContentType = @"application/x-www-form-urlencoded; charset=UTF-8",
Method = WebRequestMethods.Http.Post,
Buffer = requestData,
});
}

应用层调用示例改造

对应用层的改造,主要体现在第25~67行,增加了对OnJobStatusChanged事件的处理。

其中,第30~38行,演示了如何在应用层或业务逻辑层,取消采集任务;

其中,第39~42行,演示了如何获取当前任务的当前已下载总量,并且可以通过context.Buffer获取当前下载的增量;如果context.Response.ContentLength不为-1的话,还可以计算出已下载量的占比;不过这里要小心的另一个陷阱就是HTTP 1.1 提供的Transfer-Encoding: Chunked;如果后面能碰到具体的场景,再举栗说明,这里先点破,不说破吧:)

其中,第43~55行,演示了如何获取下载的完整数据,注意,此时的context.Buffer是context.Memory中的所有数据,而不是当前下载的增量了。本节中所说的context.Memory是指当前Job累计下载的所有数据,为什么要加一个条件“本节所说的”呢,因为MemoryStream并不是无限大的,它也有极限,如果我们用它来处理一个Html文档或一张普通小照片还好,如果我们用它来处理一个很大的资源(比如一部蓝光电影或一个巨大的压缩包文件),将会发生异常,在那种情况下,我们就要考虑去使用文件内存映射(MemoryMappedFile)或其他技术了,暂且不在本节讨论。

至此,一个简单的事件处理机制就算是改造完成了。毕竟Web资源采集很重要,后面还会继续改造升级~敬请期待~

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

《C# 爬虫 破境之道》:第二境 爬虫应用 — 第二节:以事件驱动状态、数据处理的更多相关文章

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

    首先欢迎您来到本书的第二境,本境,我们将全力打造一个实际生产环境可用的爬虫应用了.虽然只是刚开始,虽然路漫漫其修远,不过还是有点小鸡冻:P 本境打算针对几大派生类做进一步深耕,包括与应用的结合.对比它 ...

  2. 《C# 爬虫 破境之道》:第二境 爬虫应用 — 第三节:处理压缩数据

    续上一节内容,本节主要讲解一下Web压缩数据的处理方法. 在HTTP协议中指出,可以通过对内容压缩来减少网络流量,从而提高网络传输的性能. 那么问题来了,在HTTP中,采用的是什么样的压缩格式和机制呢 ...

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

    本节主要来介绍一下,在C#中制造爬虫,最为常见.常用.实用的基础类 ------ WebRequest.WebResponse. 先来看一个示例 [1.2.1]: using System; usin ...

  4. 《C# 爬虫 破境之道》:第二境 爬虫应用 — 第四节:小说网站采集

    之前的章节,我们陆续的介绍了使用C#制作爬虫的基础知识,而且现在也应该比较了解如何制作一只简单的Web爬虫了. 本节,我们来做一个完整的爬虫系统,将之前的零散的东西串联起来,可以作为一个爬虫项目运作流 ...

  5. 《C# 爬虫 破境之道》:第二境 爬虫应用 — 第五节:小总结带来的优化与重构

    在上一节中,我们完成了一个简单的采集示例.本节呢,我们先来小结一下,这个示例可能存在的问题: 没有做异常处理 没有做反爬应对策略 没有做重试机制 没有做并发限制 …… 呃,看似平静的表面下还是隐藏着不 ...

  6. 《C# 爬虫 破境之道》:第二境 爬虫应用 — 第六节:反爬策略研究

    之前的章节也略有提及反爬策略,本节,我们就来系统的对反爬.反反爬的种种,做一个了结. 从防盗链说起: 自从论坛兴起的时候,网上就有很多人会在论坛里发布一些很棒的文章,与当下流行的“点赞”“分享”一样, ...

  7. 《C# 爬虫 破境之道》:第二境 爬虫应用 — 第七节:并发控制与策略

    我们在第五节中提到一个问题,任务队列增长速度太快,与之对应的采集.分析.处理速度远远跟不上,造成内存快速增长,带宽占用过高,CPU使用率过高,这样是极度有害系统健康的. 我们在开发采集程序的时候,总是 ...

  8. 《C# 爬虫 破境之道》:第一境 爬虫原理 — 第六节:第一境尾声

    在第一境中,我们主要了解了爬虫的一些基本原理,说原理也行,说基础知识也罢,结果就是已经知道一个小爬虫是如何诞生的了~那么现在,请默默回想一下,在第一境中,您都掌握了哪些内容?哪些还比较模糊?如果还有什 ...

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

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

随机推荐

  1. H3C 路由的来源

  2. linux加载和卸载模块

    模块建立之后, 下一步是加载到内核. 如我们已指出的, insmod 为你完成这个工作. 这个 程序加载模块的代码段和数据段到内核, 接着, 执行一个类似 ld 的函数, 它连接模块中 任何未解决的符 ...

  3. addEventListener() 方法,事件监听(去哪儿网用到过)

    addEventListener() 方法,事件监听 你可以使用 removeEventListener() 方法来移除事件的监听. 语法 element.addEventListener(event ...

  4. ppk on javascript 笔记(六)--BOM

    浏览器对象模型(Browser Object Model)是语言核心和DOM之间的一个过渡层,这个过渡层特指Javascript的客户端实现,它的主要任务是管理浏览器窗口并使得它们可以彼此通信.win ...

  5. TOJ4587:抓苹果(DP)

    传送门:抓苹果 dp(i,j) = max(dp(i-1,j-1),dp(i-1,j))+当i这分钟时能否刚好移动到这棵树下. 初始化是对不移动的情况下. 代码 #include<bits/st ...

  6. layui框架实现多图片手动上传和随表单提交方法

    首先在官方文档并没有手动上传的说明文档,这里手动实现上传原理是:在表单中有三个按钮,分别是上传图片按钮.隐藏上传按钮.表单提交按钮,点击上传图片按钮之后,图片添加在前端但是并没有真正的上传,而是在点击 ...

  7. linux安装python3.*,更换Python2.*

    下载并解压:Python-3.5.7.tgz [root@AH-aQYWTYSJZX01 python3]# ll total 20268 -rw-r----- 1 temp01 temp01 207 ...

  8. Oracle 11g R2 for Win10(64位)的安装注意点

    一般我们在win10系统安装oracle11g或者10g及更低版本的oracle客户端时,都是无法安装,一般安装的时候会闪退.这是什么原因呢?其实很简单,win10出的时间比较晚,在oracle11g ...

  9. Python中的[...]是什么?

    ...就是好几个冒号 array[...] 就是array[:,:,:]

  10. 抽象类(abstract class)和接口(interface)有什么区别?

    抽象类中可以有构造器.抽象方法.具体方法.静态方法.各种成员变量,有抽象方法的类一定要被声明为抽象类,而抽象类不一定要有抽象方法,一个类只能继承一个抽象类. 接口中不能有构造器.只能有public修饰 ...