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

在 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(需要你自己的商业账号) |
| 私密账号或需要登录才能看的数据 | 两条公开路线都不适用 |
整体流程
- 扩展种子词:调
instagram/v2/search-hashtags(keyword),只保留帖子量超过阈值的标签。 - 读热门帖子:调
instagram/v2/hashtag-posts(keyword,feed_type: "top")。 - 翻页:把
pagination_token传回去,直到帖子够用或者 token 不再返回。 - 按互动排序,顺便统计这些帖子上一起出现的其他标签。
- 复查入围帖子:调
instagram/v2/post-info(code_or_url)。
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。
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,播放数只适合在视频之间比较。
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。
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 总览。准备动手的话: