《C# 爬虫 破境之道》:第二境 爬虫应用 — 第一节:HTTP协议数据采集
首先欢迎您来到本书的第二境,本境,我们将全力打造一个实际生产环境可用的爬虫应用了。虽然只是刚开始,虽然路漫漫其修远,不过还是有点小鸡冻:P
本境打算针对几大派生类做进一步深耕,包括与应用的结合、对比它们之间的区别、综合共性、封装。One-By-One。
- System.IO.Packaging.PackWebRequest
- System.Net.FileWebRequest
- System.Net.FtpWebRequest
- System.Net.HttpWebRequest
第一节,我们先来说说最重要、最常用的HttpWebRequest。撸码的细节就不说了,看不懂的童鞋可以去看看第一境就明白了:)
示例解决方案分为两个项目,一个应用程序(控制台),一个类库(取了个高大上的名字:System.Crawler)。所以,在应用程序中需要引用System.Crawler。应用程序可以是控制台、窗体项目、Win服务、WPF等,随便整,随你随地大小便……
先展示一下应用程序的最终的效果,然后我们再逐步引出类库的实现。
[Code 2.1.1]
- using System.Crawler;
- {
- #region 以GET方式请求数据
- var ant = new WorkerAnt
- {
- WorkerId = (uint)Math.Abs(DateTime.Now.ToString("HHmmssfff").GetHashCode()),
- };
- var job = new JobContext
- {
- JobName = "Mike test job 1",
- Uri = @"https://www.cnblogs.com/mikecheers/p/12090487.html",
- };
- ant.Work(job);
- #endregion
- #region 以POST方式请求数据
- 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");
- new WorkerAnt
- {
- WorkerId = (uint)Math.Abs(DateTime.Now.ToString("HHmmssfff").GetHashCode())
- }.Work(new JobContext
- {
- JobName = "Mike test job 2",
- Uri = @"https://tool.runoob.com/compile.php",
- ContentType = @"application/x-www-form-urlencoded; charset=UTF-8",
- Method = WebRequestMethods.Http.Post,
- Buffer = requestData,
- });
- #endregion
- Console.WriteLine("End of Main method.");
- Console.ReadLine();
- }
最终效果:应用代码部分
[Code 2.1.2]
- Worker 471365603 JobStatus: WaitingForActivation
- Worker 471365603 is starting a job named 'Mike test job 1'.
- Worker 471365603 JobStatus: WaitingToRun
- Worker 471365603 JobStatus: Running
- Worker 1110506678 JobStatus: WaitingForActivation
- Worker 1110506678 is starting a job named 'Mike test job 2'.
- Worker 1110506678 JobStatus: WaitingToRun
- Worker 1110506678 JobStatus: Running
- End of Main method.
- Totally 0 downloaded.
- Totally 512 downloaded.
- Totally 1024 downloaded.
- Totally 1536 downloaded.
- Totally 2048 downloaded.
- Totally 2560 downloaded.
- Totally 2624 downloaded.
- Totally 3136 downloaded.
- Totally 3648 downloaded.
- Totally 4024 downloaded.
- Totally 4028 downloaded.
- Totally 4540 downloaded.
- Totally 5052 downloaded.
- Totally 5422 downloaded.
- Totally 5934 downloaded.
- Totally 6446 downloaded.
- Totally 6822 downloaded.
- Totally 7334 downloaded.
- Totally 7846 downloaded.
- Totally 8222 downloaded.
- Totally 8734 downloaded.
- Totally 9246 downloaded.
- Totally 9758 downloaded.
- Totally 10270 downloaded.
- Totally 10782 downloaded.
- Totally 10886 downloaded.
- <!DOCTYPE html>
- <html lang="zh-cn">
- <head>
- <meta charset="utf-8" />
- <meta name="v...
- /* ********************** using 000.75ms / request ******************** */
- Worker 471365603 JobStatus: RanToCompletion
- Totally 0 downloaded.
- Totally 81 downloaded.
- {"output":"\u300aC# \u722c\u866b \u7834\u5883\u4e4b\u9053\u300b\n","errors":"\n"}
- /* ********************** using 012.32ms / request ******************** */
- Worker 1110506678 JobStatus: RanToCompletion
最终效果:应用输出显示
在[Code 2.1.1]中,主要涉及两个类:
- WorkerAnt(工蚁):在我的爬虫世界里,工蚁就是最小的工作单位了,它们勤勤恳恳,辛勤劳作,任劳任怨,颇为辛苦!致敬!
- JobContext(任务上下文):主要承载了任务的描述(参数信息),任务的状态信息,一个计时器(统计任务消耗的时间),还有Request相关的对象、缓存等。
首先,我生产了两只小工蚁,并为他们分配了不同的任务,一个以GET方式采集数据,另一个以POST方式采集数据。工蚁提供了一个Work方法,用来向小工蚁发号施令,开工喽。
在[Code 2.1.2]中,我们可以跟踪到每只小工蚁的运行状态,采集到的数据以及耗时。同时也可以看到“End of Main method.”出现在比较靠前的位置,这说明我们的小工蚁还是有点儿小聪明的,可以采用异步的方式采集数据。
好了,应用的部分比较简单,相信大家都能够理解,接下来,我们主要介绍一下类库中的两个类(工蚁和任务上下文)。
[Code 2.1.3]
- namespace System.Crawler
- {
- using System;
- using System.Diagnostics;
- using System.IO;
- using System.Net;
- using System.Security.Cryptography.X509Certificates;
- using System.Text;
- using System.Threading.Tasks;
- public class JobContext
- {
- /// <summary>
- /// 任务名称
- /// </summary>
- public String JobName { get; set; }
- /// <summary>
- /// 任务状态
- /// </summary>
- public TaskStatus JobStatus { get; set; }
- /// <summary>
- /// 跑表,计时器。
- /// </summary>
- public Stopwatch Watch { get; set; }
- public WebRequest Request { get; set; }
- public WebResponse Response { get; set; }
- public Stream RequestStream { get; set; }
- public Stream ResponseStream { get; set; }
- public MemoryStream Memory { get; set; }
- public byte[] Buffer { get; set; }
- /// <summary>
- /// 请求的目标Uri
- /// </summary>
- public String Uri { get; set; }
- /// <summary>
- /// 设置509证书集合
- /// </summary>
- public X509CertificateCollection ClientCertificates { get; set; }
- /// <summary>
- /// Headers
- /// </summary>
- public WebHeaderCollection Headers { get; set; }
- /// <summary>
- /// 代理
- /// </summary>
- public IWebProxy Proxy { get; set; }
- /// <summary>
- /// 权限认证信息
- /// </summary>
- public ICredentials Credentials { get; set; }
- /// <summary>
- /// 获取或设置用于请求的 HTTP 版本。返回结果:用于请求的 HTTP 版本。默认为 System.Net.HttpVersion.Version11。
- /// </summary>
- public Version ProtocolVersion { get; set; }
- /// <summary>
- /// 获取或设置一个 System.Boolean 值,该值确定是否使用 100-Continue 行为。如果 POST 请求需要 100-Continue 响应,则为 true;否则为 false。默认值为 true。
- /// </summary>
- public bool Expect100Continue { get; set; }
- /// <summary>
- /// 设置Request请求方式
- /// </summary>
- public String Method { get; set; }
- // Summary:
- // Gets or sets the time-out value in milliseconds for the System.Net.HttpWebRequest.GetResponse()
- // and System.Net.HttpWebRequest.GetRequestStream() methods.
- //
- // Returns:
- // The number of milliseconds to wait before the request times out. The default
- // value is 100,000 milliseconds (100 seconds).
- //
- // Exceptions:
- // System.ArgumentOutOfRangeException:
- // The value specified is less than zero and is not System.Threading.Timeout.Infinite.
- public TimeSpan Timeout { get; set; }
- // Summary:
- // Gets or sets a time-out in milliseconds when writing to or reading from a
- // stream.
- //
- // Returns:
- // The number of milliseconds before the writing or reading times out. The default
- // value is 300,000 milliseconds (5 minutes).
- //
- // Exceptions:
- // System.InvalidOperationException:
- // The request has already been sent.
- //
- // System.ArgumentOutOfRangeException:
- // The value specified for a set operation is less than or equal to zero and
- // is not equal to System.Threading.Timeout.Infinite
- public TimeSpan ReadWriteTimeout { get; set; }
- // Summary:
- // Gets or sets the value of the Accept HTTP header.
- //
- // Returns:
- // The value of the Accept HTTP header. The default value is null.
- public string Accept { get; set; }
- // Summary:
- // Gets or sets the value of the Content-type HTTP header.
- //
- // Returns:
- // The value of the Content-type HTTP header. The default value is null.
- public string ContentType { get; set; }
- // Summary:
- // Gets or sets the value of the User-agent HTTP header.
- //
- // Returns:
- // The value of the User-agent HTTP header. The default value is null.NoteThe
- // value for this property is stored in System.Net.WebHeaderCollection. If WebHeaderCollection
- // is set, the property value is lost.
- public string UserAgent { get; set; }
- /// <summary>
- /// 返回数据编码默认为NUll,可以自动识别,一般为utf-8,gbk,gb2312
- /// </summary>
- public Encoding Encoding { get; set; }
- /// <summary>
- /// 请求时的Cookie
- /// </summary>
- public string Cookie { get; set; }
- public CookieCollection Cookies { get; set; }
- /// <summary>
- /// 来源地址
- /// </summary>
- public string Referer { get; set; }
- /// <summary>
- /// 是否允许自动跳转
- /// </summary>
- public bool AllowAutoRedirect { get; set; }
- /// <summary>
- /// 最大连接数
- /// </summary>
- public int ConnectionLimit { get; set; }
- public JobContext()
- {
- Uri = null;
- ClientCertificates = null;
- Headers = null;
- Proxy = null;
- ProtocolVersion = System.Net.HttpVersion.Version11;
- Expect100Continue = true;
- Method = WebRequestMethods.Http.Get;
- Timeout = TimeSpan.FromSeconds();
- ReadWriteTimeout = TimeSpan.FromMinutes();
- Accept = null;
- ContentType = null;
- UserAgent = null;
- Encoding = null;
- Cookie = null;
- Cookies = null;
- Referer = null;
- AllowAutoRedirect = true;
- ConnectionLimit = ;
- Credentials = null;
- }
- }
- }
任务上下文(JobContext)
这个类中主要汇集了任务描述相关的信息属性们、异步操作相关的属性们、以及构建一个HttpWebRequest所需的所有属性们;并且在构造函数中,给出了部分属性的默认值,我们也可以在构造中,自动生成WorkerId和/或JobName等。
虽然属性繁多,但没有什么逻辑,还好还好~
[Code 2.1.4]
- namespace System.Crawler
- {
- using System;
- using System.Diagnostics;
- using System.IO;
- using System.Net;
- using System.Net.Security;
- using System.Security.Cryptography.X509Certificates;
- using System.Text;
- using System.Threading.Tasks;
- /// <summary>
- /// 一个爬虫的最小任务单位,一只小工蚁。
- /// </summary>
- public class WorkerAnt
- {
- public UInt32 WorkerId { get; set; }
- public void Work(JobContext context)
- {
- Console.WriteLine($"Worker { WorkerId } JobStatus: " + (context.JobStatus = TaskStatus.WaitingForActivation).ToString());
- if (null == context)
- throw new ArgumentNullException($"Worker { WorkerId } can not start a job with no context.");
- if (null == context.Method)
- throw new ArgumentNullException($"Worker { WorkerId } can not start a job with no method.");
- if (null == context.Uri || !Uri.IsWellFormedUriString(context.Uri, UriKind.RelativeOrAbsolute))
- throw new FormatException($"Worker { WorkerId } can not start a job with uri '{context.Uri}' is not well formed.");
- if (string.IsNullOrEmpty(context.JobName))
- Trace.WriteLine($"Worker {WorkerId} is starting a job with no name.");
- else
- Trace.WriteLine($"Worker {WorkerId} is starting a job named '{context.JobName}'.");
- Console.WriteLine($"Worker { WorkerId } JobStatus: " + (context.JobStatus = TaskStatus.WaitingToRun).ToString());
- context.Watch = new Stopwatch();
- context.Watch.Start();
- //这一句一定要写在创建连接的前面。使用回调的方法进行证书验证。
- if (null != context.ClientCertificates && < context.ClientCertificates.Count)
- ServicePointManager.ServerCertificateValidationCallback = (sender, certificate, chain, errors) => true;
- var Request = (context.Request = WebRequest.CreateHttp(context.Uri)) as HttpWebRequest;
- if (null != context.ClientCertificates && < context.ClientCertificates.Count)
- foreach (X509Certificate item in context.ClientCertificates)
- Request.ClientCertificates.Add(item);
- if (null != context.Headers && context.Headers.Count > )
- Request.Headers.Add(context.Headers);
- Request.Proxy = context.Proxy;
- if (null != context.ProtocolVersion)
- Request.ProtocolVersion = context.ProtocolVersion;
- Request.ServicePoint.Expect100Continue = context.Expect100Continue;
- Request.Method = context.Method;
- Request.Timeout = (Int32)context.Timeout.TotalMilliseconds;
- Request.ReadWriteTimeout = (Int32)context.ReadWriteTimeout.TotalMilliseconds;
- Request.Accept = context.Accept;
- Request.ContentType = context.ContentType;
- Request.UserAgent = context.UserAgent;
- if (!string.IsNullOrEmpty(context.Cookie))
- Request.Headers[HttpRequestHeader.Cookie] = context.Cookie;
- if (null != context.Cookies)
- {
- Request.CookieContainer = new CookieContainer();
- Request.CookieContainer.Add(context.Cookies);
- }
- Request.Referer = context.Referer;
- Request.AllowAutoRedirect = context.AllowAutoRedirect;
- if ( < context.ConnectionLimit)
- Request.ServicePoint.ConnectionLimit = context.ConnectionLimit;
- Console.WriteLine($"Worker { WorkerId } JobStatus: " + (context.JobStatus = TaskStatus.Running).ToString());
- if (null != context.Buffer && < context.Buffer.Length)
- {
- Request.ContentLength = context.Buffer.Length;
- Request.BeginGetRequestStream(acGetRequestStream =>
- {
- var contextGetRequestStream = acGetRequestStream.AsyncState as JobContext;
- contextGetRequestStream.RequestStream = contextGetRequestStream.Request.EndGetRequestStream(acGetRequestStream);
- contextGetRequestStream.RequestStream.BeginWrite(context.Buffer, , context.Buffer.Length, acWriteRequestStream =>
- {
- var contextWriteRequestStream = acWriteRequestStream.AsyncState as JobContext;
- contextWriteRequestStream.RequestStream.EndWrite(acWriteRequestStream);
- contextWriteRequestStream.RequestStream.Close();
- GetResponse(contextWriteRequestStream);
- }, contextGetRequestStream);
- }, context);
- }
- else
- GetResponse(context);
- }
- 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);
- 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.Request.Abort();
- contextGetResponse.Response.Close();
- contextGetResponse.Buffer = null;
- var content = new UTF8Encoding(false).GetString(contextGetResponse.Memory.ToArray());
- Console.WriteLine(content.Length > ? content.Substring(, ) + "..." : content);
- contextGetResponse.Watch.Stop();
- Console.WriteLine("/* ********************** using {0}ms / request ******************** */"
- + Environment.NewLine + Environment.NewLine, (contextGetResponse.Watch.Elapsed.TotalMilliseconds / ).ToString("000.00"));
- Console.WriteLine($"Worker { WorkerId } JobStatus: " + (contextGetResponse.JobStatus = TaskStatus.RanToCompletion).ToString());
- }
- }), context);
- }
- }
- }
工蚁(WorkerAnt)
重点就是小工蚁类了,这个类一共提供了一个属性,一个公有方法,一个私有方法,没有提供构造函数,将使用默认构造函数,我们来详细解释一下:
- WorkerId:给每只小工蚁分配一个编号,就像人的身份证一样,没准等它老了,也能凭此编号领个社保什么的……
- Work(JobContext context):对小工蚁发号施令,要它去完成某采集任务。
- GetResponse(JobContext context):处理收到的回复数据。
属性没什么好说的了,主要说两个方法。
Work(JobContext context)方法:
在第21、37、89行,分别重置了context.JobStatus的状态,其实,每次重置状态,都寓意着流程上的层次划分,这里借用了枚举System.Threading.Tasks.TaskStatus来做状态标记位,可能在过去或将来某些Framework中将不支持,我们也可以定义自己的状态标记位,使得意义更加精确;
在第23~35行,对JobContext做了简单的校验,为了篇幅短小一点,这里并没有列出所有的校验规则,可以根据应用的实际情况来进行校验,示例中演示了如何检查空对象以及可选对象,当然还可以加入一些逻辑校验,比如:
- 当JobContext.Method="GET"的时候,就不能指定JobContext.Buffer,否则会抛出异常;
- 提供了JobContext.Credentials对象,但对象的属性是否合法;
- 提供了JobContext.Method,但它的值是否有效;
- 提供了JobContext.Headers,但其值是否可以加入到Request.Headers中,因为有些Key是只能通过Request的属性指定的,请参考:WebHeaderCollection.IsRestricted 方法
在第38、39行,开启跑表,到这里,就要开始构建Request对象,并对对象的属性进行赋值了。
在第41~87行,就是属性的赋值操作,不一一讲解了,大的原则就是判断JobContext中是否提供了相应的属性值,如果提供了,那么就赋值;
这里有几个需要注意的属性,说一下:
- ServicePointManager.ServerCertificateValidationCallback = (sender, certificate, chain, errors) => true; :这一句一定要写在创建连接的前面。使用回调的方法进行证书验证。作为爬虫,我们就不要为难自己去验证证书了,但如果我们做的是浏览器程序,那么,还是守点儿规矩吧:)
- Request.Proxy = context.Proxy; :Proxy属性比较特殊,当我们Create一个Request后,这个属性的默认值并不是Null,这也导致我们在使用过程中,总感觉第一个请求的耗时很长,后面的请求就很快。作为优化的一种方式,就是不去判断JobContext.Proxy是不是Null,都给这个属性赋值,即使JobContext.Proxy是Null,也要明确地给Request.Proxy赋值。
- Request.ProtocolVersion = context.ProtocolVersion; :ProtocolVersion属性的类型是Version,但在赋值的时候,不要自己随便new一个Version,而是使用System.Net.HttpVersion进行赋值,参考JobContext的构造函数。目前支持的值有Version10和Version11,分别对应HTTP 1.0和HTTP 1.1。
- Request.Timeout与Request.ReadWriteTimeout的区别在于:Timeout是用来限制GetResponse和GetRequestStream的超时时间,通俗点理解就是Timeout是单次采集的整体(包括Socket.Connect()、Request和Response、读写流)超时时间,默认值是100秒,通常我们会把这个时间设置的短一点,比如5~8秒,但需要注意的是,MSDN上说它只对同步操作有效,异步操作是不起作用的,所以,JobContext中的初始值并没有修改,还是保留了原来的初始值,如果你的小虫子是同步方式工作的,那么就需要注意它们的意义了;ReadWriteTimeout则是限制实际数据流读写操作的时间,同理,也是同步有效,异步无效。
- Request.ContinueTimeout:这个属性是在Framework 4.5加入的,它表示在接收到 100-Continue 之前要等待的超时(以毫秒为单位),如果在超时过期之前接收到 100-Continue 响应,则可以发送实体正文。如果我们配置了Request.ServicePoint.Expect100Continue = true,这个timeout就会起作用。因为100Continue的报文很小,所以这个超时的默认时间也很短,仅为350毫秒。这也算是Http协议的优化吧,本节结尾会贴一小段100Continue的报文,让大家有个直观的赶脚:)
- 还有示例中没有列举的其他属性,包括在新版本中加入的属性,大家可以参考MSDN文档的相关主题。也可以直接在VS中跳转到HttpWebRequest的定义,了解您当前版本适用的属性、方法。
在第91~108行,这里要真正开启采集工作,请求数据了。分了两条路走,一条是有PostData的,另一条是没有PostData的。有PostData的,就会先获取RequestStream,把数据写进去,注意,写完数据要关闭RequestStream,比第一境中示例的位置提前了。之后两条路都汇总到了GetResponse(JobContext)方法去获取回复信息了。
这样,发送请求的处理就完成了,接下来我们看最后一个方法,处理回复。
GetResponse(JobContext context)方法:
由于是私有方法,入口点只有Work方法,同时也是为了文章代码稍微短一点,我就没有再做参数校验了,如果以后扩展了,使用的地方多了,进行参数校验还是要做的。
这个方法与第一境中处理回复的部分没有大的变动,就不细说了,只是在第141行增加了一个设置任务状态的处理,标示任务处理完成了。
异常的处理:
在本示例中,并没有对异常进行捕获和处理,就像我将这个程序集命名为System.Crawler一样,我希望它能像System.xxx一样处理异常(就是不处理:),为什么这样设计呢,因为作为通用框架、组件、控件,本身并不知道一旦发生异常,应该怎么处理,是重试?还是默不作声?还是记录个日志了事?可能都不是业务逻辑所希望看到的。业务逻辑应该有自己的流程流转,应该有处理错误的能力,甚至是业务逻辑本身出错了,收到异常会帮助业务逻辑更加完善。
当然,示例中的应用也没有捕获异常,是我的不对,就是为了篇幅短小:)
另外就是附上说好的100 Continue报文:(通过Wireshark捕获)
[Code 2.1.5]
- >>> 1. 发送HTTP报头,包含Expect: 100-continue ----------------------------------------------
- POST /xxxxxxxxxxxxxx.ashx HTTP/1.1
- Content-Type: application/x-www-form-urlencoded
- encode_key: Ha1P29PAhyzRRmBiBkTJ6Q==
- Host: 192.168.216.22:9080
- Content-Length: 480
- Expect: 100-continue
- Connection: Keep-Alive
- <<< 2. 收到100Continue ----------------------------------------------
- HTTP/1.1 100 Continue
- Content-Length: 0
- Date: Sat, 21 Sep 2019 01:27:18 GMT
- Server: WebSphere Application Server/8.0
- >>> 3. 发送请求的数据实体 ----------------------------------------------
- LloRmU0xleMjr8VibuqgDUvL9++cFpBDwtRt89fbWw2UsHjS1+cCPVmn0t9y4NysUZXAYIAlS5odowFdI/h5HAsSNk7jjaVsEK9dFseNfN+TaIIlwagFwvEEZ6tjZ0pF90hmq90iiHzH5ylDjuSfC3OJUpPrDEfAogcq/nRe8TwVRtVVSZ20RH5o0hDc/ibMSOBI/qVW+c1Ala2xfknQHi5RRGXSd3NauL9Bd0Oxk4lDIbGcWxVByoU9oZCeB8in4KdbjQtiHebigTRNiyS6lglZXY482ArxRq2Gourld/9F/gFhSCExiiBGkfwy6nzmdB66/JxBk4GYiO9fEfjamQAt3hPs8cE7zEDnPN25dVvpwhP66e3c81aUigOi6+P6634CyoSjMqyivy5p9SJsdFLeZueqH7QhZUAkR4+o4lyHVcdfs2FXlZnl23AWyBEMlcrwwzuGEYzLJqzkoxWVJ9KJP5qRbjQM
- <<< 4. 收到 Response ----------------------------------------------
- HTTP/1.1 200 OK
- Content-Language: zh-CN
- Set-Cookie: JSESSIONID=0000Hc6wSQjAvFXM1m2GbqKaRSE:-1; Path=/; HttpOnly
- Transfer-Encoding: chunked
- Date: Sat, 21 Sep 2019 01:27:18 GMT
- Server: WebSphere Application Server/8.0
- Expires: Thu, 01 Dec 1994 16:00:00 GMT
- Cache-Control: no-cache="set-cookie, set-cookie2"
- 168
- 1279tozwn5CTZnLt1r2pAYHxJ8HES8K0Sc0yhi5O2Tsk+/uZLPRraJlU9mqe/m6NwKRaWYraQmdz
- oWGsKdAgWFge5tGXlr1mvQCZO4/fWXFxM117snEnBm5bfwB9Zq+NOiF2E3L1WmT2Ooet40WAvMoR
- 3ZznhdI5Fm6gS0H7nLaYeujOlzc/lZWIl29HQHdHbnIWqqxIXbvdb9wXIycgHwAecFNtAWT7iS9H
- BQcqajo5he2h1ehDn/kJns9YMwCWVDQ7iQW/tqqlRzxFhpaAaHQXT+fZK/nhFbomFwdAekz32M6t
- 4qnHFXBsU6ABX50+bCj+QZ/e4t1M6On/nJXyQoytQwfKFJWt
- 0
100 Continue 参考报文
好了,关于HTTP部分的解毒,到这里就告一段落了,欢迎大家在评论区或加入下面的QQ群与笔者进行交流。
喜欢本系列丛书的朋友,可以点击链接加入QQ交流群(994761602)【C# 破境之道】
方便各位在有疑问的时候可以及时给我个反馈。同时,也算是给各位志同道合的朋友提供一个交流的平台。
需要源码的童鞋,也可以在群文件中获取最新源代码。
《C# 爬虫 破境之道》:第二境 爬虫应用 — 第一节:HTTP协议数据采集的更多相关文章
- Python爬虫实践 -- 记录我的第二只爬虫
1.爬虫基本原理 我们爬取中国电影最受欢迎的影片<红海行动>的相关信息.其实,爬虫获取网页信息和人工获取信息,原理基本是一致的. 人工操作步骤: 1. 获取电影信息的页面 2. 定位(找到 ...
- 《C# 爬虫 破境之道》:第二境 爬虫应用 — 第二节:以事件驱动状态、数据处理
续上一节内容,对Web爬虫进行进一步封装,通过委托将爬虫自己的状态变化以及数据变化暴露给上层业务处理或应用程序. 为了方便以后的扩展,我先定义一个蚂蚁抽象类(Ant),并让WorkerAnt(工蚁)继 ...
- 《C# 爬虫 破境之道》:第二境 爬虫应用 — 第三节:处理压缩数据
续上一节内容,本节主要讲解一下Web压缩数据的处理方法. 在HTTP协议中指出,可以通过对内容压缩来减少网络流量,从而提高网络传输的性能. 那么问题来了,在HTTP中,采用的是什么样的压缩格式和机制呢 ...
- 《C# 爬虫 破境之道》:第一境 爬虫原理 — 第二节:WebRequest
本节主要来介绍一下,在C#中制造爬虫,最为常见.常用.实用的基础类 ------ WebRequest.WebResponse. 先来看一个示例 [1.2.1]: using System; usin ...
- 《C# 爬虫 破境之道》:第二境 爬虫应用 — 第四节:小说网站采集
之前的章节,我们陆续的介绍了使用C#制作爬虫的基础知识,而且现在也应该比较了解如何制作一只简单的Web爬虫了. 本节,我们来做一个完整的爬虫系统,将之前的零散的东西串联起来,可以作为一个爬虫项目运作流 ...
- 《C# 爬虫 破境之道》:第二境 爬虫应用 — 第五节:小总结带来的优化与重构
在上一节中,我们完成了一个简单的采集示例.本节呢,我们先来小结一下,这个示例可能存在的问题: 没有做异常处理 没有做反爬应对策略 没有做重试机制 没有做并发限制 …… 呃,看似平静的表面下还是隐藏着不 ...
- 《C# 爬虫 破境之道》:第二境 爬虫应用 — 第六节:反爬策略研究
之前的章节也略有提及反爬策略,本节,我们就来系统的对反爬.反反爬的种种,做一个了结. 从防盗链说起: 自从论坛兴起的时候,网上就有很多人会在论坛里发布一些很棒的文章,与当下流行的“点赞”“分享”一样, ...
- 《C# 爬虫 破境之道》:第二境 爬虫应用 — 第七节:并发控制与策略
我们在第五节中提到一个问题,任务队列增长速度太快,与之对应的采集.分析.处理速度远远跟不上,造成内存快速增长,带宽占用过高,CPU使用率过高,这样是极度有害系统健康的. 我们在开发采集程序的时候,总是 ...
- 《C# 爬虫 破境之道》:第一境 爬虫原理 — 第六节:第一境尾声
在第一境中,我们主要了解了爬虫的一些基本原理,说原理也行,说基础知识也罢,结果就是已经知道一个小爬虫是如何诞生的了~那么现在,请默默回想一下,在第一境中,您都掌握了哪些内容?哪些还比较模糊?如果还有什 ...
随机推荐
- H3C 衡量路由协议的主要指标
- linux 操作 I/O 端口
在驱动硬件请求了在它的活动中需要使用的 I/O 端口范围之后, 它必须读且/或写到这些 端口. 为此, 大部分硬件区别 8-位, 16-位, 和 32-位端口. 常常你无法混合它们, 象你 正常使 ...
- C# 性能分析 反射 VS 配置文件 VS 预编译
本文分析在 C# 中使用反射和配置文件和预编译做注入的性能,本文的数据是为预编译框架,开发高性能应用 - 课程 - 微软技术暨生态大会 2018 - walterlv提供 本文通过代码生成工具,使用C ...
- Linux 内核PCI去除一个设备
一个 PCI 可用多个不同的方法被从系统中去除. 所有的 card-bus 设备在一个不同的物 理因素上是真正的 PCI 设备, 并且内核 PCI 核心不区分它们. 允许在机器运行时加减 PCI 设备 ...
- 【35.53%】【POJ 2912】Rochambeau
Time Limit: 5000MS Memory Limit: 65536K Total Submissions: 2837 Accepted: 1008 Description N childre ...
- LightOJ - 1284 Lights inside 3D Grid (概率计算)
题面: You are given a 3D grid, which has dimensions X, Y and Z. Each of the X x Y x Z cells contains a ...
- 【NOIP模拟赛】【数学】完全平方数
问题描述 一个数如果是另一个整数的完全平方,那么我们就称这个数为完全平方数(Pefect Sqaure),也称平方数. 小A认为所有的平方数都是很perfect的~ 于是他给了小B一个任务:用任意个不 ...
- FreeNOS学习3——了解目录结构和Main函数
下载源码的文件夹,看到里面有以下内容: 解释一下里面都有啥 lib -> 共享代码 1.libfs 文件系统(磁盘管理) 2.liballoc 内存分配和虚拟内存映射(内存管理) 3.libex ...
- acwing 102 -利用二分枚举区间平均值
我真的是服了,看了一晚上发现居然,,,,, 上图吧,话说有人评论没... 对于结果来说,不一定要枚举有序数列,感觉这是一种猜结果的方法,只不过特别精确,令人发指 #include<cstdio& ...
- 【题解】P2521 [HAOI2011]防线修建(动态凸包)
[题解]P2521 [HAOI2011]防线修建(动态凸包) 凸包是易插入不好删除的东西,按照剧情所以我们时光倒流 然后问题就是维护凸包的周长,支持加入 本来很简单,但是计算几何就是一些小地方经验不足 ...