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

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

[Code 2.2.1]

  1. using System;
  2.  
  3. public abstract class Ant
  4. {
  5. public UInt32 AntId { get; set; }
  6.  
  7. public Action<Ant, JobEventArgs> OnJobStatusChanged { get; set; }
  8.  
  9. protected virtual JobEventArgs NotifyStatusChanged(JobEventArgs args)
  10. {
  11. if (null != OnJobStatusChanged)
  12. OnJobStatusChanged(args.EventAnt, args);
  13. else
  14. Console.WriteLine($"Worker { args.EventAnt.AntId } JobStatus: {args.Context.JobStatus}.");
  15.  
  16. return args;
  17. }
  18. }

蚂蚁类(Ant)

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

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

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

[Code 2.2.2]

  1. using System.ComponentModel;
  2.  
  3. public class JobEventArgs : CancelEventArgs
  4. {
  5. public Ant EventAnt { get; set; }
  6. public JobContext Context { get; set; }
  7. public String Message { get; set; }
  8. }

委托参数类(JobEventArgs)

委托参数类也比较简单,

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

[Code 2.2.3]

  1. using System;
  2. using System.Diagnostics;
  3. using System.IO;
  4. using System.Net;
  5. using System.Security.Cryptography.X509Certificates;
  6. using System.Threading.Tasks;
  7.  
  8. /// <summary>
  9. /// 一个爬虫的最小任务单位,一只小工蚁。
  10. /// </summary>
  11. public class WorkerAnt : Ant
  12. {
  13. public void Work(JobContext context)
  14. {
  15. if (null == context)
  16. {
  17. context.JobStatus = TaskStatus.Faulted;
  18. NotifyStatusChanged(new JobEventArgs
  19. {
  20. Context = context,
  21. EventAnt = this,
  22. Message = @"can not start a job with no context",
  23. });
  24. return;
  25. }
  26.  
  27. switch ((context.Method ?? string.Empty))
  28. {
  29. case WebRequestMethods.Http.Connect:
  30. case WebRequestMethods.Http.Get:
  31. case WebRequestMethods.Http.Head:
  32. case WebRequestMethods.Http.MkCol:
  33. case WebRequestMethods.Http.Post:
  34. case WebRequestMethods.Http.Put:
  35. break;
  36. default:
  37. context.JobStatus = TaskStatus.Faulted;
  38. NotifyStatusChanged(new JobEventArgs
  39. {
  40. Context = context,
  41. EventAnt = this,
  42. Message = $"can not start a job with request method <{(context.Method ?? "no method")}> is unsupported",
  43. });
  44. return;
  45. }
  46.  
  47. if (null == context.Uri || !Uri.IsWellFormedUriString(context.Uri, UriKind.RelativeOrAbsolute))
  48. {
  49. context.JobStatus = TaskStatus.Faulted;
  50. NotifyStatusChanged(new JobEventArgs
  51. {
  52. Context = context,
  53. EventAnt = this,
  54. Message = $"can not start a job with uri '{context.Uri}' is not well formed",
  55. });
  56. return;
  57. }
  58.  
  59. context.JobStatus = TaskStatus.Created;
  60. if (NotifyStatusChanged(new JobEventArgs { Context = context, EventAnt = this, }).Cancel)
  61. {
  62. context.JobStatus = TaskStatus.Canceled;
  63. NotifyStatusChanged(new JobEventArgs { Context = context, EventAnt = this, });
  64. return;
  65. }
  66.  
  67. /* ........... 此处省略上万字 ......... */
  68. }
  69.  
  70. private void GetResponse(JobContext context)
  71. {
  72. context.Request.BeginGetResponse(new AsyncCallback(acGetResponse =>
  73. {
  74. var contextGetResponse = acGetResponse.AsyncState as JobContext;
  75. using (contextGetResponse.Response = contextGetResponse.Request.EndGetResponse(acGetResponse))
  76. using (contextGetResponse.ResponseStream = contextGetResponse.Response.GetResponseStream())
  77. using (contextGetResponse.Memory = new MemoryStream())
  78. {
  79. var readCount = ;
  80. if (null == contextGetResponse.Buffer) contextGetResponse.Buffer = new byte[];
  81. IAsyncResult ar = null;
  82. do
  83. {
  84. if ( < readCount)
  85. {
  86. contextGetResponse.Memory.Write(contextGetResponse.Buffer, , readCount);
  87. contextGetResponse.JobStatus = TaskStatus.Running;
  88. if (NotifyStatusChanged(new JobEventArgs { Context = contextGetResponse, EventAnt = this, }).Cancel)
  89. {
  90. contextGetResponse.JobStatus = TaskStatus.Canceled;
  91. NotifyStatusChanged(new JobEventArgs { Context = contextGetResponse, EventAnt = this, });
  92. break;
  93. }
  94. }
  95. ar = contextGetResponse.ResponseStream.BeginRead(
  96. contextGetResponse.Buffer, , contextGetResponse.Buffer.Length, null, contextGetResponse);
  97. } while ( < (readCount = contextGetResponse.ResponseStream.EndRead(ar))
  98. && TaskStatus.Running == contextGetResponse.JobStatus); // 与EndRead的顺序不能颠倒
  99.  
  100. contextGetResponse.Request.Abort();
  101. contextGetResponse.Response.Close();
  102. contextGetResponse.Watch.Stop();
  103.  
  104. if (TaskStatus.Running == contextGetResponse.JobStatus)
  105. {
  106. contextGetResponse.Buffer = contextGetResponse.Memory.ToArray();
  107.  
  108. contextGetResponse.JobStatus = TaskStatus.RanToCompletion;
  109. NotifyStatusChanged(new JobEventArgs { Context = context, EventAnt = this, });
  110. }
  111. contextGetResponse.Buffer = null;
  112. }
  113. }), context);
  114. }
  115. }

工蚁(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]

  1. Console.WriteLine("/* ************** 第二境 * 第二节 * 以事件驱动状态、数据处理 ************** */");
  2.  
  3. var requestDataBuilder = new StringBuilder();
  4. requestDataBuilder.AppendLine("using System;");
  5. requestDataBuilder.AppendLine("namespace HelloWorldApplication");
  6. requestDataBuilder.AppendLine("{");
  7. requestDataBuilder.AppendLine(" class HelloWorld");
  8. requestDataBuilder.AppendLine(" {");
  9. requestDataBuilder.AppendLine(" static void Main(string[] args)");
  10. requestDataBuilder.AppendLine(" {");
  11. requestDataBuilder.AppendLine(" Console.WriteLine(\"《C# 爬虫 破境之道》\");");
  12. requestDataBuilder.AppendLine(" }");
  13. requestDataBuilder.AppendLine(" }");
  14. requestDataBuilder.AppendLine("}");
  15.  
  16. var requestData = Encoding.UTF8.GetBytes(
  17. @"code=" + System.Web.HttpUtility.UrlEncode(requestDataBuilder.ToString())
  18. + @"&token=4381fe197827ec87cbac9552f14ec62a&language=10&fileext=cs");
  19.  
  20. for (int i = ; i < ; i++)
  21. {
  22. new WorkerAnt()
  23. {
  24. AntId = (uint)Math.Abs(DateTime.Now.ToString("yyyyMMddHHmmssfff").GetHashCode()),
  25. OnJobStatusChanged = (sender, args) =>
  26. {
  27. Console.WriteLine($"{args.EventAnt.AntId} said: {args.Context.JobName} entered status '{args.Context.JobStatus}'.");
  28. switch (args.Context.JobStatus)
  29. {
  30. case TaskStatus.Created:
  31. if (string.IsNullOrEmpty(args.Context.JobName))
  32. {
  33. Console.WriteLine($"Can not execute a job with no name.");
  34. args.Cancel = true;
  35. }
  36. else
  37. Console.WriteLine($"{args.EventAnt.AntId} said: job {args.Context.JobName} created.");
  38. break;
  39. case TaskStatus.Running:
  40. if (null != args.Context.Memory)
  41. Console.WriteLine($"{args.EventAnt.AntId} said: {args.Context.JobName} already downloaded {args.Context.Memory.Length} bytes.");
  42. break;
  43. case TaskStatus.RanToCompletion:
  44. if (null != args.Context.Buffer && < args.Context.Buffer.Length)
  45. {
  46. Task.Factory.StartNew(oBuffer =>
  47. {
  48. var content = new UTF8Encoding(false).GetString((byte[])oBuffer);
  49. Console.WriteLine(content.Length > ? content.Substring(, ) + "..." : content);
  50. }, new MemoryStream(args.Context.Buffer).ToArray(), TaskCreationOptions.LongRunning);
  51. }
  52. if (null != args.Context.Watch)
  53. Console.WriteLine("/* ********************** using {0}ms / request ******************** */"
  54. + Environment.NewLine + Environment.NewLine, (args.Context.Watch.Elapsed.TotalMilliseconds / ).ToString("000.00"));
  55. break;
  56. case TaskStatus.Faulted:
  57. Console.WriteLine($"{args.EventAnt.AntId} said: job {args.Context.JobName} faulted because {args.Message}.");
  58. break;
  59. case TaskStatus.WaitingToRun:
  60. case TaskStatus.WaitingForChildrenToComplete:
  61. case TaskStatus.Canceled:
  62. case TaskStatus.WaitingForActivation:
  63. default:
  64. /* Do nothing on this even. */
  65. break;
  66. }
  67. },
  68. }.Work(new JobContext
  69. {
  70. JobName = "“以事件驱动状态、数据处理”",
  71. Uri = @"https://tool.runoob.com/compile.php",
  72. ContentType = @"application/x-www-form-urlencoded; charset=UTF-8",
  73. Method = WebRequestMethods.Http.Post,
  74. Buffer = requestData,
  75. });
  76. }

