Blog/开发者工具/

Instagram 话题标签研究 API 教程 | SandBase

围绕一个细分领域研究 Instagram 话题标签:找相关标签、翻页读取热门帖子、按互动量排序,三个 SandBase 端点搞定。无需 Instagram 登录或 OAuth,用一个 SandBase API Key 鉴权。

暗色电影感渲染:玻璃质感的井号发出一串图片卡片,经过分拣轨道堆成逐级升高的阶梯,最后流入 Agent 核心

在 Instagram 上做话题标签调研,起点往往就是一个词,加上几个说不太清的问题:围绕「vegan recipes」,哪些标签真的有量?大标签下面,现在什么样的帖子跑得好?那些表现好的帖子旁边,又反复挂着哪些小标签?

这篇教程用三个 SandBase 端点、大约 150 行 Python,把这三个问题依次回答掉。端点全貌可以先看Instagram 公开数据 API 总览。如果你要研究的是某个具体账号,而不是标签,走Instagram 账号研究教程那条路更合适,本文只讲标签。

这里读的都是公开、只读数据。不需要 Instagram 账号,也不需要 SDK,只要一个 SandBase API Key 做鉴权。本文用到的端点目前在 SandBase 目录里标的是 Free。

参数和响应信封以端点 API 参考为准,参考只保证信封结构。下文出现的业务字段名都来自我自己跑的调用(测试于 2026-10-01,UTC),属于实测观察,不是文档保证。上线前请拿真实响应再核对一遍。

先说结论

  • instagram/v2/search-hashtags(参数 keyword)返回相关标签和各自的 media_count,拿来圈候选标签足够了。
  • instagram/v2/hashtag-posts(参数 keyword、feed_type)返回帖子的点赞、评论和播放数;把响应里的 pagination_token 原样传回去就能翻页。
  • 按「点赞 + 评论」排序;隐藏了点赞数的帖子要单独标记,别当成 0。
  • instagram/v2/post-info(参数 code_or_url)可以回头复查某条帖子,但它的计数放在 metrics 里,不在顶层。

为什么用 v2 的标签接口,而不是 v3

总览里推荐的标签帖子端点是 instagram/v3/hashtag-posts,我先试的也是它。这个系列之前的一次测试里,它直接返回了空结果。这次倒是有数据了:run e0a2628b-a7b9-4dc7-b678-a6a83989af9b 返回 27 条帖子,带 more_available: true 和一个 next_max_id 游标。问题是,这一页的帖子全是最近几小时发的,大部分只有 0 到 8 个赞。说白了就是一条按时间排的流,拿来做监测可以,想看「这个标签下什么内容表现好」就没用了。

instagram/v1/hashtag-posts 也是同样的问题,而且结构是 GraphQL 那一套(data.hashtag.edge_hashtag_to_media.edges[].node),每条帖子只给发布者 id,没有用户名。

instagram/v2/hashtag-posts 多了一个 feed_type 参数,可选 top、recent、reels,默认 top。热门流(run ef34d367-313d-4470-a4d9-505e238103cc)返回 24 条帖子,时间跨度大约三个月,点赞数上千的不少,Reels 还带播放数。每条帖子里有发布者用户名、认证标记,以及已经解析好的正文标签列表。这正是做调研要的结构,所以全文统一用 v2 这一组。

你的需求用什么
公开的标签搜索、标签帖子流、帖子互动数据,用于调研SandBase Instagram 公开数据 API
发帖、管理自己的账号、看自己账号的数据洞察Instagram 官方 Graph API(需要你自己的商业账号)
私密账号或需要登录才能看的数据两条公开路线都不适用

整体流程

  1. 扩展种子词:调 instagram/v2/search-hashtags(keyword),只保留帖子量超过阈值的标签。
  2. 读热门帖子:调 instagram/v2/hashtag-posts(keyword,feed_type: "top")。
  3. 翻页:把 pagination_token 传回去,直到帖子够用或者 token 不再返回。
  4. 按互动排序,顺便统计这些帖子上一起出现的其他标签。
  5. 复查入围帖子:调 instagram/v2/post-info(code_or_url)。

SandBase Instagram API 目录页,列出 Instagram 端点并显示 Available 和 Free 状态 SandBase 上的 Instagram 目录页:共显示 81 个端点,路径形如 GET /apis/v1/instagram/...,右侧选中的端点标为 Available、Free。本文调用的是 Model API 的 POST /v1/api/instagram/... 路由。

写代码前先说清楚一件事:目录页展示的是 GET /apis/v1/instagram/<path>,而本文用的是端点参考里的 Model API,也就是 POST /v1/api/instagram/<path>,请求体是 JSON,只放这个端点自己的参数。复制示例时,POST 方法和 /v1/api/ 前缀都别改。

第 0 步:所有调用共用一个 helper

import os
import time
from datetime import datetime, timezone

import requests

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


def call(path: str, payload: dict, retries: int = 2) -> dict:
    for attempt in range(retries + 1):
        try:
            resp = requests.post(f"{API}/{path}", headers=HEADERS, json=payload, timeout=90)
            resp.raise_for_status()
            break
        except requests.RequestException:
            if attempt == retries:
                raise
            time.sleep(2 * (attempt + 1))
    body = resp.json()
    if body.get("status") != "completed":
        raise RuntimeError(body.get("error", {}).get("message", f"{path} did not complete"))
    # Documented shape is outputs[0].data; also accept a top-level `output`.
    output = body.get("output")
    if output is None and body.get("outputs"):
        output = body["outputs"][0].get("data", {})
    print(f"  {path} run id: {body.get('id')}")
    return output or {}

参考文档里,完成态的响应写的是 outputs[0].data。这次 Instagram 的所有调用确实都是这个结构。helper 仍然兼容顶层 output,因为我在 SandBase 其他平台端点上见过这种形态,而且两种结构可能在不同调用之间来回变。另外,v2 这几个接口在 outputs[0].data 里面又包了一层 data,所以后面的代码会再取一次 .get("data")。helper 顺手把每次的 run id 打出来,结果不对劲时方便回头对照原始响应。

第 1 步:把种子词扩展成候选标签

def find_hashtags(seed: str, min_posts: int = 1000) -> list[dict]:
    out = call("instagram/v2/search-hashtags", {"keyword": seed})
    items = (out.get("data") or {}).get("items", []) or []
    tags = [
        {"name": t.get("name"), "media_count": t.get("media_count") or 0}
        for t in items
        if (t.get("media_count") or 0) >= min_posts
    ]
    return sorted(tags, key=lambda t: t["media_count"], reverse=True)

种子词用 veganrecipes,搜索(run 0ae668b6-3580-498d-ae55-55e34a7fb84c)返回了 55 个标签。下面是 outputs[0].data 的节选:

{
  "data": {
    "count": 55,
    "items": [
      {"id": "17843646523025713", "name": "veganrecipeshare", "media_count": 332329, "allow_following": false},
      {"id": "17842274242068426", "name": "veganrecipes", "media_count": 9249683, "allow_following": false},
      {"id": "17842769464147759", "name": "veganrecipesforhealth", "media_count": 7781, "allow_following": false}
    ]
  }
}

返回列表并不是按帖子量排的,得自己排。阈值设成 1000,能把一长串几乎没人用的拼写变体筛掉。后面完整跑的那一次,排在最前的是 veganrecipes(约 920 万条帖子),接着是 veganrecipeshare、easyveganrecipes、alkalineveganrecipes、rawveganrecipes,都在 10 万条以上。

我也试了 instagram/v3/search-hashtags,它的参数叫 query,不叫 keyword。返回 20 个标签,id 和帖子量跟 v2 一致,另外多一个用于翻页的 rank_token。但我那次调用里 rank_token 是 null,根本没有第二页可翻。v2 一次就给得更多,所以留用 v2。

SandBase 上 Instagram v2 search-hashtags 端点的 API 参考页 instagram/v2/search-hashtags 参考页:POST /v1/api/instagram/v2/search-hashtags,只有一个必填参数 keyword。文档里的响应示例中,outputs[0].data 是空对象。

第 2 步:读取并翻页标签下的热门帖子

def to_utc(value) -> str | None:
    # The top feed returned ISO strings; the recent feed returned Unix seconds.
    if isinstance(value, (int, float)):
        return datetime.fromtimestamp(value, tz=timezone.utc).isoformat()
    return value


def hashtag_posts(tag: str, feed_type: str = "top", max_pages: int = 2) -> list[dict]:
    posts, token = [], None
    for _ in range(max_pages):
        payload = {"keyword": tag, "feed_type": feed_type}
        if token:
            payload["pagination_token"] = token
        out = call("instagram/v2/hashtag-posts", payload)
        for item in (out.get("data") or {}).get("items", []) or []:
            user = item.get("user") or {}
            posts.append({
                "code": item.get("code"),
                "url": f"https://www.instagram.com/p/{item.get('code')}/",
                "account": user.get("username"),
                "verified": user.get("is_verified"),
                "media_type": item.get("media_type"),
                "likes": item.get("like_count"),  # None when likes are hidden
                "comments": item.get("comment_count") or 0,
                "plays": item.get("play_count") or 0,
                "taken_at": to_utc(item.get("taken_at")),
                "hashtags": item.get("caption_hashtags") or [],
                "paid_partnership": item.get("is_paid_partnership"),
            })
        token = out.get("pagination_token")
        if not token:
            break
    return posts

参考文档写了 keyword、feed_type、pagination_token 三个参数。实测里 token 的位置也很明确:它在 outputs[0].data 的顶层,跟内层 data 并列,而不是在 data 里面。原样作为 pagination_token 传回去就能拿到下一页。热门流第一页 24 条,第二页(run 4c96135e-d0c9-4642-8979-bd47599fe0be)又来了 30 条,两页没有重复,还带了新的 token。我只翻了两页,热门流到底能翻多深没测过。

下面是热门流里的一条帖子,做了删减。这是一个认证食谱账号发的 Reel,账号名我去掉了,只保留计数:

{
  "code": "DcZlsXtzS-X",
  "media_type": 2,
  "product_type": "clips",
  "like_count": 5719,
  "comment_count": 1763,
  "play_count": 661027,
  "taken_at": "2026-08-23T22:44:44Z",
  "taken_at_ts": 1787525084,
  "is_paid_partnership": false,
  "like_and_view_counts_disabled": false,
  "caption_hashtags": ["#pineapplechutney", "#spicychutney", "#easyrecipes", "#veganrecipes"]
}

这里踩了三个坑。第一,24 条里有 2 条的 like_count 是 null,这两条的 like_and_view_counts_disabled 都是 true,也就是作者把点赞数藏起来了,不能当成 0。第二,taken_at 在 top 流里是 ISO 字符串,到了 recent 流(run a249ddfd-f55a-4f94-809f-f4cf42482341)却变成 Unix 时间戳,所以 to_utc 两种都要处理。第三,media_type 实测有 1(图片)、2(视频或 Reel)、8(轮播)三种,轮播帖的 play_count 是 0,播放数只适合在视频之间比较。

SandBase 上 Instagram v2 hashtag-posts 端点的 API 参考页 instagram/v2/hashtag-posts 参考页:必填 keyword(不带 #),可选 feed_type(默认 top),可选 pagination_token(来自上一次响应)。

第 3 步:按互动排序,统计伴随标签

def rank_by_engagement(posts: list[dict]) -> list[dict]:
    seen, ranked = set(), []
    for p in posts:
        if p["code"] in seen:
            continue
        seen.add(p["code"])
        p["likes_hidden"] = p["likes"] is None
        # hidden likes: no score, sorted after posts with full counts
        p["engagement"] = None if p["likes_hidden"] else p["likes"] + p["comments"]
        ranked.append(p)
    return sorted(ranked, key=lambda p: (p["engagement"] is not None, p["engagement"] or 0), reverse=True)


def co_hashtags(posts: list[dict], tag: str, top: int = 10) -> list[tuple[str, int]]:
    counts: dict[str, int] = {}
    for p in posts:
        for h in {h.lower().lstrip("#") for h in p["hashtags"]}:
            if h != tag.lower():
                counts[h] = counts.get(h, 0) + 1
    return sorted(counts.items(), key=lambda kv: kv[1], reverse=True)[:top]

这里的「互动」就是点赞加评论的绝对数。标签帖子流里没有发布者的粉丝数,所以光靠这个端点算不出互动率。真要算,就把入围账号拿去走另一篇教程里的账号查询流程,代价是多几次调用。

隐藏了点赞数的帖子不计互动分,统一排在数据完整的帖子后面,likes_hidden 标记说明原因,这样残缺的数字不会和完整的数字混着比。伴随标签取自 caption_hashtags,实测带 # 前缀,大小写也不统一(同一条帖子里同时有 #DairyFree 和 #dairyFree),所以代码先转小写,再在单条帖子内去重,然后才计数。

第 4 步:回头复查一条入围帖子

def refresh_post(code: str) -> dict:
    data = call("instagram/v2/post-info", {"code_or_url": code}).get("data", {}) or {}
    metrics = data.get("metrics") or {}
    caption = data.get("caption") or {}
    return {
        "code": data.get("code"),
        "account": (data.get("user") or {}).get("username"),
        "product_type": data.get("product_type"),
        "taken_at": data.get("taken_at_date"),
        "likes": metrics.get("like_count"),
        "comments": metrics.get("comment_count"),
        "hashtags": caption.get("hashtags") or [],
    }

我第一版就是在这一步读错了字段。标签帖子流里,like_count 在每条帖子的顶层;post-info 的顶层压根没有 like_count,计数都放在 metrics 对象里。我拿 Instant Pot 品牌官方账号的一条轮播帖做测试,它出现在 #veganrecipes 的最新流里(run f714c418-01ef-4074-9cf1-94f6241169d8)。节选如下:

{
  "data": {
    "code": "Dd7t9arlwZC",
    "product_type": "carousel_container",
    "carousel_media_count": 3,
    "taken_at_date": "2026-10-01T01:22:00+00:00",
    "metrics": {"like_count": 4, "comment_count": 0, "play_count": null, "share_count": null},
    "caption": {
      "text": "Yes, ice cream can be vegan. We tested it - and it works perfectly. ...",
      "hashtags": ["#instantpot", "#instantchill", "#veganicecream", "#vegan", "#veganrecipes"]
    },
    "user": {"username": "instantpotofficial", "full_name": "Instant Pot®", "is_verified": true}
  }
}

这条帖子当时才发了几个小时,数字小很正常。复查的意义就在这里:隔一天、隔一周再跑一次,看入围的帖子是不是还在涨。我也拿同一个 shortcode 试了 instagram/v3/post-info-by-code(run bc654757-6209-44de-9ff6-de74b144316c),返回的是一个 items 列表,结构完全不同,为了前后一致还是用 v2。

SandBase 上 Instagram v2 post-info 端点的 API 参考页 instagram/v2/post-info 参考页:只有一个必填参数 code_or_url,可以传帖子 shortcode,也可以传完整帖子链接。

串起来跑一遍

if __name__ == "__main__":
    seed = "veganrecipes"
    print("1) related hashtags")
    tags = find_hashtags(seed)
    for t in tags[:8]:
        print(f"   #{t['name']:<28} {t['media_count']:>10,}")

    print("2) top posts under the seed tag, two pages")
    posts = hashtag_posts(seed, "top", max_pages=2)
    print(f"   collected {len(posts)} posts")

    print("3) ranked by likes + comments")
    ranked = rank_by_engagement(posts)
    for p in ranked[:5]:
        flag = " (likes hidden)" if p["likes_hidden"] else ""
        print(f"   {p['code']}  eng={str(p['engagement']):>6}  plays={p['plays']:>7}  type={p['media_type']}{flag}")
    print("   co-occurring tags:", co_hashtags(posts, seed, 8))

    print("4) refresh one post")
    print("  ", refresh_post("Dd7t9arlwZC"))

整份文件我原样跑过一遍。搜索是 run 4323d2d9-d773-447b-adaa-297cc5982320,两页标签帖子是 45830a7e-d588-4674-b629-5e2a5b5d9309 和 27d391bb-1aeb-4af4-a9fa-a88a9bcc1bcd,复查是 f714c418-01ef-4074-9cf1-94f6241169d8。两页一共 54 条帖子。互动量前五名里有四条是 Reel(media_type 为 2),第一名点赞加评论约 2 万,播放约 50 万。出现最多的伴随标签是 vegan(54 条里有 11 条)、plantbased(10 条)、plantbasedrecipes(6 条)和 easyrecipes(6 条)。

测试范围说在前面:一个标签、一个下午、两页数据。它只能说明那个时刻这个标签的热门流长什么样,代表不了 Instagram 整体的排序逻辑。把第 1 步圈出来的每个标签都这样跑一遍,就能得到一张小对比表交给 Agent 总结:哪些标签的热门流以 Reel 为主,哪些伴随标签反复出现,哪些帖子值得细看。

成本也好算:每个标签每页一次调用,每个种子词一次搜索,每条复查的帖子一次调用。按 code 做缓存,重跑时就不会重复抓。

文档保证 vs. 实测观察

项目状态
POST /v1/api/instagram/v2/search-hashtags,参数 keyword参考文档已写明
POST /v1/api/instagram/v2/hashtag-posts,参数 keyword、feed_type、pagination_token参考文档已写明
POST /v1/api/instagram/v2/post-info,参数 code_or_url参考文档已写明
信封 id / status / model / outputs[0].data参考文档已写明
搜索结果 data.items[] 中的 name、id、media_count仅实测观察
pagination_token 位于 outputs[0].data 顶层仅实测观察
帖子的 code、like_count、comment_count、play_count、media_type、caption_hashtags、user仅实测观察
taken_at 在 top 流是 ISO 字符串,在 recent 流是 Unix 秒仅实测观察
post-info 的计数在 metrics 下,另有 caption.hashtags、taken_at_date仅实测观察

常见用法

细分领域选标签

从一个种子词出发,按帖子量设阈值,再对比各标签的热门流。输入:种子词。输出:带帖子量的候选标签清单。端点:v2/search-hashtags。

内容形式调研

统计每个标签热门流里的 media_type 分布,看是 Reel、轮播还是图片占主导。输入:标签名。输出:各标签的内容形式占比。端点:v2/hashtag-posts。

为投放找伴随标签

统计热门帖子上一起出现的标签,找出值得测试的相邻标签。输入:一个标签。输出:伴随标签频次表。端点:v2/hashtag-posts。

发现创作者和品牌

把互动最高的帖子背后的账号列出来,再交给账号查询流程拿粉丝数。输入:排好序的帖子。输出:待评估的账号清单。端点:先 v2/hashtag-posts,再走账号研究流程。

实操提醒

  • 不同版本参数名不一样。 v2 搜索用 keyword,v3 搜索用 query;标签帖子 v2 用 keyword,v3 用 tag,v1 用 hashtag。
  • 按目的选 feed。 看「什么表现好」用 top,做监测用 recent。我测的时候,v3 标签接口的表现更像一条按时间排的流。
  • 隐藏点赞是 null,不是 0。 检查 like_and_view_counts_disabled,把这类帖子单独标出来。
  • 时间戳和标签要统一格式。 不同 feed 的时间格式不同,标签带 # 且大小写混用。
  • post-info 的计数换了位置。 读 metrics.like_count,不要读顶层。
  • 别把个人数据存进库。 标签下的帖子里有不少普通个人的公开账号。建议只存帖子 code 和计数,用户名只保留你打算联系的品牌或商业账号。
  • 业务字段只是实测观察。 依赖之前先拿真实响应核对。

常见问题

需要 Instagram 账号或登录吗? 不需要。你用 SANDBASE_API_KEY 在 SandBase 鉴权,这几个只读端点不需要你这边的 Instagram 账号或 OAuth。

收费吗? 本文用到的端点目前在 SandBase 目录里标的是 Free,具体以目录页当前状态为准。

能算互动率吗? 只靠标签帖子流不行。它给每条帖子的计数,但不给发布者粉丝数。把入围账号单独查一下,再自己相除。

怎么拿到一个标签下更多的帖子? 把上一次响应里的 pagination_token 传回去,响应里不再有 token 时就停。

关键词要带 # 吗? 不用。参考文档要求不带 #,我也是这么调的。

收个尾

研究 Instagram 上的一个标签领域,一个种子词就够起步:一次调用扩展出带帖子量的标签,翻几页看清某个标签下什么内容表现好,最后再复查真正关心的那几条帖子。按点赞加评论排序、标记隐藏点赞、统计伴随标签,Agent 手里就有了能具体总结的材料。其他 Instagram 端点见Instagram 公开数据 API 总览。准备动手的话: