.NET 爬虫库从 23 个包到 4 个包:我做了什么?为什么?

旧版 23 个 NuGet 包 → 新版 4 个。代码量减半,可维护性提升 10 倍。本文讲清楚为什么。


背景:一个真实存在的"过度拆分"案例

Aneiang.Pa 是我自己维护的一个 .NET 爬虫库。最初的设计哲学是 "每个平台一个 NuGet 包"------

  • 用户想抓微博?dotnet add package Aneiang.Pa.WeiBo
  • 想抓知乎?dotnet add package Aneiang.Pa.ZhiHu
  • 想抓 B站?dotnet add package Aneiang.Pa.Bilibili

听起来很模块化、很专业,对吧?

结果是灾难。

项目 数量
NuGet 包总数 23
csproj 文件数 23
重复的 Options/Service/接口模板代码 16 × 3 ≈ 48 个文件
每次发布要打的包 23 个
用户最常问的问题 "我应该引用哪个?"

用户视角:选包恐惧症

场景 1:新手第一次接触

"嗯......我想抓微博,那是不是引用 Aneiang.Pa.WeiBo 就行了?"

几分钟后......

"啊?还要装 Aneiang.Pa.Core?"

"等等,Aneiang.Pa.News 是干嘛的?是聚合包吗?"

"我直接装 Aneiang.Pa 是不是把所有平台都装进来了?"

场景 2:维护者发版

23 次 dotnet pack + 23 次 dotnet nuget push。一不小心漏一个,用户编译报错"找不到方法",因为 23 个包必须版本一致。

场景 3:第三方贡献

"我想加个 Bing 热搜,怎么贡献?"

"嗯......新建一个 Aneiang.Pa.Bing 项目,加到 sln,写 IBingNewScraperBingNewScraperBingScraperOptions,再到聚合包改 csproj 依赖、改 ServiceCollectionExtensions、改 ScraperSource 枚举......"

"好吧,我放弃。"


反思:拆分的成本

过度拆分的本质是把"未来可能的需求"当作"现在必须满足的需求"。

我以为 实际上
用户挑着装 99% 用户直接装 Aneiang.Pa 聚合包
平台之间体积差异大 每个平台 < 50KB,加起来才 800KB
拆分有利于扩展 拆分让扩展更难,因为第三方要走完整发包流程
单一职责原则 把"配置数据"拆成"独立项目"完全是过度工程

核心洞察:差异在配置,不在代码

16 个平台爬虫之间,差异不在"代码逻辑",而在"配置数据"。

每个爬虫长这样:

csharp 复制代码
public class WeiBoNewScraper : INewsScraper
{
    public string Source => "WeiBo";

    public async Task<Result> GetNewsAsync()
    {
        var client = httpFactory.CreateClient(...);
        var html = await client.GetStringAsync("https://s.weibo.com/top/summary");
        var nodes = doc.SelectNodes("//tr/td[@class='td-02']/a");
        foreach (var node in nodes) { ... }
        return result;
    }
}

只看不同的地方:

  • URL:https://s.weibo.com/top/summary
  • 选择器://tr/td[@class='td-02']/a
  • 字段映射:标题=InnerText,链接=href

这就是配置,不是代码。 写成 YAML 就够了:

yaml 复制代码
name: WeiBo
fetch:
  url: https://s.weibo.com/top/summary
parse:
  type: html
  container: "tr"
  fields:
    title: { selector: "td.td-02 a" }
    url:   { selector: "td.td-02 a", attr: href }

第一刀:16 个项目 → 16 个 YAML

复制代码
旧结构                          新结构
src/                            src/Aneiang.Pa/
├── BaiDu/    ← 删除            └── BuiltInRecipes/
├── WeiBo/    ← 删除                ├── baidu.yaml
├── ZhiHu/    ← 删除                ├── weibo.yaml
└── ...(16 个)                    └── ...(16 个 YAML)

YAML 嵌入到 Aneiang.Pa.dll 作为 EmbeddedResource,启动自动加载。

收益

  • 项目数:16 → 0
  • 代码量:~4000 行 C# → ~600 行 YAML
  • 新增平台流程:建项目+写类+注册 DI → 写一份 YAML 文件

第二刀:横切关注点抽离到管道

每个旧爬虫还重复着 16 份相同的 try/catch、HttpClient 创建、错误转换。

借鉴 ASP.NET Core 中间件模式,把横切关注点抽到管道层:

复制代码
请求 → 日志 → 指标 → 追踪 → 缓存 → 限流 → 熔断 → 重试 → 超时 → 抓取 → 解析 → 响应

业务代码(YAML)零侵入,可观测性自动获得。


第三刀:23 个包 → 4 个包

说明
Aneiang.Pa 核心 + 20 内置平台,单包搞定 90% 用户
Aneiang.Pa.AspNetCore Web API 集成(需要才装)
Aneiang.Pa.Client HTTP 客户端(需要才装)
Aneiang.Pa.Abstractions 接口与模型(扩展用)
bash 复制代码
dotnet add package Aneiang.Pa

完事。


重构成果对比

指标 旧版 新版
NuGet 包数量 23 4
内置平台 16 20
新增平台代码量 1 个 csproj + 3 个 .cs 文件 1 个 YAML 文件
入门代码行数 5+ 行 1 行
横切关注点 每个爬虫自己处理 8 个中间件自动处理
单元测试 0 15
构建时间 ~3 分钟 ~10 秒
发版操作 23 次 pack + push 4 次

教训

  1. 过早拆分是万恶之源。 16 个平台包在"有 100 个平台"时才有意义,在"只有 16 个"时只是增加复杂度。
  2. 声明式优于命令式。 YAML 比 C# 更适合表达"抓取规则"这种配置型需求。
  3. 管道模式是 .NET 生态的银弹。 ASP.NET Core 的中间件、HttpClient 的 DelegatingHandler,都是管道模式。爬虫也不例外。
  4. API 设计要面向"最常用场景"。 90% 的用户只需要 Pa.Source("X").GetAsync(),不要让他们先理解 DI、工厂模式、接口抽象。
  5. 配置即数据。 当你发现"每个类只有 URL 和选择器不同"时,就该把它变成配置文件。

开源地址

Gitee: https://gitee.com/aneiangsoft/Aneiang.Pa

GitHub: https://github.com/AneiangSoft/Aneiang.Pa

欢迎 Star、Issue、PR。新增平台只需提交一份 YAML,无需 C# 代码。