项目

C# 爬虫框架

跨平台的 C# 网络爬虫框架,旨在实现速度与灵活性

Abot 是一个开源的 C# 网页爬虫框架,专注于速度与灵活性。它处理了底层的复杂工作(多线程、HTTP 请求、调度、链接解析等),您只需注册事件来处理页面数据即可。您还可以插入自定义的核心接口实现,以完全控制爬取过程。Abot NuGet 包版本大于等于 2.0 的目标是.NET Standard 2.0,而 Abot NuGet 包版本小于 2.0 的目标是 .NET Framework 4.0,这使其与众多 .NET 框架/核心实现高度兼容。

它为何如此出色?

  • 开源(免费用于商业和个人用途)
  • 速度快,非常快!!
  • 易于定制(可插拔架构让你决定爬取什么以及如何爬取)
  • 大量单元测试(高代码覆盖率)
  • 非常轻量级(没有过度设计)
  • 无进程外依赖(无需数据库、无需安装服务等...)

快速入门

安装 Abot

PM> Install-Package Abot

使用 Abot

using System;
using System.Threading.Tasks;
using Abot2.Core;
using Abot2.Crawler;
using Abot2.Poco;
using Serilog;

namespace TestAbotUse
{
    class Program
    {
        static async Task Main(string[] args)
        {
            Log.Logger = new LoggerConfiguration()
                .MinimumLevel.Information()
                .WriteTo.Console()
                .CreateLogger();

            Log.Logger.Information("Demo starting up!");

            await DemoSimpleCrawler();
            await DemoSinglePageRequest();
        }

        private static async Task DemoSimpleCrawler()
        {
            var config = new CrawlConfiguration
            {
                MaxPagesToCrawl = 10, // 只爬取 10 个页面
                MinCrawlDelayPerDomainMilliSeconds = 3000 // 间隔多少毫秒后发送请求
            };
            var crawler = new PoliteWebCrawler(config);

            crawler.PageCrawlCompleted += PageCrawlCompleted; // 服务器可用事件处理...

            var crawlResult = await crawler.CrawlAsync(new Uri("http://!!!!!!!!YOURSITEHERE!!!!!!!!!.com"));
        }

        private static async Task DemoSinglePageRequest()
        {
            var pageRequester = new PageRequester(new CrawlConfiguration(), new WebContentExtractor());

            var crawledPage = await pageRequester.MakeRequestAsync(new Uri("http://google.com"));
            Log.Logger.Information("{result}", new
            {
                url = crawledPage.Uri,
                status = Convert.ToInt32(crawledPage.HttpResponseMessage.StatusCode)
            });
        }

        private static void PageCrawlCompleted(object sender, PageCrawlCompletedArgs e)
        {
            var httpStatus = e.CrawledPage.HttpResponseMessage.StatusCode;
            var rawPageText = e.CrawledPage.Content.Text;
        }
    }
}

Abot 配置

Abot 框架中的 Abot2.Poco.CrawlConfiguration 类提供了大量的配置选项。您可以通过查看代码注释来了解每个配置值对爬取过程的影响。

var crawlConfig = new CrawlConfiguration();
crawlConfig.CrawlTimeoutSeconds = 100;
crawlConfig.MaxConcurrentThreads = 10;
crawlConfig.MaxPagesToCrawl = 1000;
crawlConfig.UserAgentString = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.169 Safari/537.36";
crawlConfig.ConfigurationExtensions.Add("SomeCustomConfigValue1", "1111");
crawlConfig.ConfigurationExtensions.Add("SomeCustomConfigValue2", "2222");
etc...

Abot 事件

注册事件并创建处理方法

crawler.PageCrawlStarting += crawler_ProcessPageCrawlStarting;
crawler.PageCrawlCompleted += crawler_ProcessPageCrawlCompleted;
crawler.PageCrawlDisallowed += crawler_PageCrawlDisallowed;
crawler.PageLinksCrawlDisallowed += crawler_PageLinksCrawlDisallowed;
void crawler_ProcessPageCrawlStarting(object sender, PageCrawlStartingArgs e)
{
	PageToCrawl pageToCrawl = e.PageToCrawl;
	Console.WriteLine($"即将爬取链接 {pageToCrawl.Uri.AbsoluteUri} ,该链接是在页面上发现的。 {pageToCrawl.ParentUri.AbsoluteUri}");
}

void crawler_ProcessPageCrawlCompleted(object sender, PageCrawlCompletedArgs e)
{
	CrawledPage crawledPage = e.CrawledPage;
	if (crawledPage.HttpRequestException != null || crawledPage.HttpResponseMessage.StatusCode != HttpStatusCode.OK)
		Console.WriteLine($"页面爬取失败 {crawledPage.Uri.AbsoluteUri}");
	else
		Console.WriteLine($"页面爬取成功 {crawledPage.Uri.AbsoluteUri}");

	if (string.IsNullOrEmpty(crawledPage.Content.Text))
		Console.WriteLine($"页面没有内容 {crawledPage.Uri.AbsoluteUri}");

	var angleSharpHtmlDocument = crawledPage.AngleSharpHtmlDocument; //AngleSharp parser
}

void crawler_PageLinksCrawlDisallowed(object sender, PageLinksCrawlDisallowedArgs e)
{
	CrawledPage crawledPage = e.CrawledPage;
	Console.WriteLine($"由于某些原因,未爬取页面 {crawledPage.Uri.AbsoluteUri} 上的链接 {e.DisallowedReason}");
}

void crawler_PageCrawlDisallowed(object sender, PageCrawlDisallowedArgs e)
{
	PageToCrawl pageToCrawl = e.PageToCrawl;
	Console.WriteLine($"未爬取页面 {pageToCrawl.Uri.AbsoluteUri} 上的链接 {e.DisallowedReason}");
}

自定义对象与动态爬取包

向动态爬取包或页面包中添加任意数量的自定义对象。这些对象将在 CrawlContext.CrawlBag 对象、PageToCrawl.PageBag 对象或 CrawledPage.PageBag 对象中可用。

var crawler crawler = new PoliteWebCrawler();
crawler.CrawlBag.MyFoo1 = new Foo();
crawler.CrawlBag.MyFoo2 = new Foo();
crawler.PageCrawlStarting += crawler_ProcessPageCrawlStarting;
void crawler_ProcessPageCrawlStarting(object sender, PageCrawlStartingArgs e)
{
    // 从 CrawlContext 对象中获取您的 Foo 实例
    var foo1 = e.CrawlConext.CrawlBag.MyFoo1;
    var foo2 = e.CrawlConext.CrawlBag.MyFoo2;

    // 同时向 PageToCrawl 或 CrawledPage 添加一个动态值
    e.PageToCrawl.PageBag.Bar = new Bar();
}

取消

CancellationTokenSource cancellationTokenSource = new CancellationTokenSource();

var crawler = new PoliteWebCrawler();
var result = await crawler.CrawlAsync(new Uri("addurihere"), cancellationTokenSource);

自定义爬取行为

Abot 被设计为尽可能地可插拔。这使您能够轻松地改变其工作方式,以满足您的需求。

更改 Abot 常见功能行为的最简单方法是修改控制它们的配置值。请参阅 快速入门 页面上的示例,了解 Abot 可以通过不同方式配置的示例。

爬取决策回调/委托

有时,您可能不想创建一个类并经历扩展基类或直接实现接口的繁琐过程。对于所有追求简便的开发者来说,Abot 提供了一种简写方法,可以轻松添加您的自定义爬取决策逻辑。注意:ICrawlDecisionMaker 接口对应的方怯会首先被调用,如果它没有 “允许” 某个决策,这些回调将不会被调用。

var crawler = new PoliteWebCrawler();

crawler.ShouldCrawlPageDecisionMaker = (pageToCrawl, crawlContext) =>
{
	var decision = new CrawlDecision{ Allow = true };
	if(pageToCrawl.Uri.Authority == "google.com")
		return new CrawlDecision{ Allow = false, Reason = "不想爬取谷歌页面" };

	return decision;
};

crawler.ShouldDownloadPageContentDecisionMaker = (crawledPage, crawlContext) =>
{
	var decision = new CrawlDecision{ Allow = true };
	if (!crawledPage.Uri.AbsoluteUri.Contains(".com"))
		return new CrawlDecision { Allow = false, Reason = "仅下载以 .com 为顶级域名的网页原始内容" };

	return decision;
};

crawler.ShouldCrawlPageLinksDecisionMaker = (crawledPage, crawlContext) =>
{
	var decision = new CrawlDecision{ Allow = true };
	if (crawledPage.Content.Bytes.Length < 100)
		return new CrawlDecision { Allow = false, Reason = "仅爬取至少包含 100 字节的页面中的链接" };

	return decision;
};

自定义实现

PoliteWebCrawler 是爬取过程的指挥者,其职责是协调所有实用类以 “爬取” 一个网站。PoliteWebCrawler 通过其构造函数接受所有依赖项的替代实现。

var crawler = new PoliteWebCrawler(
    new CrawlConfiguration(),
	new YourCrawlDecisionMaker(),
	new YourThreadMgr(),
	new YourScheduler(),
	new YourPageRequester(),
	new YourHyperLinkParser(),
	new YourMemoryManager(),
    new YourDomainRateLimiter,
	new YourRobotsDotTextFinder())
    ;

如果为任何实现传递 null,则将使用默认实现。下面的例子将使用您自定义的 IPageRequesterIHyperLinkParser 实现,而其他所有实现都将使用默认的。

var crawler = new PoliteWebCrawler(
	null,
	null,
    null,
    null,
	new YourPageRequester(),
	new YourHyperLinkParser(),
	null,
    null,
	null)
    ;

以下是对 PoliteWebCrawler 依赖以完成实际工作的每个接口的解释。

ICrawlDecisionMaker

回调/委托快捷方式非常适合添加少量逻辑,但如果你正在进行更复杂的操作,你将需要传入自定义的 ICrawlDecisionMaker 实现。爬虫调用此实现来判断是否应爬取某个页面、是否应下载页面内容以及是否应爬取已爬取页面中的链接。

CrawlDecisionMaker.cs 是 Abot 默认使用的 ICrawlDecisionMaker。这个类负责处理一些常见的检查,例如确保 MaxPagesToCrawl 配置值不超过限制。大多数用户只需要创建一个继承自 CrawlDecisionMaker 的类,并添加他们的自定义逻辑即可。然而,你完全可以自由地创建一个实现 ICrawlDecisionMaker 接口的类,并将其传入 PoliteWebCrawler 的构造函数中。

/// <summary>
/// 定义哪些页面应当被爬取、是否应下载原始内容以及页面中的链接是否应当被进一步爬取的决策逻辑。
/// </summary>
public interface ICrawlDecisionMaker
{
	/// <summary>
	/// 决定是否应当爬取指定页面。
	/// </summary>
	CrawlDecision ShouldCrawlPage(PageToCrawl pageToCrawl, CrawlContext crawlContext);

	/// <summary>
	/// 决定是否应当爬取已爬页面中的链接。
	/// </summary>
	CrawlDecision ShouldCrawlPageLinks(CrawledPage crawledPage, CrawlContext crawlContext);

	/// <summary>
	/// 决定是否应当下载页面的原始内容。
	/// </summary>
	CrawlDecision ShouldDownloadPageContent(CrawledPage crawledPage, CrawlContext crawlContext);
}

IThreadManager

IThreadManager 接口处理多线程相关的细节,爬虫使用它来管理并发的 HTTP 请求。

TaskThreadManager.cs 是 Abot 默认采用的 IThreadManager 实现。

/// <summary>
/// 处理多线程实现的具体细节
/// </summary>
public interface IThreadManager : IDisposable
{
    /// <summary>
    /// 使用的最大线程数
    /// </summary>
    int MaxThreads { get; }

    /// <summary>
    /// 将在单独的线程上异步执行指定的操作
    /// </summary>
    /// <param name="action">要执行的操作</param>
    void DoWork(Action action);

    /// <summary>
    /// 是否存在正在运行的线程
    /// </summary>
    bool HasRunningThreads();

    /// <summary>
    /// 中止所有正在运行的线程
    /// </summary>
    void AbortAll();
}

IScheduler

IScheduler 接口负责管理需要爬取的页面。爬虫将找到的链接交给 IScheduler 实现,并从中获取需要爬取的页面。编写自定义实现的一个常见应用场景可能是跨多台机器分配爬取任务,这可能由分布式调度程序(DistributedScheduler)来管理。

Scheduler.cs 是爬虫使用的默认 IScheduler,默认构造时使用内存集合来确定哪些页面已被爬取以及哪些需要爬取。

/// <summary>
/// 处理需要爬取页面的优先级管理
/// </summary>
public interface IScheduler
{
    /// <summary>
    /// 当前已安排待爬取项目的计数
    /// </summary>
    int Count { get; }

    /// <summary>
    /// 安排参数页面进行爬取
    /// </summary>
    void Add(PageToCrawl page);

    /// <summary>
    /// 安排多个参数页面进行爬取
    /// </summary>
    void Add(IEnumerable<PageToCrawl> pages);

    /// <summary>
    /// 获取下一个要爬取的页面
    /// </summary>
    PageToCrawl GetNext();

    /// <summary>
    /// 清除所有当前安排的页面
    /// </summary>
    void Clear();
}

IPageRequester

IPageRequester 接口负责发起原始的 HTTP 请求。

PageRequester.cs 是爬虫使用的默认 IPageRequester

public interface IPageRequester : IDisposable
{
    /// <summary>
    /// 向URL发起HTTP网页请求并下载其内容
    /// </summary>
    Task<CrawledPage> MakeRequestAsync(Uri uri);

    /// <summary>
    /// 根据参数函数的决策向URL发起HTTP网页请求并下载其内容
    /// </summary>
    Task<CrawledPage> MakeRequestAsync(Uri uri, Func<CrawledPage, CrawlDecision> shouldDownloadContent);
}

IHyperLinkParser

IHyperLinkParser 接口负责从原始 HTML 中解析链接。

AngleSharpHyperLinkParser.cs 是爬虫使用的默认 IHyperLinkParser,它使用知名的 AngleSharp 来进行 HTML 解析。AngleSharp 类似于 jQueryCSS 风格选择器,但完全使用 C# 实现。

/// <summary>
/// 负责从原始HTML中解析超链接
/// </summary>
public interface IHyperLinkParser
{
    /// <summary>
    /// 解析HTML以提取超链接,并将每个链接转换为绝对URL
    /// </summary>
    IEnumerable<Uri> GetLinks(CrawledPage crawledPage);
}

IMemoryManager

IMemoryManager 处理内存监控与使用。此功能仍处于实验阶段,如果未来发现不可靠,可能会在后续版本中移除。

MemoryManager.cs 是爬虫使用的默认实现。

/// <summary>
/// 处理内存监控/使用情况
/// </summary>
public interface IMemoryManager : IMemoryMonitor, IDisposable
{
    /// <summary>
    /// 判断当前托管此实例的进程分配/使用的内存是否超过参数指定的MB大小
    /// </summary>
    bool IsCurrentUsageAbove(int sizeInMb);

    /// <summary>
    /// 判断是否有至少参数指定大小的MB内存空间可用
    /// </summary>
    bool IsSpaceAvailable(int sizeInMb);
}

IDomainRateLimiter

IDomainRateLimiter 接口负责处理针对每个域名的请求速率限制。它将处理确定需要经过多少时间后,才可再次对该域名发起 HTTP 请求。

DomainRateLimiter.cs 是爬虫使用的默认实现。

/// <summary>
/// 按域名基础进行速率限制或控制访问频率
/// </summary>
public interface IDomainRateLimiter
{
    /// <summary>
    /// 如果参数中的域名已被标记为需进行速率限制,它将根据配置的最小爬取延迟进行速率限制
    /// </summary>
    void RateLimit(Uri uri);

    /// <summary>
    /// 添加域名条目,以便根据参数中的最小爬取延迟对域名进行速率限制
    /// </summary>
    void AddDomain(Uri uri, long minCrawlDelayInMillisecs);
}

IRobotsDotTextFinder

IRobotsDotTextFinder 负责为每个域名检索 robots.txt 文件(如果 isRespectRobotsDotTextEnabled="true"),并构建实现 IRobotsDotText 接口的 robots.txt 抽象。

RobotsDotTextFinder.cs 是爬虫使用的默认实现。

/// <summary>
/// 查找并构建robots.txt文件的抽象表示
/// </summary>
public interface IRobotsDotTextFinder
{
    /// <summary>
    /// 使用rootUri查找robots.txt文件。
    ///
    IRobotsDotText Find(Uri rootUri);
}

这两个接口分别帮助控制爬虫在访问特定域名时的行为,确保遵守速率限制和 robots.txt 协议,以尊重网站的抓取规则并避免给服务器带来过大负担。

在本文档中