应用层调用示例改造

对应用层的改造,主要体现在第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. Spring Boot Thymeleaf 使用详解

    在上篇文章Spring Boot (二):Web 综合开发中简单介绍了一下 Thymeleaf,这篇文章将更加全面详细的介绍 Thymeleaf 的使用.Thymeleaf 是新一代的模板引擎,在 S ...

  2. Python--day38--JoinableQueue解决生产者消费者模型

    ############################# # 在消费者这一端: #每次获取一个数据 #处理一个数据 #发送一个记号:标志一个数据被处理成功 #在生产者这一端: #每一次生成一个数据 ...

  3. H3C DNS域名解析原理

  4. linux 选择 ioctl 命令

    在为 ioctl 编写代码之前, 你需要选择对应命令的数字. 许多程序员的第一个本能的反 应是选择一组小数从0或1 开始, 并且从此开始向上. 但是, 有充分的理由不这样做. ioctl 命令数字应当 ...

  5. 一个基于 Slab 缓存的 scull: scullc

    是时候给个例子了. scullc 是一个简化的 scull 模块的版本, 它只实现空设备 -- 永久 的内存区. 不象 scull, 它使用 kmalloc, scullc 使用内存缓存. 量子的大小 ...

  6. es6笔记 day3---数组新增东西

    Array.from()的作用就是把类数组转成数组.所谓类数组,就是有长度的数组 ----------------------------------------------------------- ...

  7. CodeForces 1204 (#581 div 2)

    传送门 A.BowWow and the Timetable •题意 给你一个二进制数,让你求小于这个数的所有4的幂的个数 •思路 第一反应是二进制与四进制转换 (其实不用真正的转换 QwQ) 由于二 ...

  8. 阿里云 CentOS8 Repo

    # CentOS-Base.repo # # The mirror system uses the connecting IP address of the client and the # upda ...

  9. C++Review1_多态和虚函数

    继承是实现多态的基础.虚函数是实现多态的方法.虚函数.多态.继承都是紧密相关的概念.而继承是所有概念的基础: 多态:简单来讲就是接口一样,实现多样.多态是指通过基类的指针或者引用,在运行时动态调用实际 ...

  10. Kali之msf简单的漏洞利用

    1.信息收集 靶机的IP地址为:192.168.173.136 利用nmap工具扫描其开放端口.系统等 整理一下目标系统的相关信息 系统版本:Windows server 2003 开放的端口及服务: ...