背景:FGDigitalFetcher 是我实习期间开始做的一款农产品价格爬虫。当时我主要负责开发 service 层和 crawler 层;此后我独立补全了剩余的应用部分——自建 HTTP API、常驻调度器、SQLite 持久化、数据看板、SPA 抓取(BrowserFetcher)等,完成了完整的软件架构。
FGDigitalFetcher 是一个用 Go 编写的农产品价格抓取服务。它从惠农网、肉交所等站点抓取当日行情,支持按省份遍历全国、按中文品种名或关键词检索,内置 Nuxt2 解包器与反爬限速,数据经清洗校验后持久化到 SQLite。
- 多站点支持:已接入惠农网(农产品行情)、肉交所(肉类挂牌价),新增站点只需一个 Extractor 文件
- 全国省份遍历:每个站点维护独立的「省份 → 站点 ID」映射(两站编号体系不同),省份简称/官方全称统一归一,一键遍历全国各省
- 品种中文搜索:惠农网(63 个顶级品类 + 辣椒 29 个子品种)与肉交所(8 大肉类 + 约 100 个子分类)都支持中文名定位;肉交所还支持任意中文关键词搜索
- 语义字段提取:调用方只传字段名(如
price_list、avg_price),字段提取规则由站点 Extractor 内部维护,页面改版只改 Extractor 一处 - Nuxt2 IIFE 解包器:惠农网行情数据内嵌在压缩 JS 中,内置纯 Go 解包器还原为 JSON,无需浏览器或 Node
- 反爬防御:请求间隔带随机抖动([-21%, +100%)),时序接近真人,降低被封概率
- 数据清洗管线:去空白 → 内容校验 → 字段校验 → 类型转换 → 归一化,可插拔组合
- SQLite 持久化:WAL 模式、软删除、URL 去重(TTL),零外部依赖
- 主动拉取模式:自建 HTTP API + Scheduler 常驻调度,任务入队 → 定时拉取 → 抓取 → 入库全自动
| 组件 | 选型 | 说明 |
|---|---|---|
| 语言 | Go 1.26 | 标准库 + goquery + modernc SQLite |
| 抓取 | net/http |
HTTP 直取服务端渲染页面 |
| HTML 解析 | goquery |
jQuery 风格选择器,用于肉交所等 HTML 站点 |
| 数据提取 | encoding/json |
惠农网 __NUXT__ 数据、Extractor 输出 |
| 存储 | modernc.org/sqlite |
纯 Go 驱动,无 CGO,Windows/Linux 直接编译 |
# 构建
go build ./...
# 运行全部测试
go test ./...
# 静态检查
go vet ./...# 抓惠农网辣椒(广西)
go run ./cmd/fetcher --site cnhnb --province 广西
# 抓惠农网辣椒(全国各省)
go run ./cmd/fetcher --site cnhnb --all-provinces
# 抓全国各省的朝天椒
go run ./cmd/fetcher --site cnhnb --category 朝天椒 --all-provinces
# 中文模糊搜索品种
go run ./cmd/fetcher --search 椒
# 直接抓指定 URL 并写入 SQLite
go run ./cmd/fetcher --url https://www.cnhnb.com/hangqing/lajiao/ --save常驻服务内置数据看板,启动后浏览器打开 http://localhost:8080/ 即可用:
go run ./cmd/fetcherd # 或 go run ./cmd/fetcherd --config config.yaml惠农网行情模块(元/斤)
肉交所挂牌模块(元/吨)
看板支持:按站点(惠农网 / 肉交所)分模块汇总结果、品种搜索选择(惠农网 63 品类 + 肉交所 8 大类)、省份筛选、一键触发抓取(完成后自动刷新);不常驻轮询,数据用手动「刷新」按钮拉取。
flowchart TB
API["HTTP API(自建)"]
SVC["Service 层(Scheduler 常驻调度)"]
ORCH["Orchestrator 编排中心<br/>URL 校验 → 路由 → 限流(抖动)<br/>→ 抓取 → 提取 → Pipeline 清洗"]
CRL["Crawler 层:fetcher → extractor → pipeline<br/>Store 层:SQLite 持久化 + 去重"]
API <-->|"定时拉取任务 / 回传结果"| SVC
SVC --> ORCH
ORCH --> CRL
flowchart TB
subgraph s1["请求接入与校验"]
direction LR
A["API 传 {URL, 字段列表}"] --> B["validateURL 校验"] --> C["Match 路由到站点 Extractor"] --> D["RateLimit 等待(含抖动)"]
end
subgraph s2["抓取处理与入库"]
direction RL
E["Fetcher 抓取页面"] --> F["Extractor 按字段名提取"] --> G["Pipeline 清洗校验"] --> H["SQLite 入库 + 返回 ScrapeResult"]
end
D --> E
fetcher/
├── cmd/
│ ├── fetcher/
│ │ └── main.go # 一次性 CLI 入口(--site / --province / --category / --search 等)
│ └── fetcherd/
│ └── main.go # 常驻服务入口(HTTP API + Scheduler)
├── internal/
│ ├── crawler/
│ │ ├── fetcher/ # 抓取层:HTTPFetcher、BrowserFetcher(chromedp,Edge/Chrome)
│ │ ├── extractor/ # 抽取层
│ │ │ ├── interface.go # Extractor 接口 + ProvinceURLBuilder / CategoryURLBuilder
│ │ │ ├── registry.go # 注册表:Register / Match / All
│ │ │ ├── base.go # 公共工具方法
│ │ │ └── sites/ # ★ 站点实现(每站一个子包,自注册)
│ │ │ ├── cnhnb/ # 惠农网:农产品行情(元/斤)
│ │ │ │ ├── hangqing.go # Nuxt2 __NUXT__ 解包 + 行情提取
│ │ │ │ ├── provinces.go # 省份→地区ID 映射 + ProvinceURL
│ │ │ │ ├── categories.go # 品种中文名→品类ID 映射
│ │ │ │ └── testdata/ # 真实页面 fixture
│ │ │ └── roujiaosuo/ # 肉交所:肉类挂牌价(元/吨)
│ │ │ ├── mall.go # goquery 解析商品表格
│ │ │ ├── provinces.go # 省份→仓库ID 映射 + ProvinceURL
│ │ │ ├── categories.go # 分类→catid 映射 + kw 关键词搜索
│ │ │ └── testdata/ # 真实页面 fixture
│ │ ├── orchestrator/ # 编排中心:完整流水线 + 限流抖动
│ │ ├── pipeline/ # 清洗管线:5 个可插拔 Step
│ │ └── middleware/ # 站点级限流器(跨并发任务错峰)
│ ├── geo/ # 省份别名归一化(简称/官方全称 → 短名)
│ ├── model/ # 数据模型:Page、ScrapeResult、Task、FieldError 等
│ ├── store/ # 持久化层:SQLite 实现 + 接口抽象(结果 + 任务)
│ ├── task/ # 语义请求 → URL 列表展开(CLI 与 API 共用)
│ └── service/ # Service 层:Scheduler + HTTP API + 数据看板(dashboard.html)
├── config/
│ └── config.go # 配置结构 + Default + Load(YAML)
├── config.yaml # 配置文件示例
├── docs/
│ ├── architecture.md # 架构设计文档
│ └── dev-plan.md # 开发计划
└── LICENSE # Apache-2.0
| 站点 | 文件 | 数据 | 价格单位 | 状态 |
|---|---|---|---|---|
惠农网 cnhnb.com |
sites/cnhnb/ |
各产地当日行情 | 元/斤 | ✅ 可用 |
肉交所 roujiaosuo.com |
sites/roujiaosuo/ |
肉类商品挂牌价 | 元/吨 | ✅ 可用 |
两站的省份 ID 体系互不相同,因此每个站点维护独立的「省→ID」映射(各站子包内的 provinces.go),省份输入经 internal/geo 归一化(简称/官方全称均可,如「广西」「广西壮族自治区」都归一到「广西」):
| 站点 | 省份维度 | 映射文件 | 备注 |
|---|---|---|---|
| 惠农网 | URL 第 3 段「省份 ID」 | cnhnb/provinces.go |
编号有空档(缺 1/2/9/22),直辖市在 45081~45084,广西=20 |
| 肉交所 | cangku 参数 |
roujiaosuo/provinces.go |
0~34 连续(0=全部,广西=21,天津=3) |
品种维度:惠农网支持中文品种名(cnhnb/categories.go,63 个顶级品类 + 辣椒 29 个子品种;全国搜索走 slug 路由,按省筛选需 cateId、部分品类暂缺);肉交所支持中文分类名(roujiaosuo/categories.go,8 大肉类 + 约 100 个子分类),且未命中分类表的输入会自动降级为 kw 关键词搜索,因此肉交所也支持「猪肉」「五花肉」等任意中文词。
- 在
internal/crawler/extractor/sites/下新建一个站点子包(如sites/your_site/) - 实现
Extractor接口(Match/FetcherType/Extract/RateLimit/AuthProvider) init()中调用extractor.Register(&YourExtractor{})自注册- 在
cmd/fetcher的 import 里加一行空导入,触发自注册 - 在子包
testdata/放真实页面 fixture 写测试
详见 docs/architecture.md 第 7 节。
| 阶段 | 状态 |
|---|---|
| Fetcher 抓取层 | ✅ 完成 |
| Extractor 抽取层(惠农网 + 肉交所) | ✅ 完成 |
| Orchestrator 编排中心 | ✅ 完成(含集成测试) |
| Pipeline 管线层 | ✅ 完成 |
| Store 持久化层(SQLite) | ✅ 完成 |
| 全国省份遍历 + 品种中文搜索 | ✅ 完成 |
| Service 调度层 + HTTP API | ✅ 完成 |
| BrowserFetcher(SPA 页面,Edge/Chrome) | ✅ 完成 |
抓取遵循以下原则:
- 遵守目标站点的
robots.txt和服务条款 - 限速抓取(RateLimit + 随机抖动),不对抗式绕过反爬
- 仅采集公开数据,不采集个人信息

