旧版 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,写IBingNewScraper、BingNewScraper、BingScraperOptions,再到聚合包改 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 次 |
教训
- 过早拆分是万恶之源。 16 个平台包在"有 100 个平台"时才有意义,在"只有 16 个"时只是增加复杂度。
- 声明式优于命令式。 YAML 比 C# 更适合表达"抓取规则"这种配置型需求。
- 管道模式是 .NET 生态的银弹。 ASP.NET Core 的中间件、HttpClient 的 DelegatingHandler,都是管道模式。爬虫也不例外。
- API 设计要面向"最常用场景"。 90% 的用户只需要
Pa.Source("X").GetAsync(),不要让他们先理解 DI、工厂模式、接口抽象。 - 配置即数据。 当你发现"每个类只有 URL 和选择器不同"时,就该把它变成配置文件。
开源地址
Gitee: https://gitee.com/aneiangsoft/Aneiang.Pa
GitHub: https://github.com/AneiangSoft/Aneiang.Pa
欢迎 Star、Issue、PR。新增平台只需提交一份 YAML,无需 C# 代码。