返回博客

小红书 API 接入教程(Go):用 net/http 跑通第一个请求

Rnote API 团队 · · 1231 次阅读 · English
小红书数据 API教程 Go

这篇教程带你用 Go 接入小红书(RedNote / Xiaohongshu)数据接口。和 Python 与 Node.js 版一样,你无需自建爬虫——Rnote API 已经把采集基础设施备好,你只用标准库 net/http 发一个 GET 请求即可。

第一步:注册获取 API Key

前往注册页创建账户并验证邮箱,获取免费额度与 API Key。

第二步:发起第一个请求

仅用 Go 标准库,无需第三方依赖:

package main

import (
    "encoding/json"
    "fmt"
    "io"
    "net/http"
    "net/url"
)

func main() {
    endpoint := "https://rnote.dev/api/v2/crawler/search/notes"
    q := url.Values{}
    q.Set("keyword", "美食推荐")
    q.Set("page", "1")

    req, _ := http.NewRequest("GET", endpoint+"?"+q.Encode(), nil)
    req.Header.Set("X-API-Key", "YOUR_API_KEY")

    resp, err := http.DefaultClient.Do(req)
    if err != nil {
        panic(err)
    }
    defer resp.Body.Close()

    body, _ := io.ReadAll(resp.Body)
    var data map[string]any
    json.Unmarshal(body, &data)
    fmt.Println(data)
}

第三步:封装一个带重试的调用

func call(path string, params url.Values, retries int) ([]byte, error) {
    endpoint := "https://rnote.dev/api/v2/crawler/" + path + "?" + params.Encode()
    var lastErr error
    for i := 0; i < retries; i++ {
        req, _ := http.NewRequest("GET", endpoint, nil)
        req.Header.Set("X-API-Key", "YOUR_API_KEY")
        resp, err := http.DefaultClient.Do(req)
        if err != nil {
            lastErr = err
        } else {
            body, _ := io.ReadAll(resp.Body)
            resp.Body.Close()
            if resp.StatusCode == 200 {
                return body, nil
            }
            lastErr = fmt.Errorf("HTTP %d", resp.StatusCode)
        }
        time.Sleep(time.Duration(1<<i) * time.Second) // 指数退避
    }
    return nil, lastErr
}

仅成功请求(HTTP 2xx)扣费,失败重试不额外计费,所以可以放心加重试逻辑。

计费与额度

按请求计费、无月费、无最低消费;注册即送免费额度。充值方式与当前优惠见定价页。

用带缓冲的 channel 控制并发

Go 写采集最容易犯的错,是 for 里直接开 goroutine——几百个请求瞬间打出去,稳定收获一片 429。用一个带缓冲的 channel 当信号量即可:

package main

import (
    "fmt"
    "sync"
)

func fetchAll(ids []string) map[string][]byte {
    const concurrency = 4          // 与这把 Key 的限速匹配,别贪多

    sem := make(chan struct{}, concurrency)
    var wg sync.WaitGroup
    var mu sync.Mutex
    out := make(map[string][]byte, len(ids))

    for _, id := range ids {
        wg.Add(1)
        go func(noteID string) {
            defer wg.Done()
            sem <- struct{}{}        // 占一个位置,满了就在这里阻塞
            defer func() { <-sem }() // 用完让出

            body, err := getNote(noteID)   // 你自己的带重试封装
            if err != nil {
                fmt.Println("失败:", noteID, err)
                return
            }
            mu.Lock()
            out[noteID] = body
            mu.Unlock()
        }(id)
    }
    wg.Wait()
    return out
}

三个要点:concurrency 要和这把 Key 的限速匹配(不确定就从 4 起步);defer func() { <-sem }() 必须有,否则 panic 时位置永远不会释放,程序会卡死;写共享 map 必须加锁,Go 的 map 不是并发安全的,漏了会直接 fatal error: concurrent map writes。

常见报错与排查

状态码 含义 怎么办
401 Key 无效或没带 检查是不是 req.Header.Set("X-API-Key", key),不是 Authorization
402 余额不足 去充值。重试无效,请求没执行也没扣费
429 触发限速 降并发 + 退避重试,见上一节
422 参数不合法 响应体会指出字段名;注意爬虫接口是 GET + query,不是 JSON body

Go 这边还有一个语言相关的坑:resp.Body 必须关。漏掉 defer resp.Body.Close(),连接不会归还连接池,跑几千条之后就会开始出现莫名其妙的超时。

超时要设,而且别只设一个

client := &http.Client{Timeout: 30 * time.Second}

http.Client 的零值没有超时——一个卡住的连接可以让整个采集任务永远挂着。批量任务里这条几乎必踩。如果要更细的控制,用 context.WithTimeout 给单次请求单独设,这样重试逻辑能更快地放弃一个明显没救的请求。

下一步

免费注册,现在就用 Go 发出第一个请求。