在信息流处理场景中,把多个RSS源抓取下来的文章按类型自动分组,是构建个人阅读器或内容中台的常见需求。Golang凭借轻量协程与标准库,很适合写这类定时聚合与分类服务。下面直接看具体实现方式。

一、RSS数据结构与解析
大多数RSS 2.0源都是XML格式,根节点为<rss>,内部包含<channel>与多个<item>。每个item通常有<title>、<link>、<description>以及可选的<category>。Go的encoding/xml包可以把XML映射到结构体,我们只需定义对应的字段标签。
需要注意,并非所有源都提供category,有些科技类博客只用标题前缀区分。因此解析层应先尽量拿到标准分类,缺失时再走关键词补全逻辑。下面代码展示基础结构体与解析函数:
package main
import (
"encoding/xml"
"fmt"
"net/http"
)
type Rss struct {
Channel Channel `xml:"channel"`
}
type Channel struct {
Items []Item `xml:"item"`
}
type Item struct {
Title string `xml:"title"`
Link string `xml:"link"`
Description string `xml:"description"`
Categories []string `xml:"category"`
}
func fetchRss(url string) (*Rss, error) {
resp, err := http.Get(url)
if err != nil {
return nil, err
}
defer resp.Body.Close()
var rss Rss
dec := xml.NewDecoder(resp.Body)
err = dec.Decode(&rss)
if err != nil {
return nil, err
}
return &rss, nil
}
func main() {
data, err := fetchRss("https://ipipp.com/feed.xml")
if err != nil {
fmt.Println("fetch err:", err)
return
}
fmt.Println("item count:", len(data.Channel.Items))
}
上面代码中,Categories被定义为字符串切片,因为单个item可能带有多个<category>节点。使用xml.NewDecoder而不是直接Unmarshal,可以在大文件时控制内存,也方便以后加入字符集处理。
如果源里完全没有category,我们可在Item结构体加一个ExtraType字段,由后续分类函数填充。解析阶段保持纯粹,有助于后期维护。
二、根据类型归类显示的逻辑
归类显示的核心是把文章放到以类型为键的map中。类型来源优先取Categories,若为空则用标题匹配预设关键词,例如“Go”“Golang”归为后端,“Vue”“React”归为前端。这样即使源不规范也能稳定输出。
下面示例展示分类函数与打印逻辑。我们用map[string][]Item保存结果,遍历时顺带做去重,避免同链接重复入组:
package main
import (
"fmt"
"strings"
)
func classify(items []Item) map[string][]Item {
result := make(map[string][]Item)
seen := make(map[string]bool)
for _, it := range items {
key := "未分类"
if len(it.Categories) > 0 {
key = it.Categories[0]
} else {
title := strings.ToLower(it.Title)
if strings.Contains(title, "go") || strings.Contains(title, "golang") {
key = "后端"
} else if strings.Contains(title, "vue") || strings.Contains(title, "react") {
key = "前端"
}
}
if seen[it.Link] {
continue
}
seen[it.Link] = true
result[key] = append(result[key], it)
}
return result
}
func showGroup(result map[string][]Item) {
for typ, list := range result {
fmt.Printf("类型:%s 共%d篇n", typ, len(list))
for _, it := range list {
fmt.Printf(" - %sn", it.Title)
}
}
}
这种归类方式优点是实现简单、易调试;缺点是关键词写死在代码里。生产环境可把规则放到配置文件中,用map加载,便于运营人员调整。
当文章需要按类型显示到不同页面时,只需把result里的key作为路由参数,例如/type/后端,前端拉取对应切片渲染即可。Go的html/template也能直接循环map输出静态页。
三、定时聚合与接口输出
真实服务不能每次请求都去抓RSS,应该用time.Ticker每十分钟拉一次,分类后存到全局变量或Redis。下面代码演示一个最简定时任务骨架:
package main
import (
"fmt"
"time"
)
func scheduleFetch() {
ticker := time.NewTicker(10 * time.Minute)
for {
items, err := fetchAll([]string{"https://ipipp.com/feed.xml"})
if err == nil {
grouped = classify(items)
fmt.Println("refresh done")
}
<-ticker.C
}
}
var grouped map[string][]Item
func fetchAll(urls []string) ([]Item, error) {
var all []Item
for _, u := range urls {
r, err := fetchRss(u)
if err != nil {
continue
}
all = append(all, r.Channel.Items...)
}
return all, nil
}
接口层可用net/http提供JSON,前端按类型请求。由于Go的并发模型,多源抓取可用goroutine配合sync.WaitGroup加速,比串行更省时间。
整体看,用Golang做RSS文章分类并不需要复杂框架,标准库已覆盖解析、HTTP与文本处理。重点是把分类规则与解析解耦,并保证归类结果可直接驱动展示层。这样后续接微信推送或邮件日报都很容易。