首页 > Python基础教程 >
-
DotNet应用之爬虫入门系列(二):HttpClient的前世今生
对想写网页爬虫的人来说,最先要搞清楚的基础知识就是HTTP/HTTPS,以及所使用的编程语言对应该领域的基础库。因此,本文将为有兴趣的读者介绍.Net基础库中涉及HTTP的基础部分,并展示一些简单易懂的示例(同时,我计划在下期或下下期提供一个稍有难度、较为完整的爬虫案例,让新手更深刻地理解.Net库对HTTP的封装,以及如何处理编写爬虫过程中的意外情况)。另外须提示的是,看懂本文起码需要知道Http相关的“概念性”基础知识,例如知道GET、POST方法是什么,Http请求的Headers通常有哪些及分别表示什么等等;一些案例如果自己想尝试和复现,需要知道浏览器调试或抓包工具使用方面的知识(这些资料网上非常丰富,笔者不再赘叙,浏览器建议选择谷歌或火狐,抓包工具有Windows适用的Fiddler或Mac上常用的Charles)。
前世的HttpWebRequest/HttpWebResponse
早先使用C#编写一个Http请求,需要用到System.Net.HttpWebRequest类。这个类的设计可谓非常之原始,但这也意味着它非常贴合编写Http请求的需要。

让我们借一个很简单的例子来看看编写出的代码。简书,是一个非常简洁、别致的创作社区(知乎别吃醋哦)。在它的首页上,有一个“推荐作者”的区域(已用红色方框标注),如图:

通过浏览器调试可以很轻松地发现,这个区域的数据是通过一个请求单独获取的:

那么,我们按部就班地向HttpWebRequest实例填入网站所需的请求参数,就可以获得我们想要的响应文本。控制台代码如下:
HttpWebRequest request = WebRequest.CreateHttp("https://www.jianshu.com/users/recommended?seen_ids=&count=5&only_unfollowed=true");
request.Method = "GET";
request.Accept = "application/json";
request.Referer = "https://www.jianshu.com/";
request.UserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.100 Safari/537.36";
request.Host = "www.jianshu.com";
request.KeepAlive = false;
request.Timeout = 5000;
request.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate;
request.Headers.Add(HttpRequestHeader.AcceptLanguage, "zh-CN,zh;q=0.9");
request.Headers.Add(HttpRequestHeader.AcceptEncoding, "br");
//request.Proxy = new WebProxy("127.0.0.1", 8888);//让Fiddler抓包以查看实际发送出去的请求
var response = request.GetResponse() as HttpWebResponse;
if (response.StatusCode == HttpStatusCode.OK)
{
Stream rspStrm = response.GetResponseStream();
var reader = new StreamReader(rspStrm);
string rspText = reader.ReadToEnd();
Console.WriteLine(rspText);
}
Console.ReadKey();
运行代码后就可以在控制台程序上打印出响应文本(很明显,这是一个Json格式的文本):

让我们回头看一下这段代码。如果读者对Http有了基础性认识的话,应该会觉得这样的代码很亲切,无非就是复制粘贴,依次给类里的属性赋值即可。这是抽象层次低的好处,非常方便理解。但这也意味着使用者要注意的地方变多了,例如:
- Method属性是一个String类型,如果不小心写错了,就会触发异常。
- GetResponse()方法返回的是一个父类——WebResponse类,每次都需要通过类型转换变成HttpWebResponse对象。
- 输出文本并没有相应的API,只能先通过HttpWebResponse对象的GetResponseStream()方法获取Stream,然后通过StreamReader读取出文本。
- 示例代码未启用Cookie参数,如果启用,需要自己创建并维护一个CookieContainer。
- 包含很多不常用的属性和配置,显得臃肿、复杂。
这还只是单纯代码层面而言。如果是严肃的项目代码,还需要注意性能方面的问题,例如:
- 单个HttpWebRequest默认只支持2个线程(.Net遵循了“Http协议规定,同个Http请求的并发连接数最大为2”),多线程环境下需要通过设置ServicePointManager类的DefaultConnectionLimit属性来解决(对代码的焦点产生了干扰)。
- 每个请求都需要创建一个HttpWebRequest对象,过多的对象会导致资源来不及释放直至端口耗尽。
- 一些特殊场景下的性能问题(比如HttpWebRequest.GetRequestStreamAsync doesn't open the connection to the server - we always buffer the content #11873)。
以及代码之外的现实问题,例如:
- .Net Framework和.Net Core的实现不太一样了,可能会导致行为上有不同的结果(事实是,Github上.Net Core库有不少与此相关的issues,比如HttpWebResponse.Cookies different behavior in .net framework and .net core #33122)。
- 官方文档网站Docs明确表示了“不建议使用HttpWebRequest进行新的开发”。

可以说,对于.Net Core项目,HttpWebRequest/HttpWebResponse已经成为了“历史遗产”。

不过,官方也是有两手准备:Avoid creating a new HttpClient per HttpWebRequest in some cases #15460。未来可能会在.Net5(其实就是.Net Core 4.0,微软改名部又发功了)又有一些优化工作。当然,作为一个新手来说,我们并不需要了解得那么详细,以上只是为了说明:HttpWebRequest/HttpWebResponse的设计,作为一个基础类库来说,它仅仅满足了让你“能用”,却还是“用起来不太舒服”。
今生的HttpClient
伴随着时代发展,.Net重新整合设计了System.Net.Http.HttpClient(Java程序猿可能有话说:“这不是跟Java抄的嘛!”嗯……名字的确是一样)。那么HttpClient又变成什么样子了呢?

对比一下“前世”,我们惊喜地发现HttpClient的API似乎更精简了。它自带了常用的Http请求方法,比如获取百度首页,我们现在可以写成短短几行代码:
using (var client = new HttpClient())
{
//client.DefaultRequestHeaders.Add("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.100 Safari/537.36");
string rspText = client.GetStringAsync("https://www.baidu.com").Result;//核心代码就这么一行,注意这里为了示例简单而未使用异步语法而是直接同步调用Result
Console.WriteLine(rspText);
}
结合上图,可以先简单总结几点改进:
- HttpClient使用自带的GetStringAsync()方法就可以获取到响应文本,相应地还有GetStreamAsync()方法和GetAsync()方法,由浅入深,应有尽有,可以充分满足不同场景的需求。
- 同时很明显的是,HttpClient的APIs全部是异步方法(且是线程安全的),这意味着HttpClient天生就是为高并发场景而设计。
- 原本HttpWebRequest臃肿的各种属性,现在分散到HttpMessageHandler/HttpRequestMessage/HttpResponseMessage/HttpContent中了,虽然对编程新手来说抽象的提高意味着更高的学习成本,但一旦理解就会发现这样的设计使得代码更精简易读、逻辑焦点更突出。
现在我们尝试加入代理:
var handler = new SocketsHttpHandler() { UseProxy = true, Proxy = new WebProxy("127.0.0.1", 8888), UseCookies = false, AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate };
using (var client = new HttpClient(handler))
{
client.DefaultRequestHeaders.Add("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.100 Safari/537.36");
string rspText = client.GetStringAsync("https://www.baidu.com").Result;
Console.WriteLine(rspText);
}
可以看到我们不需要去修改请求参数赋值的代码块,而是引入一个HttpMessageHandler的子类SocketsHttpHandler,替换HttpClient的重载构造方法即可。如果我们要存储Cookie呢?只需要将UseCookie设为true即可——HttpClient在绝大多数情况下不需要手动维护Cookie。
现在让我们重写上面简书的例子:
using System;
using System.Net;
using System.Net.Http;
namespace ConsoleAppTest
{
internal class Program
{
private static async void Main(string[] args)//.Net Core 2.1版本以上支持异步形式的Main方法
{
var handler = new SocketsHttpHandler() { UseProxy = true, Proxy = new WebProxy("127.0.0.1", 8888), UseCookies = false, AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate };
using (var client = new HttpClient(handler))
{
client.Timeout = TimeSpan.FromSeconds(5);
client.DefaultRequestHeaders.Host = "www.jianshu.com";
client.DefaultRequestHeaders.Referrer = new Uri("https://www.jianshu.com/");
client.DefaultRequestHeaders.Add("Accept", "application/json");
client.DefaultRequestHeaders.Add("Accept-Encoding", "gzip, deflate, br");
client.DefaultRequestHeaders.Add("Accept-Language", "");
client.DefaultRequestHeaders.Add("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.100 Safari/537.36");
string rspText = await client.GetStringAsync("https://www.jianshu.com/users/recommended?seen_ids=&count=5&only_unfollowed=true");
Console.WriteLine(rspText);
//也可以使用更底层且更灵活的方式,满足不同的需求。如下:
//HttpRequestMessage reqMsg = new HttpRequestMessage(HttpMethod.Get, "https://www.jianshu.com/users/recommended?seen_ids=&count=5&only_unfollowed=true");
//HttpResponseMessage rspMsg = await client.SendAsync(reqMsg);
//if(rspMsg.IsSuccessStatusCode)
//{
//string rspText = await rspMsg.Content.ReadAsStringAsync();
//Console.WriteLine(rspText);
//}
}
Console.ReadKey();
}
}
}
是否感觉这样的代码,结构层次更强,逻辑的焦点更明确?
还有,我们经常会遇到一些类似翻页的情况,还是以简书首页的“推荐作者”为例:

当我们点击换一批时,会发现请求变成了:

seen_ids记录了我们看过的推荐作者id,所以我们需要之前所有请求得到的作者id,拼成新的URL再做新的请求。如果使用HttpWebRequest,那么每次都要生成新的对象,并为它赋值后再请求。而HttpClient推荐的用法是尽量使用单例,而此案例只需使用1个HttpClient,就足够满足我们的需求。参考上面注释区域的代码,我们只需要每次生成一个极轻量的HttpRequestMessage,通过SendAsync()方法依次请求即可,最大程度地复用了HttpClient的DefaultRequestHeaders部分。我想从这一点,就足以令读者发现分离设计的好处。
HttpClient的发展之路还远远没有结束,它还在不断地被.Net社区讨论和优化。关于HttpClient更多的详细信息,可以阅读官方文档的“注解”:

小结
笔者仅以此文向读者粗略介绍了.Net上Http基础库的基本用法——是的,以上还只是“粗略”、“基本”——但是起码我们可以写一些像样的爬虫了。爬到了数据,我们就可以尝试挖掘数据的价值,就可以像有些文章作者一样,贴出各种好看的图表来炫耀自己的所得了。希望这种获得和挖掘的乐趣能够不断驱动着读者们的技术人生,让我们下期再会!