in

如何使用C#进行网页爬取?

如何使用C#进行网页爬取

C#作为后端编程语言相当流行,你可能会发现自己需要它来爬取一个或多个网页。在这篇文章中,我们将介绍如何使用C#来爬取一个网站。具体来说,我们将指导你如何发送HTTP请求,如何用C#解析收到的HTML文档,以及如何访问和提取我们想要的信息。

正如我们在其他文章中提到的,只要我们爬取服务器渲染或服务器组成的HTML,这就能很好地工作。当我们要处理单页应用程序,或任何其他严重依赖JavaScript的东西时,事情就变得复杂了。这就是我们将在本文第二部分讨论的内容,我们将深入了解PuppeteerSharpSelenium 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将在断点处停止,现在你可以查看应用程序的当前状态。

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表示,可以继续爬取该文档了。

  1. 我们用Descendants()获得所有<li>的孩子。
  2. 我们使用LINQ(Where())来过滤出使用上述HTML类的元素
  3. 我们对我们的链接进行迭代(foreach),并在我们的wikiLink字符串列表中把它们的(相对)URLs保存为绝对URLs
  4. 我们将字符串列表返回给我们的调用者

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。第一个基本步骤是:

  1. 用新的LaunchOptions类定义几个选项(其中,我们的浏览器实例的路径‼️)。
  2. Puppeteer.LaunchAsync()启动一个新的浏览器实例,用NewPageAsync()得到一个新的页面对象。
  3. 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这样的工具,很容易快速实现一个爬虫项目并获得你想要的数据。

我们只简单地讨论了一个方面,那就是避免被服务器阻挡或速率限制的不同技术。通常情况下,这才是网页爬取的真正障碍,而不是任何技术限制。

blank

Written by 爬取 大师

阿里P12级别选手,能够突破各种反爬, 全能的爬取大师,擅长百万级的数据抓取!没有不能爬,只有你不敢想,有爬取项目可以联系我邮箱 [email protected] (带需求和预算哈, 不然多半不回复)