Blog/开发者工具/

短视频趋势 API:抖音 + 快手热榜聚合

用一个 SandBase 密钥把抖音和快手热榜聚合成一条归一化趋势流——读两个榜、映射到统一结构,无需登录、无需 SDK。

深色电影质感画面:抖音与快手热榜合并为一条归一化短视频趋势流,汇入 Agent 内核

在国内追短视频趋势,你不会只盯一个平台。抖音和快手各有热榜,但两者的负载长得完全不一样——抖音把排名视频包在 code/data.objs 信封里,快手则把榜单以纯 list 返回。手工去调和这些差异,你的时间就花在解析怪癖上,而不是花在趋势信号本身。

这篇教程搭一个小聚合器:一个函数通过 SandBase 公开数据 API 读取两个热榜,把它们归一化成一个统一结构,再把一条排好序的趋势流交给你的 Agent。一个 SandBase API 密钥,不需要平台登录,不需要 SDK。每个端点的 API 参考是其参数和响应信封(id/status/model/outputs[0].data)的权威来源;下面展示的业务载荷字段名只是一个示例结构、并非保证的 schema,请以一份真实响应为准核对。

要了解每个平台背后完整的端点面,见 抖音公开数据 API 和 快手公开数据 API 两个总览。准备动手?获取 SandBase API 密钥。

先说结论

  • 两个热榜端点——douyin/billboard/hot-total-high-like-list 和 kuaishou/web/hot-list-v1——用同一个密钥读取。
  • 两者都用相同的 { id, status, model, outputs } 信封,所以一次 status 判断、一个 HTTP 辅助函数就能覆盖两个。
  • 负载不同:抖音把排名视频嵌在 data.data.objs 下;快手把 list 直接放在 data 下。把两者归一化到同一行结构。
  • 仅公开、只读数据。你这边无需发帖、无需登录;用 SandBase API 密钥鉴权即可。

方案

三步,每步一个普通 REST 调用加一个小映射器:

  1. 读抖音榜。 调用 douyin/billboard/hot-total-high-like-list,从 data.data.objs 取排名视频。
  2. 读快手榜。 调用 kuaishou/web/hot-list-v1,读 data 下直接返回的 list。
  3. 归一化并合并。 把每个平台的字段映射到一个行结构,标注来源,返回一条统一趋势流。

SandBase 抖音 API 页面,展示 billboard 端点及其路径 SandBase 上的抖音 API 页面——billboard 簇里包含 hot-total-high-like-list 端点。

第 1 步 —— 读抖音热榜

每个 SandBase Model API 调用都是向 /v1/api/<vendor>/<path> 发一个 POST,把密钥放在 Authorization 头里。读 outputs 前先按 status 分支判断,而且——因为抖音包了一层——读 data.objs 前先检查上游 code。

import os
import requests

SANDBASE = "https://api.sandbase.ai/v1/api"
HEADERS = {
    "Authorization": f"Bearer {os.environ['SANDBASE_API_KEY']}",
    "Content-Type": "application/json",
}


def call(path: str, body: dict) -> dict:
    resp = requests.post(f"{SANDBASE}/{path}", headers=HEADERS, json=body, timeout=90)
    resp.raise_for_status()
    out = resp.json()
    if out.get("status") != "completed":
        raise RuntimeError(out.get("error", {}).get("message", f"{path} 未完成"))
    return out["outputs"][0]["data"]


def douyin_hot() -> list[dict]:
    data = call("douyin/billboard/hot-total-high-like-list", {})
    if data.get("code") != 0:
        raise RuntimeError(data.get("message", "抖音上游错误"))
    # 参考只保证信封,业务字段随端点而定、以真实响应为准。
    objs = data.get("data", {}).get("objs", [])
    return [
        {
            "source": "douyin",
            "rank": i + 1,
            "title": o.get("item_title"),
            "author": o.get("nick_name"),
            "metric": o.get("like_cnt"),
            "url": o.get("item_url"),
        }
        for i, o in enumerate(objs)
    ]

下面是一个示例响应结构——字段名并非保证的 schema、以真实响应为准,请把字段和取值当作示例,并对照真实响应核对,因为负载会随时间变化:

{
  "id": "5494b9ed-3c71-4b9b-a1ee-b248ffaa6fae",
  "status": "completed",
  "model": "douyin/billboard/hot-total-high-like-list",
  "outputs": [
    {
      "data": {
        "code": 0,
        "message": "…",
        "data": {
          "objs": [
            { "item_id": "…", "item_title": "…", "nick_name": "…", "like_cnt": 0, "item_url": "…" }
          ]
        }
      }
    }
  ]
}

某个抖音 billboard 端点的 SandBase API 参考,展示带厂商前缀的 URL 和响应结构 端点 API 参考是每个参数名和响应路径的事实来源。

第 2 步 —— 读快手热榜

快手把榜单以 list 直接放在 data 下——没有内层 code 包裹——所以映射器略简单。复用同一个 call 辅助函数:

def kuaishou_hot() -> list[dict]:
    items = call("kuaishou/web/hot-list-v1", {})
    # 参考只保证信封,业务字段随端点而定、以真实响应为准。
    return [
        {
            "source": "kuaishou",
            "rank": it.get("rank"),
            "title": it.get("name"),
            "author": None,
            "metric": it.get("viewCount"),
            "url": None,
        }
        for it in items
    ]

这里 data 下的负载本身就是排名话题的 list——每个条目带一个 name 和一个 rank。因为 call 已经按 status 分支过了,映射器只负责整形字段。

某个快手 hot-list-v1 端点的 SandBase API 参考,展示响应结构 快手的 hot-list-v1 把榜单以 list 直接放在 data 下。

第 3 步 —— 归一化并合并

两个映射器已经输出相同的行结构:source、rank、title、author、metric、url。合并就变得很简单——拼接,再可选按 rank 交错,让每个榜的顶部都升到合并流的顶部:

def combined_trends() -> list[dict]:
    rows = douyin_hot() + kuaishou_hot()
    # 保留各平台自己的排名,按来源分组
    rows.sort(key=lambda r: (r["source"], r["rank"] if isinstance(r["rank"], int) else 999))
    return rows


for row in combined_trends()[:10]:
    print(f"[{row['source']}] #{row['rank']} {row['title']}")

收益在于:你流水线的其余部分——去重、关键词打标、存储、告警——看到的是一个统一行,而不是两种厂商专属负载。以后加第三个平台,它也照样接在同一个 call 辅助函数和同一个行结构后面。

为什么这里统一信封很关键

两个榜在传输层看着不同,但它们回来时用的是相同的 { id, status, model, outputs } 信封。这意味着脆弱的那部分——鉴权、传输、status 处理、重试——只在 call 里写一次,两个都复用。唯一的每平台代码是那个知道各榜把行放在哪里的小映射器:抖音在 data.data.objs,快手在 data 下的 list。当负载变动时,你修一个映射器,而不是一整个采集器。

正是这种分离让聚合器容易生长。换进另一个热榜端点,写五行映射到同一行结构,所有下游消费者原封不动继续工作。你的注意力停在”趋势意味着什么”上,而不是停在调和信封上。当你需要的不止是热榜——某条视频的详情、某个创作者的主页——去各平台的总览里查到合适的端点,并在接入前确认它的参数。

局限与边界

  • 仅公开、只读数据。 不发帖、不关注、不涉及私有或仅账号可见的数据。
  • 负载结构不同且会变。 抖音嵌在 data.data.objs 下并带上游 code;快手把 list 放在 data 下。先看一次真实响应、读一遍各自结构。
  • 速率与量级。 把响应当作尽力而为的读取;遇到 HTTP 429 等瞬时错误时按退避策略重试。
  • 以线上参考核对端点。 可用性和字段可能变化;在依赖某个具体端点前先确认。
  • 这不是官方合作。 SandBase 提供对公开数据的统一访问;请尊重各平台条款和适用规则。

常见问题

我需要抖音或快手登录吗? 不需要。你用自己的 SANDBASE_API_KEY 向 SandBase 鉴权。这些读取端点不需要你这边有平台账号或 OAuth。

为什么抖音映射器检查 code,快手的不检查? 抖音把结果包在上游 code/data 对象里,所以读 data.objs 前先检查 code == 0。快手把 list 直接放在 data 下。每个端点都要检查一次真实响应。

我能加第三个平台吗? 能。复用 call 辅助函数,写一个映射到同一行结构的映射器,再拼接。这正是尽早归一化的意义所在。

动手搭

创建一个 SandBase API 密钥,跑两个热榜调用,归一化成一条流。准备好后: