在本文中,你会了解到两种用于 HTML 解析的类库。另外,我们将讨论关于网页抓取,编码转换和压缩处理的知识,以及如何在 .NET 中实现它们,最后进行优化和改进。
有了 Copilot 的加持,可以让我们快速的完成开发任务,并在极短的时间内完成小工具的开发。谁能想到现如今,写的代码注释却是为了给 AI 看,甚至不需要写注释,AI 都能猜的懂你的意图。如今代码本身更是不值钱了,只有产品才能体现它的价值。
因为平时会看小说作为娱乐消遣,习惯使用本地纯文本的阅读器,这就涉及到小说的下载,有的网站是提供有 TXT 的直接下载,但有的小说网站就没有提供。当然我也有用过 uncle-novel[1] 这样类似的工具,用起来也还是很不错的,但总感觉有些不是很顺手。
在.NET中,HtmlAgilityPack[2] 库是经常使用的 HTML 解析工具,为解析 DOM 提供了足够强大的功能支持,经常用于网页抓取分析任务。
var web = new HtmlWeb();
var doc = web.Load(url);
在我写的小工具中也使用了这个工具库,小工具用起来也是顺手,直到前几天抓取一个小说时,发现竟出现了乱码,这才意识到之前抓取的网页均是 UTF-8
的编码,今次这个是 GBK
的。
虽然 HtmlAgilityPack
提供了 AutoDetectEncoding
功能,也是默认开启状态,但是似乎实际效果并没有起效。通过使用 HttpClient
拿到htmlStream
后喂给 HtmlDocument
启用 OptionReadEncoding
也是一样。
既如此,那就直接用 HttpClient
抓了再说,虽然解析还是逃不过 HtmlAgilityPack
。对于 GBK
的支持,这里则需要引入System.Text.Encoding.CodePages
包。
对于抓取的网页内容我们先读取 bytes 然后以 UTF-8
编码读取后,通过正则解析出网页的实际的字符编码,并根据需要进行转换。
var client = new HttpClient();
var response = await client.GetAsync(url);
var bytes = await response.Content.ReadAsByteArrayAsync();
var htmldoc = Encoding.UTF8.GetString(bytes);
var match = Regex.Match(htmldoc, "<meta.*?charset=\"?(?<charset>.*?)\".*?>", RegexOptions.IgnoreCase);
在使用 HttpClient
抓取网页时,最好是加入个请求头进行伪装一番,Copilot 也是真的省事,注释“设置请求头”一写直接回车,都不用去搜浏览器 UA 的。说起搜索,基本上搜索除了要被搜索引擎的广告折磨外,也有可能被某些吸引人的热搜转移精力,然后就没有然后了……
不过,这次回车可能敲多了,把我敲坑里了。本来只是想加个 UA,觉得提示的也还挺有用的,最后加了一堆:
// 设置请求头
client.DefaultRequestHeaders.Add("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " +
"AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.149 Safari/537.36");
client.DefaultRequestHeaders.Add("Accept", "*/*");
client.DefaultRequestHeaders.Add("Accept-Encoding", "gzip, deflate, br");
client.DefaultRequestHeaders.Add("Accept-Language", "zh-CN,zh;q=0.9");
client.DefaultRequestHeaders.Add("Connection", "keep-alive");
然后我测试一番,发现我的代码就不能跑了,人麻了,该不是网站有什么高深的防火墙吧:
压缩导致乱码
调试了半天,才想起来,莫不是因为加入了压缩的请求头吧?
注释掉再次测试,果然是它。哎,本想着你好我好大家好,加上压缩,这抓的速度更快,对面也省流量。
不过,注释是不可能注释掉的,遇到问题就解决问题,直接问 GPT 就是了。大段大段复杂的解决方法,解压缩的方式这里就不说了。当我告诉 GPT 我用的最新的 .NET 开发,你给我优雅一些后,它果然就优雅了起来:
var handler = new HttpClientHandler
{
AutomaticDecompression = System.Net.DecompressionMethods.GZip | System.Net.DecompressionMethods.Deflate | System.Net.DecompressionMethods.Brotli
};
var httpClient = new HttpClient(handler);
毕竟 HttpClient
是支持自动处理压缩的。可以使用 HttpClientHandler
来启用自动解压缩功能,确实比去找官方文档[3]方便的多。
通过前面的调整,我们基本已经写好了核心代码。当然,优化的空间还是很大的,这里我们可以直接请 GPT4 来帮忙处理:
/// <summary>
/// 下载网页内容,并将其他编码转换为 UTF-8 编码
/// 记得看后面的优化说明
/// </summary>
static async Task<string> GetWebHtml(string url){
// 使用 HttpClient 下载网页内容
var handler = new HttpClientHandler();
// 忽略证书错误
handler.ServerCertificateCustomValidationCallback = (message, cert, chain, errors) => true;
handler.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate | DecompressionMethods.Brotli;
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/80.0.3987.149 Safari/537.36");
client.DefaultRequestHeaders.Add("Accept", "*/*");
// 加上后不处理解压缩会乱码
client.DefaultRequestHeaders.Add("Accept-Encoding", "gzip, deflate, br");
client.DefaultRequestHeaders.Add("Accept-Language", "zh-CN,zh;q=0.9");
client.DefaultRequestHeaders.Add("Connection", "keep-alive");
var response = await client.GetAsync(url);
var bytes = await response.Content.ReadAsByteArrayAsync();
// 获取网页编码 ContentType 可能为空,从网页获取
var charset = response.Content.Headers.ContentType?.CharSet;
if (string.IsNullOrEmpty(charset))
{
// 从网页获取编码信息
var htmldoc = Encoding.UTF8.GetString(bytes);
var match = Regex.Match(htmldoc, "<meta.*?charset=\"?(?<charset>.*?)\".*?>", RegexOptions.IgnoreCase);
if (match.Success) charset = match.Groups["charset"].Value;
else charset = "utf-8";
}
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
Encoding encoding;
switch (charset.ToLower())
{
case "gbk":
encoding = Encoding.GetEncoding("GBK");
break;
case "gb2312":
encoding = Encoding.GetEncoding("GB2312");
break;
case "iso-8859-1":
encoding = Encoding.GetEncoding("ISO-8859-1");
break;
case "ascii":
encoding = Encoding.ASCII;
break;
case "unicode":
encoding = Encoding.Unicode;
break;
case "utf-32":
encoding = Encoding.UTF32;
break;
default:
return Encoding.UTF8.GetString(bytes);
}
// 统一转换为 UTF-8 编码
var html = Encoding.UTF8.GetString(Encoding.Convert(encoding, Encoding.UTF8, bytes));
return html;
}
事情的起因是 HtmlAgilityPack
库的自动编码解析出现了问题,那么有没有其他替代的库呢?
当然,GPT4 推荐了 AngleSharp[4] ,这个库我简单测试了一下,无需配置可以直接识别网页编码,看起来是比 HtmlAgilityPack
好用一些。另外,其还支持输出 Javascript、Linq 语法、ID 和 Class 选择器、动态添加节点、支持 Xpath 语法。
总的来说,此番虽然是造了轮子,但是编程知识却是增加了嘛。
虽然有以下要优化的地方,但是真的不如直接换轮子来的方便啊,因为换了轮子就没有下面的问题了:
1.对于实际的使用,使用静态的 HttpClient
实例,而不是为每个请求创建一个新的 HttpClient
实例。这可以避免不必要的资源浪费。可以将其及其配置移到一个单独的帮助类中如:HttpClientHelper
,并在需要时访问它。2.这里我们单独写了一个函数,在其中使用了额外的编码注册 Encoding.RegisterProvider(CodePagesEncodingProvider.Instance)
,在实际使用中,应该将其放在程序启动时执行。这样,只需在程序启动时注册一次编码提供程序,而不是每次调用方法时都注册。3. 其他一些写法上的优化,如 switch 和方法命名等。
这篇文章是我在开发 BookMaker
小工具时的一些关于网页抓取的心得,主要介绍了两个 Html 解析库,解决了编码转换和压缩的一些问题,希望对大家能有所帮助。
[1]
uncle-novel: https://github.com/uncle-novel/uncle-novel?WT.mc_id=DT-MVP-5005195
[2]
HtmlAgilityPack: https://github.com/zzzprojects/html-agility-pack?WT.mc_id=DT-MVP-5005195
[3]
官方文档: https://learn.microsoft.com/zh-cn/dotnet/api/system.net.http.httpclienthandler.automaticdecompression?WT.mc_id=DT-MVP-5005195
[4]
AngleSharp: https://github.com/AngleSharp/AngleSharp?WT.mc_id=DT-MVP-5005195