C#作为后端编程语言相当流行,你可能会发现自己需要它来爬取一个或多个网页。在这篇文章中,我们将介绍如何使用C#来爬取一个网站。具体来说,我们将指导你如何发送HTTP请求,如何用C#解析收到的HTML文档,以及如何访问和提取我们想要的信息。
正如我们在其他文章中提到的,只要我们爬取服务器渲染或服务器组成的HTML,这就能很好地工作。当我们要处理单页应用程序,或任何其他严重依赖JavaScript的东西时,事情就变得复杂了。这就是我们将在本文第二部分讨论的内容,我们将深入了解PuppeteerSharp、Selenium WebDriver for C#和Headless Chrome。
注意:本文假设读者熟悉C#和ASP.NET,以及HTTP请求库。PuppeteerSharp和Selenium WebDriver .NET库可以让开发者更容易整合Headless Chrome。此外,该项目还使用了.NET Core 3.1框架和HTML Agility Pack来解析原始HTML。
第一部分:静态页面
设置
如果你使用C#这种语言,你可能已经使用了Visual Studio。本文使用一个简单的.NET核心网络应用程序项目,使用MVC(模型视图控制器)。在你创建了一个新项目后,使用NuGet包管理器来添加本教程中所使用的必要库。
在NuGet中,点击 “浏览 “标签,然后输入 “HTML Agility Pack “来获取该软件包。
安装该软件包,然后你就可以开始了。这个软件包可以很容易地解析下载的HTML,找到你想保存的标签和信息。
最后,在你开始对刮刀进行编码之前,你需要在代码库中添加以下库。
using HtmlAgilityPack; using System.Net.Http; using System.Net.Http.Headers; using System.Threading.Tasks; using System.Net; using System.Text; using System.IO;
在C#中向网页发出HTTP请求
想象一下,你有一个项目,需要从维基百科上搜罗著名软件工程师的信息。如果维基百科没有这样的文章,它就不是维基百科了,对吗?
对吗?
https://en.wikipedia.org/wiki/List_of_programmers
那篇文章有一个程序员的名单,并有他们各自的维基百科页面的链接。你可以爬取该名单,并将信息保存到CSV文件中(例如,你可以很容易地用Excel处理),以供日后使用。
这只是一个简单的例子,说明你可以用网络抓取来做什么,但一般的概念是找到一个有你需要的信息的网站,用C#来抓取内容,并将其存储起来供以后使用。在更复杂的项目中,你可以使用在顶级分类页面上发现的链接来抓取页面。
不过,让我们在下面的例子中专注于那个特定的维基百科页面。
使用.NET的HttpClient来检索HTML
.NET已经在其System.Net.Http
命名空间中自带了一个HTTP客户端(恰当地命名为HttpClient
),所以不需要任何外部的第三方库或依赖。此外,它还支持开箱即用的异步调用。
使用GetStringAsync()
,可以比较直接地以异步、非阻塞的方式获得任何URL的内容,我们可以在下面的例子中看到。
private static async Task CallUrl(string fullUrl) { HttpClient client = new HttpClient(); var response = await client.GetStringAsync(fullUrl); return response; }
我们简单地实例化一个新的HttpClient对象,调用GetStringAsync(),”等待 “其完成,并将完成的任务返回给我们的调用者。现在我们只需将该方法添加到我们的控制器类中,我们就可以从我们的索引()方法中调用CallUrl()了。让我们实际操作一下:
public IActionResult Index() { string url = "https://en.wikipedia.org/wiki/List_of_programmers"; var response = CallUrl(url).Result; return View(); }
在这里,我们在url中定义了我们的维基百科的URL,它是CallUrl(),并在我们的响应变量中存储其响应。
好了,发出HTTP请求的代码已经完成。我们还没有对它进行解析,但现在是运行代码的好时机,以确保返回的是维基百科的HTML而不是任何错误。
为此,我们首先要在Index()方法中的return View();处设置一个断点。这将确保你可以使用Visual Studio调试器用户界面来查看结果。
你可以通过点击Visual Studio菜单中的 “运行 “按钮来测试上述代码。Visual Studio将在断点处停止,现在你可以查看应用程序的当前状态。
如果你从上下文菜单中选择 “HTML Visualizer”,你会得到一个HTML页面的预览,但通过悬停在变量上,我们可以看到,我们得到了一个由服务器返回的适当的HTML页面,所以我们应该可以了。
解析HTML
检索到HTML后,就该对其进行解析了。HTML Agility Pack是一个流行的解析器套件,例如,它也可以很容易地与LINQ结合。
在解析HTML之前,你需要对页面的结构有一定的了解,这样你才知道到底要提取哪些元素。这就是你的浏览器的开发工具将再次大显身手的地方,因为它们允许你详细分析DOM树。
对于我们的维基百科页面,我们会注意到我们的目录中有很多链接,我们不需要这些。还有不少其他的链接(例如编辑链接),我们的数据集不一定需要这些链接。仔细观察,我们注意到所有我们感兴趣的链接都是<li>
父类的一部分。
根据DOM树,我们现在已经确定,<li>
s不仅用于我们的实际链接元素,而且还用于页面的内容表。由于我们并不真正追求内容表,我们需要确保过滤掉这些<li>
s,幸运的是,它们有自己独特的HTML类,所以我们可以简单地在代码中排除所有具有tocsection
类的<li>
元素。
是时候编码了!我们首先为我们的控制器类添加一个方法,ParseHtml()
。
private List ParseHtml(string html) { HtmlDocument htmlDoc = new HtmlDocument(); htmlDoc.LoadHtml(html); var programmerLinks = htmlDoc.DocumentNode.Descendants("li") .Where(node => !node.GetAttributeValue("class", "").Contains("tocsection")) .ToList(); List wikiLink = new List(); foreach (var link in programmerLinks) { if (link.FirstChild.Attributes.Count > 0) wikiLink.Add("https://en.wikipedia.org/" + link.FirstChild.Attributes[0].Value) ; } return wikiLink; }
在这里,我们首先创建一个HtmlDocument的实例,并加载我们先前从CallUrl()收到的HTML文档。现在我们有了一个合适的文档的DOM表示,可以继续爬取该文档了。
- 我们用
Descendants(
)获得所有<li>
的孩子。 - 我们使用LINQ
(Where(
))来过滤出使用上述HTML类的元素 - 我们对我们的链接进行迭代
(foreach
),并在我们的wikiLink
字符串列表中把它们的(相对)URLs保存为绝对URLs - 我们将字符串列表返回给我们的调用者
XPath
有一点需要注意,我们本来不需要手动进行元素选择。我们这样做只是为了举例说明。
在现实世界的代码中,应用一个XPath表达式会更方便。有了它,我们的整个选择逻辑将适合于一个单行代码。
var programmerLinks = htmlDoc.DocumentNode.SelectNodes("//li[not(contains(@class, 'tocsection'))]")
这与我们的手动选择遵循相同的逻辑,并将选择所有(//
)不包含所述类(not(contains())
)的<li>
s。
将爬取到的数据导出到一个文件中
到目前为止,我们已经从维基百科获取了HTML文档,将其解析为DOM树,并设法提取了所有需要的链接,现在我们有一个来自该页面的通用链接列表。
现在,我们想把这些链接导出到CSV文件中。我们将添加另一个名为WriteToCsv()
的方法,将通用列表中的数据写到文件中。下面的代码是完整的方法,它将提取的链接写入一个名为 “links.csv “的文件并存储在本地磁盘上。
private void WriteToCsv(List links) { StringBuilder sb = new StringBuilder(); foreach (var link in links) { sb.AppendLine(link); } System.IO.File.WriteAllText("links.csv", sb.ToString()); }
上述代码就是使用本地.NET框架库将数据写入本地存储的文件的全部内容。
但为了回顾一下,让我们也把HomeController的代码完整地贴出来。
using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; using HtmlAgilityPack; using System.Net.Http; using System.Net.Http.Headers; using System.Threading.Tasks; using System.Net; using System.Text; using System.IO; namespace ScrapingBeeScraper.Controllers { public class HomeController : Controller { private readonly ILogger _logger; public HomeController(ILogger logger) { _logger = logger; } public IActionResult Index() { string url = "https://en.wikipedia.org/wiki/List_of_programmers"; string response = CallUrl(url).Result; var linkList = ParseHtml(response); WriteToCsv(linkList); return View(); } [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)] public IActionResult Error() { return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier }); } private static async Task CallUrl(string fullUrl) { HttpClient client = new HttpClient(); ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls13; client.DefaultRequestHeaders.Accept.Clear(); var response = client.GetStringAsync(fullUrl); return await response; } private List ParseHtml(string html) { HtmlDocument htmlDoc = new HtmlDocument(); htmlDoc.LoadHtml(html); var programmerLinks = htmlDoc.DocumentNode.Descendants("li") .Where(node => !node.GetAttributeValue("class", "").Contains("tocsection")) .ToList(); List wikiLink = new List(); foreach (var link in programmerLinks) { if (link.FirstChild.Attributes.Count > 0) wikiLink.Add("https://en.wikipedia.org/" + link.FirstChild.Attributes[0].Value) ; } return wikiLink; } private void WriteToCsv(List links) { StringBuilder sb = new StringBuilder(); foreach (var link in links) { sb.AppendLine(link); } System.IO.File.WriteAllText("links.csv", sb.ToString()); } } }
第二部分:爬取动态JavaScript页面
在第一部分中,整个爬取工作相当简单,因为我们已经从服务器上收到了完整的HTML文档,我们只需要解析它并挑选我们想要的数据。
然而,JavaScript已经改变了这一格局,主要依靠JavaScript的网站–当然,特别是用Angular、React或Vue.js制作的网站–不能以我们之前讨论的方式进行刮擦。如果你使用同样的方法,你不会得到很多HTML,而大部分只是JavaScript代码。你需要实际执行这些JavaScript代码来获得你想要的数据。
动态JavaScript并不是唯一的问题。一些网站检测是否启用了JavaScript,或评估浏览器发送的用户代理。用户代理头是HTTP请求的一部分,它告诉网络服务器用于访问网页的浏览器类型(如Chrome、Firefox等)。如果你使用网络爬取器代码,它通常会发送一些默认的用户代理,许多网络服务器会根据用户代理返回不同的内容。一些网络服务器会使用JavaScript来检测一个请求是否来自人类用户。
你可以通过使用利用Headless Chrome的库来渲染页面,然后解析结果,从而克服这个问题。下面,我们将讨论两个可从NuGet免费获得的库,它们可与Headless Chrome结合使用来解析结果。PuppeteerSharp是我们使用的第一个解决方案,它对网页进行异步调用。另一个解决方案是Selenium WebDriver,它是一个用于网络应用程序自动化测试的通用平台,但也可以完全适用于爬取任务。
在Headless Chrome中使用PuppeteerSharp
与我们之前的例子类似,我们再次从控制器的Index()
方法开始,但这次需要的 “额外 “方法较少,因为Puppeteer已经涵盖了我们之前自己处理的相当多的领域。
public async Task Index() { string fullUrl = "https://en.wikipedia.org/wiki/List_of_programmers"; List programmerLinks = new List(); var options = new LaunchOptions() { Headless = true, ExecutablePath = "C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe" }; var browser = await Puppeteer.LaunchAsync(options, null, Product.Chrome); var page = await browser.NewPageAsync(); await page.GoToAsync(fullUrl); var links = @"Array.from(document.querySelectorAll('li:not([class^=""toc""]) a')).map(a => a.href);"; var urls = await page.EvaluateExpressionAsync<string[]>(links); foreach (string url in urls) { programmerLinks.Add(url); } WriteToCsv(programmerLinks); return View(); }
请不要忘记用NuGet和
PuppeteerSharp
来安装和导入Puppeteer。
我们没有自己发送HTTP请求,解析HTML,并提取数据,而是在这里真正只依靠Puppeteer和Chrome。第一个基本步骤是:
- 用新的
LaunchOptions
类定义几个选项(其中,我们的浏览器实例的路径‼️)。 - 用
Puppeteer.LaunchAsync(
)启动一个新的浏览器实例,用NewPageAsync(
)得到一个新的页面对象。 - 用
GoToAsync(
)加载https://en.wikipedia.org/wiki/List_of_programmers。
到目前为止,很好,现在是真正的魔术。
我们现在不需要自己处理整个元素的选择,而是简单地将下面的JavaScript片段传递给EvaluateExpressionAsync()
,它很好地为我们做了所有的重活。
Array.from(document.querySelectorAll('li:not([class^="toc"]) a:first-child')).map(a => a.href);
如果你熟悉JavaScript,你会注意到,我们运行querySelectorAll()
,用CSS选择器 li:not([class^="toc"])a
来获得第一个例子中的同一组元素,并最终用map()
将元素值切换到各自的链接属性(href
)。
现在我们只需要收集我们的链接,并用WriteToCsv()
把它们写到CSV文件。
将Selenium与Headless Chrome一起使用
如果Puppeteer不是一个选项,你也可以看看Selenium WebDriver。Selenium是一个非常流行的网络应用程序自动化测试平台,其工作原理与Puppeteer相当相似。除了Puppeteer之外,它还允许你在本地执行典型的用户操作(如鼠标点击)。
对于我们的例子,我们首先需要克服通常的NuGet仪式,安装Selenium.WebDriver
,以及以及Selenium.WebDriver.ChromeDriver
。后者是Chrome浏览器的必要助手,当然也有Firefox的助手。
现在,只要再有两个使用
声明,我们就可以开始了。
using OpenQA.Selenium; using OpenQA.Selenium.Chrome;
现在,你可以添加代码,打开一个页面并从结果中提取所有链接。下面的代码演示了如何提取链接并将它们添加到一个通用列表中。
public async Task Index() { string fullUrl = "https://en.wikipedia.org/wiki/List_of_programmers"; List programmerLinks = new List(); var options = new ChromeOptions() { BinaryLocation = "C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe" }; options.AddArguments(new List() { "headless", "disable-gpu" }); var browser = new ChromeDriver(options); browser.Navigate().GoToUrl(fullUrl); var links = browser.FindElementsByXPath("//li[not(contains(@class, 'tocsection'))]/a[1]"); foreach (var url in links) { programmerLinks.Add(url.GetAttribute("href")); } WriteToCsv(programmerLinks); return View(); }[文中代码源自Scrapingbee]
与我们的Puppeteer例子很相似,不是吗?
在这里,我们用ChromeOptions设置了我们的浏览器选项,实例化了一个ChromeDriver对象,用GoToUrl加载了页面,并最终提取了元素,再次将所有内容保存到我们的CSV文件中。
作为细心的读者,你肯定已经注意到我们悠闲地引入的技术转换。我们没有使用CSS选择器,而是使用了XPath表达式,但不要着急,Selenium同样支持CSS选择器。
var links = browser.FindElementsByCssSelector(@"li:not([class^=""toc""]) a:first-child");
请注意,Selenium不是异步的,所以如果你在一个页面上有大量的链接和动作,它将冻结你的程序,直到爬取完成。这是Puppeteer和Selenium的主要区别之一。
总 结
C# 以及一般的.NET,拥有所有必要的工具和库,可以让你实现自己的数据爬取器,尤其是像Puppeteer和Selenium这样的工具,很容易快速实现一个爬虫项目并获得你想要的数据。
我们只简单地讨论了一个方面,那就是避免被服务器阻挡或速率限制的不同技术。通常情况下,这才是网页爬取的真正障碍,而不是任何技术限制。