拓冰建站拓冰建站
首页 / 资讯中心 / 正文

对标Scrapy的.NET爬虫框架DotnetSpider,开箱即用还跨平台

很多从 Python 转过来的朋友开口就问“.NET 里有像 Scrapy 那样开箱即用的爬虫库吗”。早期你翻遍 NuGet答案基本都是“没有自己拼”。但这两年情况真的变了.NET 生态里出现了能对标 Scrapy 的爬虫框架而且天然享受 .NET 的跨平台红利一套代码跑 Windows、Linux、macOS 都没问题。如果你正打算用 .NET 做数据采集又不想从零手写调度、去重、下载、解析那一整套流水线这篇文章就是冲你来的。我要推荐的主角是 DotnetSpider一个真正开箱即用的 .NET 多平台爬虫库。文章后面会把它拆开来讲附上可以直接跑通的最小示例再补两套轻量级备选方案AngleSharp 组合、HttpClient HtmlAgilityPack 组合最后把我在多平台部署和实际抓取中踩过的坑整理成一份速查表。读完你至少能判断两件事自己的场景适不适合上框架以及上手后哪些坑可以提前绕开。1. 什么算“开箱即用”.NET 为什么需要它1.1 一个能用的爬虫最少要包含哪些部件很多人觉得“爬虫就是发个 HTTP 请求然后把 HTML 解析一下”这个理解太乐观了。真实项目里一个能稳定跑的爬虫至少需要下面这几块东西协同工作并发下载器不能一个页面一个页面地串行请求否则抓个几千页会等到天荒地老URL 调度器要决定接下来抓哪个链接还要记录哪些已经抓过避免重复抓去重机制尤其是抓列表页、翻页链接、详情页链接时不去重会浪费大量请求还容易被网站封页面解析CSS 选择器、XPath、正则总要有一个好用的抽象不能每次写一堆字符串切割数据持久化抓下来的数据要落库落到 MySQL、MongoDB、CSV、JSON 都行最好切换时不用改业务代码异常处理与重试网络抖动、目标站超时、偶发 5xx都需要自动重试而不是直接崩掉日志与监控大规模采集时你总得知道当前跑到哪、成功多少、失败多少如果你用最朴素的 HttpClient 手写以上每一项都要自己造轮子。单独看每一项都不难但它们组合在一起工程量就很可观了而且每做一个新项目都要重写一遍。这也是“框架”存在的意义把通用能力沉淀成约定你只管写业务规则。1.2 Python 有 Scrapy.NET 为什么不能凑合Python 生态里 Scrapy 太成熟了以至于大家都觉得“爬虫库 Python 专属”。但现实是很多团队的核心业务是 C# / .NET 写的数据管道、消息队列、业务系统都在 .NET 这一侧。为了抓数据再单独起一套 Python 服务维护成本、部署成本、跨语言沟通成本都会翻倍。.NET 以前的尴尬在于标准库里没有好用的 HTML 解析器第三方库也比较散抓个网页要拼装很多组件。.NET Core 出现之后局面有了两个根本性改变。第一运行时跨平台了写好的爬虫可以发布到 Linux 服务器、容器里不用再被 Windows 绑定第二社区开始出现组织化程度更高的爬虫框架。DotnetSpider 就是其中诚意比较足的一个它把 Scrapy 那套“下载器 调度器 中间件 Item Pipeline”的思路搬到了 C# 里同时保留了 C# 的强类型和 async 特性。说白了.NET 不是没有爬虫库而是过去没有一个足够“开箱即用”的。现在有了而且它跟 .NET 主栈天然兼容你可以在一个项目里同时写完采集、清洗、入库、分析的整条链路这对团队来说是非常舒服的。2. DotnetSpider 怎么做到“开箱即用”2.1 核心能力拆解调度、去重、下载、数据流DotnetSpider 的架构思路用一句话概括把爬虫的通用流程拆成一个个可插拔的环节你只管往流水线上放自己的逻辑。Spider 抽象类你通过继承 Spider 来定义一次爬取任务。在构造方法里指定起始请求、要抽取的实体类型、数据处理方式框架负责调度和执行DataFlow 数据流这是它的灵魂。爬虫从发出请求到拿到数据会经过一长串数据流处理节点比如 URL 规范化、去重、内容解析、数据清洗、持久化。你可以像写中间件一样把这些节点挂到任务上也可以自定义自己的处理节点插进链里去重机制内置基于内存的去重队列抓小站点开箱即用如果抓取量大可以换成 Redis 做分布式去重多平台支持库本身面向 .NET Standard所以只要你的目标环境能跑 .NET它就能跑。Windows 服务器、Linux 容器、macOS 开发机都不需要额外适配我特别喜欢它的一点是你不用关心“下一个请求是怎么被选出来”的也不用操心“一个页面解析出来的链接什么时候再压入队列”。这些框架都替你处理好了你写的代码基本上就是“告诉我抓哪、怎么抓”。2.2 比手写方案舒服的几个点第一个舒服点是强类型实体抽取。手写解析的时候你拿到的是一个字符串然后做正则、Replace、Trim非常啰嗦。DotnetSpider 允许你定义一个 C# 类来表示目标数据字段上标注对应的 XPath 或 CSS 选择器框架解析完 HTML 后会自动把数据映射到实体对象上。这不仅仅是省了几行代码更关键的是让字段语义变得极其清晰后期维护一看实体类就知道抓了哪些数据。第二个舒服点是内置持久化方案。像 Scrapy 需要自己写 Item Pipeline 一样DotnetSpider 也支持把实体直接输出到多种存储。你可以把一次采集的数据落地为 Json 文件做归档也可以直接写进 MySQL 表甚至集成消息队列做进一步分发。这种“切换存储不用改爬虫逻辑”的设计在实际项目中太实用了。第三个舒服点是并发控制。无脑开 100 个线程去抓目标站几十毫秒就给你封 IP。框架里可以配置请求延迟、并发量、重试次数方便你对不同站点做不同的压力策略。这些都是手写方案容易被忽略但实际非常关键的细节。3. 实战先用 5 分钟跑通一个商品列表爬虫3.1 初始化项目两步装好依赖打开终端先建立一个控制台项目然后通过 NuGet 引入 DotnetSpider。具体的包版本以你在 NuGet 上拉到的为准我这边用的是 4.x 系列。dotnet new console -n SimpleCrawler cd SimpleCrawler dotnet add package DotnetSpider装完包的 csproj 看起来会多一行 PackageReference。如果你是在国内网络环境下拉取 NuGet 包偶尔超时可以给 nuget.org 源配置一下超时时间或者使用国内镜像源。这个跟爬虫本身无关但属于新人经常卡住的环境问题。3.2 定义实体类声明抽取规则假设我要抓的是一个演示站点的商品列表页页面上每个商品包含名称、价格、描述三个字段对应的 HTML 结构大概是div classcard h2 classtitlea href/p/1001无线机械键盘/a/h2 p classprice299.00/p p classdesc支持蓝牙三模连接兼容多平台/p /div我定义一个 C# 实体类来承接数据。这里用到了特性标注选择器具体特性名可能随版本略有变化但思路是一致的用 XP 标签声明这个字段对应的 XPath 路径用 Css 声明 CSS 选择器路径。using DotnetSpider.Selector; public class Product { [PropertyDefine(Expression //h2[classtitle]/a, Type SelectorType.XPath)] public string Name { get; set; } [PropertyDefine(Expression //p[classprice], Type SelectorType.XPath)] public decimal Price { get; set; } [PropertyDefine(Expression //p[classdesc], Type SelectorType.XPath)] public string Description { get; set; } }这里有个细节值得注意如果目标站的 HTML 结构经常变XPath 写得太绝对比如div[3]/div[1]/a会很脆优先用可靠的 class 属性或 id 锚定节点。等采集稳定了再考虑提取下一页、分页规律等扩展逻辑。3.3 写一个最小的 Spider 入口接下来继承 Spider在构造函数里注册起始请求和实体类型然后在底层方法里处理解析后的数据。框架会自动完成 HTML 下载和实体映射我只需要关注业务结果。using DotnetSpider; public class ProductSpider : Spider { public ProductSpider() : base(new SpiderOptions()) { AddRequests(new Request(https://example.com/list?page1)); AddEntityTypeProduct(); } protected override async Task OnExtracted(DataFlowContext context) { var products context.GetEntitiesProduct(); if (products null || !products.Any()) { _logger.LogWarning(页面解析结果为 0检查选择器是否匹配); return; } await File.WriteAllTextAsync( products.json, System.Text.Json.JsonSerializer.Serialize(products), Encoding.UTF8 ); _logger.LogInformation(本页提取到 {Count} 条商品, products.Count); } }看起来是不是没有“手写请求再手动解析再手工存文件”那种臃肿感你在 OnExtracted 里拿到的是强类型实体列表而不是一个充满字符串的文档对象。这个抽象在字段多、页面多的时候优势特别明显。3.4 加入翻页与频率控制真实的商品列表往往不止一页。一个常见的做法是在抽取完当前页后从页面里找到“下一页”的链接然后把它提交给调度器。这个逻辑可以放在自定义 DataFlow 里也可以简单地在 OnExtracted 中解析下一页 URL 并 AddRequests。var nextPage context.Selectable.XPath(//a[relnext]/href).GetValue(); if (!string.IsNullOrEmpty(nextPage)) { AddRequests(new Request(new Uri(new Uri(https://example.com), nextPage).ToString())); }同时我建议在 SpiderOptions 中设置请求延迟和重试次数避免短时间高频率请求被目标站限制。比如每次请求间隔 200~500 毫秒对大多数小站点来说已经是足够礼貌的节奏了。3.5 发布到 Linux 服务器或容器开发机上跑通只是第一步爬虫迟早要挂到服务器上持续采。.NET 的发布命令可以把程序打成单目录或无依赖包方便部署dotnet publish -c Release -r linux-x64 -o ./publish如果要用容器运行一个最小的 Dockerfile 长这样FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base WORKDIR /app COPY publish/ . ENTRYPOINT [dotnet, SimpleCrawler.dll]构建镜像时记得把源码和发布产物分开运行容器时不带 SDK镜像体积会小很多。这里的坑往往不是程序本身而是容器内的时区、地区语言设置。比如有些网站在输出时间时依赖服务器时区你得在容器里设置 TZ 环境变量否则日志时间和数据里的时间会差出 8 小时。4. 不想上框架两套轻量组合同样能打4.1 AngleSharp只想要解析能力时选它有些场景其实就是“定期抓一个接口或一个静态页提取几个字段”这么小的任务引入完整爬虫框架反而显得重。这时候我非常推荐 AngleSharp它是 .NET 生态里最接近浏览器 DOM 的解析库使用体验接近 jQuery 选择器。它的用法非常直接。先安装包dotnet add package AngleSharp然后直接用 BrowsingContext 打开一个地址使用 QuerySelectorAll 抽取节点using AngleSharp; using AngleSharp.Html.Parser; var context BrowsingContext.New(Configuration.Default); var document await context.OpenAsync(https://example.com/list); var titles document.QuerySelectorAll(h2.title a) .Select(x x.TextContent.Trim()) .ToList(); foreach (var title in titles) { Console.WriteLine(title); }是不是很像在浏览器开发者工具里写选择器AngleSharp 的优点是解析能力强、API 直觉化、不依赖浏览器进程适合快速验证和中小规模采集。它的局限是不会主动帮你做 URL 调度和持久化请求和存储还是要自己写。所以它更像是“一把趁手的解析刀”而不是完整爬虫框架。4.2 HttpClient HtmlAgilityPack最朴素但最可控另一种非常常见的组合是 HttpClient 负责下载HtmlAgilityPack 负责解析。HtmlAgilityPack 老牌、稳定、资料多网上十个 .NET 爬虫教程有八个在用。它的 XPath 能力很扎实适合目标页面结构比较固定、你愿意手写节点路径的场景。using System.Net.Http; using HtmlAgilityPack; var http new HttpClient(); http.DefaultRequestHeaders.UserAgent.ParseAdd(Mozilla/5.0 ...); var html await http.GetStringAsync(https://example.com/list); var doc new HtmlDocument(); doc.LoadHtml(html); var nodes doc.DocumentNode.SelectNodes(//h2[classtitle]/a); if (nodes ! null) { foreach (var node in nodes) { Console.WriteLine(node.InnerText.Trim()); } }这个组合最大的优点是“可控”。每一层都是自己写的代码出了问题你完全知道是哪一行导致的。缺点也很明显所有调度、去重、解析失败处理都得自己搭。我自己的经验是抓一次性的小报表、小接口这套组合最快要长期维护一个几十个页面的大型采集任务还是上框架省心。4.3 到底怎么选我的场景对照表选型适合场景优点缺点DotnetSpider长期、大型、多页面、需要去重和分布式开箱即用、调度去重完备、支持多种持久化学习曲线略陡框架抽象较重AngleSharp中小规模、页面解析复杂、需要流畅 DOM 查询API 直观、解析能力强、启动轻量不提供调度和持久化HttpClient HtmlAgilityPack快速脚本、单页抓取、高定制需求最灵活、无额外依赖、容易定位问题所有组件都要自己写PuppeteerSharp目标站是动态渲染、必须执行 JS 才能拿数据无头浏览器所见即所得资源占用高、速度慢、并发难控制我见过很多团队一上来就上无头浏览器结果因为不控制并发CPU 和内存直接被打满。你要记住能用普通 HTTP 抓的页面不要用无头浏览器能用框架解决的调度问题不要自己维护一个类库去解决能用正则/选择器定位的节点不要反复解析整段 HTML。选型不迷信要看场景。5. 我踩过的坑和排查记录5.1 常见问题速查表这部分内容是我在实际跑采集任务时反复遇到并排查过的直接整理成表现象可能原因解决办法目标站返回 403请求头没有设置 User-Agent 或访问频率过快添加浏览器 UA、Cookie拉长请求间隔解析结果全是空目标站是 JS 动态渲染HTML 里根本没有数据节点检查响应 HTML 是否包含目标字段必要时切换到无头浏览器中文乱码目标页面不是 UTF-8 编码HttpClient 按 UTF-8 解码了根据响应头 charset 指定编码GBK 页面用 GBK 解码请求超时/连接被重置网络不稳定或目标站对高频访问做限制增加超时时间、加入重试退避策略、降低并发拉到网页但找不到想要的 class网站的 CSS 类名是动态生成的可能每次加载不同改用更稳定的结构锚点比如 id 或 data 属性程序能跑但数据重复没有启用去重配置同一个 URL 被反复请求启用框架的去重队列或自己维护一个 URL 集合5.2 几个非常值得养成的习惯第一采集频率一定要克制。不管你的目标是电商还是资讯站疯狂请求只会导致 IP 被封、网站性能受损。设置合理的请求间隔像每天跑增量任务而不是 24 小时全速抓取才是长期可持续的姿势。第二解析 HTML 前先看一眼原始响应。很多新人拿不到数据就直接怀疑代码写错了其实打开目标页面按 F12 看网络响应十次里有六次是“数据根本不在静态 HTML 里”。第三失败重试要做幂等。比如同一批详情页抓了两次入库前要按业务主键去重避免产生脏数据。我自己的习惯是每次解析失败时把原始 HTML 落一份到本地 debug 目录文件名用时间戳标记。这招看起来笨但排查问题时特别有效因为它保留了“现场”。等你真正面对一个解析偶发失败的诡异问题时就知道现场有多重要了。5.3 我在多平台部署时踩过的几个具体坑第一次把爬虫发布到 Linux 容器时我遇到过一个很有意思的问题程序里打印的路径在本地 Windows 能用到 Linux 上就报“找不到文件”。后来才意识到文件路径分隔符和相对路径基准在跨平台时都不同。从那以后我写任何涉及路径的代码都会用Path.Combine绝不硬编码\或/并且用Environment.CurrentDirectory明确基准目录。另一个坑是字符编码。Windows 默认的文本编码和 Linux 容器里的 UTF-8 存在差异如果抓取的页面本身不是 UTF-8你需要在下载后显式指定编码去解码而不是老是依赖默认配置。很多“换了个环境就乱码”的问题根源都在这里。再就是日志时区。容器里默认 UTC 时间你人却在东八区造成日志时间看起来永远差 8 小时。这个问题不影响采集逻辑但排查问题时非常容易误判“任务是不是卡了”。我现在都会在 Dockerfile 里设置ENV TZAsia/Shanghai把时区问题提前解决掉。我个人的体会是爬虫技术栈里没有银弹。DotnetSpider 这类框架帮你解决了调度、去重和持久化这些脏活但页面结构变化、站点反爬策略、数据质量校验这些永远需要你自己持续关注。框架再强也只是把你的精力从“造轮子”挪到了“维护业务规则”上这恰恰是它最有价值的地方。最后再分享一个小技巧如果你正在开发的爬虫会运行在多个服务器上建议把采集任务的配置起始 URL、选择器、请求间隔外置到配置文件或者在管理后台动态下发而不是硬编码在代码里。目标站的页面结构变了不用重新编译发布程序改一下配置就能恢复采集。这套思路搭配 DotnetSpider 的数据流扩展点可以做出一个真正可维护的采集服务。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门