聊一聊.NET的网页抓取和编码转换

在本文中,你会了解到两种用于 HTML 解析的类库。另外,我们将讨论关于网页抓取,编码转换和压缩处理的知识,以及如何在 .NET 中实现它们,最后进行优化和改进。

1. 背景

有了 Copilot 的加持,可以让我们快速的完成开发任务,并在极短的时间内完成小工具的开发。谁能想到现如今,写的代码注释却是为了给 AI 看,甚至不需要写注释,AI 都能猜的懂你的意图。如今代码本身更是不值钱了,只有产品才能体现它的价值。

因为平时会看小说作为娱乐消遣,习惯使用本地纯文本的阅读器,这就涉及到小说的下载,有的网站是提供有 TXT 的直接下载,但有的小说网站就没有提供。当然我也有用过 uncle-novel[1] 这样类似的工具,用起来也还是很不错的,但总感觉有些不是很顺手。.

2. 网页抓取

在.NET中,HtmlAgilityPack[2] 库是经常使用的 HTML 解析工具,为解析 DOM 提供了足够强大的功能支持,经常用于网页抓取分析任务。

var web = new HtmlWeb();var doc = web.Load(url);

在我写的小工具中也使用了这个工具库,小工具用起来也是顺手,直到前几天抓取一个小说时,发现竟出现了乱码,这才意识到之前抓取的网页均是 UTF-8 的编码,今次这个是 GBK 的。

虽然 HtmlAgilityPack 提供了 AutoDetectEncoding 功能,也是默认开启状态,但是似乎实际效果并没有起效。通过使用 HttpClient 拿到htmlStream 后喂给 HtmlDocument 启用 OptionReadEncoding 也是一样。

3. 编码转换

既如此,那就直接用 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);

4. 网页压缩处理

在使用 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");

然后我测试一番,发现我的代码就不能跑了,人麻了,该不是网站有什么高深的防火墙吧:

聊一聊.NET的网页抓取和编码转换

压缩导致乱码

调试了半天,才想起来,莫不是因为加入了压缩的请求头吧?

聊一聊.NET的网页抓取和编码转换

注释掉再次测试,果然是它。哎,本想着你好我好大家好,加上压缩,这抓的速度更快,对面也省流量。

不过,注释是不可能注释掉的,遇到问题就解决问题,直接问 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]方便的多。

5. 代码优化

通过前面的调整,我们基本已经写好了核心代码。当然,优化的空间还是很大的,这里我们可以直接请 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;}

5.1 更换 Html 解析库

事情的起因是 HtmlAgilityPack 库的自动编码解析出现了问题,那么有没有其他替代的库呢?

当然,GPT4 推荐了 AngleSharp[4] ,这个库我简单测试了一下,无需配置可以直接识别网页编码,看起来是比 HtmlAgilityPack 好用一些。另外,其还支持输出 Javascript、Linq 语法、ID 和 Class 选择器、动态添加节点、支持 Xpath 语法。

总的来说,此番虽然是造了轮子,但是编程知识却是增加了嘛。

聊一聊.NET的网页抓取和编码转换

5.2 对于轮子的优化

虽然有以下要优化的地方,但是真的不如直接换轮子来的方便啊,因为换了轮子就没有下面的问题了:

1.对于实际的使用,使用静态的 HttpClient 实例,而不是为每个请求创建一个新的 HttpClient 实例。这可以避免不必要的资源浪费。可以将其及其配置移到一个单独的帮助类中如:HttpClientHelper,并在需要时访问它。2.这里我们单独写了一个函数,在其中使用了额外的编码注册 Encoding.RegisterProvider(CodePagesEncodingProvider.Instance),在实际使用中,应该将其放在程序启动时执行。这样,只需在程序启动时注册一次编码提供程序,而不是每次调用方法时都注册。3.  其他一些写法上的优化,如 switch 和方法命名等。

6. 最后

这篇文章是我在开发 BookMaker 小工具时的一些关于网页抓取的心得,主要介绍了两个 Html 解析库,解决了编码转换和压缩的一些问题,希望对大家能有所帮助。