在Golang中开发RSS抓取工具,核心工作只有两件:其一是通过HTTP请求把远端Feed内容取回来,其二是把XML格式的内容反序列化成Go语言里的结构体,从而提取出新闻标题、链接与发布时间等字段。标准库已经提供了足够能力,我们不必依赖第三方重量级框架。

一、RSS与XML基础结构
RSS本质上是一种基于XML的站点摘要格式,最常见的版本是RSS 2.0。一个典型的Feed会包含一个频道(channel)信息,以及若干条目(item)。每个条目里通常有标题、链接、描述与发布时间。理解这个层级关系,是后续定义Go结构体的前提。
下面是一段简化过的RSS 2.0示例,我们可以看到最外层是rss节点,内部有channel,channel下有多个item。实际网络中返回的文档可能带有命名空间或CDATA,但主干结构基本不变。明白这一点后,我们就能用固定模式去解析绝大多数新闻源。
<rss version="2.0">
<channel>
<title>示例新闻</title>
<link>https://ipipp.com/news</link>
<item>
<title>第一条新闻</title>
<link>https://ipipp.com/news/1</link>
<pubDate>Mon, 02 Jun 2025 08:00:00 GMT</pubDate>
</item>
</channel>
</rss>
二、定义Go结构体映射XML
Go的encoding/xml包通过结构体字段上的xml标签来完成节点绑定。我们需要为channel和item分别建模,并把可能用到的字段都列出来。对于文本类内容,使用string类型即可;如果某些源把内容放在CDATA中,标准库也能自动处理。
在定义时,建议把时间字段先声明为string,后面再按RSS常用的RFC1123格式做解析,这样能避免时区或格式轻微差异导致直接解析失败。以下代码展示了最基础的结构体写法,你可以按自己需要的字段继续扩展,例如添加description或author。
package main
import (
"encoding/xml"
)
type Rss struct {
Channel Channel `xml:"channel"`
}
type Channel struct {
Title string `xml:"title"`
Link string `xml:"link"`
Items []Item `xml:"item"`
}
type Item struct {
Title string `xml:"title"`
Link string `xml:"link"`
PubDate string `xml:"pubDate"`
Desc string `xml:"description"`
}
三、使用net/http抓取Feed
抓取环节主要依赖net/http包。为了不让默认客户端使用长连接或超时失控,我们应当显式构造一个http.Client,并设置合理的超时时间。RSS源一般更新不频繁,抓取间隔可以放在分钟级,避免给对方服务器造成压力,也防止自己被限流。
下面的函数演示了如何发起GET请求并读取全部响应体。这里使用了context来控制超时,比单纯给Client设置Timeout更灵活。如果返回状态码不是200,我们直接返回错误,由调用方决定是否重试。
package main
import (
"context"
"fmt"
"io"
"net/http"
"time"
)
func fetchFeed(ctx context.Context, url string) ([]byte, error) {
client := &http.Client{}
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return nil, err
}
req.Header.Set("User-Agent", "golang-rss-reader/1.0")
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("unexpected status: %d", resp.StatusCode)
}
return io.ReadAll(resp.Body)
}
四、解析XML并提取新闻
拿到字节流之后,就可以调用xml.Unmarshal进行反序列化。这个函数会把XML内容按照我们之前定义的结构体标签填进去。如果某个节点缺失,对应字段会是零值,不会报错,因此解析过程相对稳健。
随后我们遍历Items,把需要的新闻信息打印或落库。下面的例子把标题、链接和发布时间输出到控制台,并演示了如何用time.Parse解析RSS标准时间。若解析时间失败,仅打印原始字符串,不影响其他条目处理。
package main
import (
"encoding/xml"
"fmt"
"time"
)
func parseFeed(data []byte) (*Rss, error) {
var rss Rss
if err := xml.Unmarshal(data, &rss); err != nil {
return nil, err
}
return &rss, nil
}
func printItems(rss *Rss) {
for _, item := range rss.Channel.Items {
t, err := time.Parse(time.RFC1123, item.PubDate)
if err != nil {
fmt.Printf("标题: %sn链接: %sn时间: %snn", item.Title, item.Link, item.PubDate)
continue
}
fmt.Printf("标题: %sn链接: %sn时间: %snn", item.Title, item.Link, t.Local().Format("2006-01-02 15:04"))
}
}
五、简单并发与调度
当我们需要同时监控多个RSS源时,串行抓取会很慢。Go的goroutine让并发变得简单:为每个源启动一个任务,用sync.WaitGroup等待全部完成,再通过channel收集结果或错误。注意要限制并发上限,例如使用带缓冲的信号量,避免瞬间打出过多请求。
下面示例用长度为3的channel当作信号量,最多同时抓3个源。每个goroutine内部复用了前面写的fetch与parse逻辑。这种写法在十几个源的规模下完全够用,若要继续扩大,可以引入定时器和去重表。
package main
import (
"context"
"sync"
"time"
)
func crawlAll(urls []string) {
var wg sync.WaitGroup
sem := make(chan struct{}, 3)
for _, u := range urls {
wg.Add(1)
sem <- struct{}{}
go func(url string) {
defer wg.Done()
defer func() { <-sem }()
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
data, err := fetchFeed(ctx, url)
if err != nil {
return
}
rss, err := parseFeed(data)
if err != nil {
return
}
printItems(rss)
}(u)
}
wg.Wait()
}
六、错误处理与重试策略
网络请求不稳定是常态,RSS源也可能临时下线。建议在fetch层之外包装一个重试函数,例如最多重试三次,采用指数退避。同时对于解析错误,可以记录源地址和错误详情,方便后续人工排查,而不是让整个程序崩溃。
下面的retry函数接受要执行的抓取解析动作,在错误时等待越来越久再试。这种轻量做法比引入复杂调度库更贴合小型工具定位,也更容易在后期改造成持久化任务。
package main
import (
"context"
"fmt"
"time"
)
func retryFetch(ctx context.Context, url string, times int) ([]byte, error) {
var lastErr error
for i := 0; i < times; i++ {
data, err := fetchFeed(ctx, url)
if err == nil {
return data, nil
}
lastErr = err
wait := time.Duration(1<<uint(i)) * time.Second
fmt.Printf("第%d次失败: %v, %s后重试n", i+1, err, wait)
time.Sleep(wait)
}
return nil, lastErr
}
七、小结与扩展思路
通过上述几个模块,我们已经拥有了一个能抓取多个RSS源、解析XML并输出新闻条目的Golang工具。它完全基于标准库,编译后单个二进制即可部署,非常适合放在树莓派或轻量服务器上长期运行。
如果你希望进一步增强,可以考虑把结果写入SQLite或Redis,加上增量更新判断;或者针对Atom格式再建一套结构体,用同一个调度器兼容两种协议。整体架构不用大改,只需要把解析层抽象成接口,就能平滑扩展。