做小红书评论类项目时,我见过太多人一上来就调接口,跑完一遍数据后发现:一级评论抓了一堆,真正想看的楼中楼回复却一条都没有。这不是个别现象,而是普遍的误区——大多数评论列表接口默认只返回最外层的评论,二级评论需要另外一个调用维度。这篇内容就把“小红书笔记评论API调用获取小红书笔记评论二级评论”这件事彻底讲透,包括接口结构、参数含义、遍历逻辑、高频报错和合规边界,适合做社交舆情分析、达人营销评估、评论运营管理的人参考。
1. 先搞清楚评论的两层结构:一级评论和二级评论不是同一个世界
1.1 你看到的评论列表,只是浮在水面上的冰山
打开任意一篇小红书笔记,评论区默认展示的是按热度排序的一级评论。每条一级评论下面如果还有讨论,会折叠成类似“共12条回复”的入口,点击后才加载该条评论下的二级评论,也就是俗称的楼中楼。
对应到接口侧,情况完全一样。大多数开发者第一次调用评论接口时,拿到的 comments 数组里只有顶层评论。这个数组里的对象,看起来字段齐全——有评论ID、用户信息、点赞数、内容文本,于是很多人便误以为“我已经拿到了这条笔记的全部评论”。
问题就出在这里。顶层评论对象里通常带着一个 sub_comment_count 字段,它告诉你这条评论底下还挂了多少条二级评论。如果你拿到这个字段却视而不见,你的数据里就会永久地缺失掉一部分互动内容。对于营销投放分析来说,这不仅是数据不全的问题,而是会直接影响结论——评论区里真正产生对话、讨论甚至争议的部分,恰好都藏在二级评论里。
实操经验:拿到一条笔记的评论数据后,第一件事不是急着遍历下一页,而是先统计 sub_comment_count 的总和,看看二级评论在一级评论中的占比。通常互动率高的笔记,二级评论占比在30%到60%之间,如果这个比例是0,几乎可以断定你的调用方式漏掉了楼中楼层。
1.2 从字段设计看懂评论的树形关系
我在对接评论接口时,习惯先用一张表把字段关系理清楚,避免在后面写遍历逻辑时搞混。下面是评论对象中几个核心字段的最小模型:
| 字段 | 可能出现的位置 | 含义 | 注意事项 |
|---|---|---|---|
id |
一级/二级评论都有 | 评论唯一ID | 二级评论的ID不能用于请求二级评论列表 |
note_id |
一级/二级评论都有 | 笔记ID | 翻页时不要动这个字段 |
sub_comment_count |
仅一级评论 | 该评论下的二级评论总数 | 为0时无需再调用二级评论接口 |
sub_comment_cursor |
仅一级评论 | 二级评论翻页游标 | 不能和一级评论的游标混用 |
sub_comment_has_more |
仅一级评论 | 是否还有更多二级评论 | 用于控制楼中楼层翻页 |
top_comment_id |
请求参数 | 一级评论ID | 获取某条一级评论下二级评论时的必传参数 |
这个结构的核心逻辑是:小红书目前的评论区最多开放两层,二级评论下面不会再挂更深层的嵌套,所以不存在“三级评论”一说。这个设计大大简化了树形遍历的复杂度——你只需要做一层循环,把每条一级评论作为父节点,拉取它下面的子节点,然后挂上去即可。
容易踩坑的地方:cursor 字段。很多人习惯把游标理解成页码,于是试图用 cursor=2、cursor=3 的方式翻页,结果发现返回的数据永远只有第一页。因为小红书接口用的是天级变化的字符串游标,每次翻页必须使用上一次响应返回的新游标,而不是自己拼接数字。后面我会专门讲翻页逻辑。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 评论接口调用前必须理清的请求链路
2.1 从页面请求到接口参数:URL长什么样
小红书的Web端笔记页是典型的单页应用,页面本身不包含评论数据,数据全部通过异步接口加载。评论相关的接口经过几次版本演进,现在常用的大致是这样一个路径:
text复制GET https://www.xiaohongshu.com/api/sns/web/v2/comment/page
我在实际项目中见过有人还在用 v1 版本的接口路径,虽然也能返回数据,但字段命名和二级评论的处理方式有明显差异,而且稳定性不如 v2。建议新项目一律以 v2 为主,v1 仅作为兼容备选。
请求参数大致如下:
| 参数名 | 类型 | 是否必传 | 说明 |
|---|---|---|---|
note_id |
string | 是 | 笔记的唯一ID,可以从笔记详情页URL中提取 |
cursor |
string | 否 | 一级评论翻页游标,首次请求不传 |
top_comment_id |
string | 否 | 获取二级评论时传入的一级评论ID |
num |
int | 是 | 每页数量,通常在10到30之间 |
image_formats |
string | 否 | 图片格式,如 jpg,webp,avif,不影响评论正文 |
这里最关键的是 top_comment_id。当你不传这个参数时,接口返回的是笔记的一级评论列表;当你传入某条一级评论的 id 时,接口返回的是该评论下的二级评论列表。
我用 Python 把请求过程封装成了一个基础函数,核心逻辑可以参考:
python复制import requests
def fetch_comments(note_id, cursor=None, top_comment_id=None, cookie="", headers=None):
params = {
"note_id": note_id,
"num": 20,
"image_formats": "jpg,webp,avif",
}
if cursor:
params["cursor"] = cursor
if top_comment_id:
params["top_comment_id"] = top_comment_id
# headers 里必须包含签名、cookie、UA等,下面会讲
resp = requests.get(
"https://www.xiaohongshu.com/api/sns/web/v2/comment/page",
params=params,
headers=headers,
timeout=10,
)
resp.raise_for_status()
return resp.json()
注意这只是一个示意实现,真实调用时 headers 里必须携带完整的浏览器标识和签名参数。现阶段先理解参数结构,签名问题我单独拆一节讲。
2.2 Header 里不能省的字段:签名、Cookie 与浏览器画像
小红书的Web端接口有一个特点:单纯用 Postman 直接拼 URL 调用,大概率返回错误。原因在于每个请求的 Header 里需要携带一组动态签名参数,常见的有 x-s、x-t、x-s-common 这几个。
简单解释一下这组参数的作用:
x-t:请求发起时的毫秒级时间戳,服务端用它校验请求的新鲜度。x-s:基于请求路径、参数和特定算法生成的结果,本质是一道签名校验,防止接口被无脑调用。x-s-common:一段更长、包含设备指纹信息的签名串,和上面的x-s配套使用。
这就引出了很多人在“API如何调用”这个问题上的第一个困惑:为什么明明URL和参数都对,返回还是400? 大概率就是签名缺失或已过期。
我的建议非常务实:不要自己去逆向完整的签名算法,那是高成本、低收益的投入,而且容易触发法律风险。更稳妥的方式是借助浏览器环境来生成签名。具体思路是:自己启动一个浏览器自动化工具(例如 Playwright 或 Puppeteer),用真实的浏览器访问笔记页面,在页面上下文中注入脚本,挂载或拦截接口请求,让浏览器帮我们补齐所有 Header 和签名,再把响应的 JSON 数据转发给业务逻辑。
用这种方案,签名由真实浏览器环境产出,几乎不存在过期和算法版本变动的问题,你只需要关心接口的数据解析。代价是QPS上不去,但对评论这种低频数据采集场景完全够用。
如果一个请求的 Header 里缺少 Referer 或 Origin,有些版本的服务端也会直接拒绝。最省事的做法是:在浏览器开发者工具里复制完整请求,把请求头原样保留,然后只替换 cookie 和签名部分。这样可以避免很多定位障碍。
2.3 响应结构:如何判断你拿到的是一级还是二级评论
接口返回的 JSON 结构并不复杂,但字段命名需要留意。一个典型的成功响应长这样:
json复制{
"code": 0,
"success": true,
"data": {
"cursor": "xxxxxx",
"has_more": true,
"comments": [
{
"id": "64f8d3a2000000001b016a88",
"content": "这条评论写得不错",
"user_info": {},
"like_count": 128,
"sub_comment_count": 5,
"sub_comment_cursor": "yyyyyy",
"sub_comment_has_more": true
}
]
}
}
判断当前响应是一级评论还是二级评论,不需要额外标记字段,看两点:
- 请求时是否传了
top_comment_id。传了,返回的就是初级评论下的二级评论。 - 返回对象里是否包含
sub_comment_count字段。一级评论通常带这个字段,二级评论一般不再带深层嵌套字段。
如果某个 comments 数组里的元素出现了 id 却找不到对应一级评论的标识,千万别把它当成一级评论来处理,它在树形结构里是挂在父节点下的子节点。
3. 二级评论获取的核心流程:从单条遍历到全量并发
3.1 拉取某条一级评论下二级评论的完整步骤
获取二级评论和获取一级评论在接口上是同一个,只是多了 top_comment_id 参数。核心流程归纳下来是四步:
- 调用评论接口获取一级评论列表,解析每条评论的
id和sub_comment_count。 - 筛选出
sub_comment_count > 0的一级评论,逐条进入二级评论拉取环节。 - 使用一级评论的
id作为top_comment_id,请求二级评论列表,并通过响应中的cursor持续翻页。 - 每次翻页前检查
has_more或sub_comment_has_more,直到该一级评论下的二级评论全部拉完。
这里最容易犯的错误是:把一级评论翻页时的 cursor 用在二级评论请求里。一级评论的游标只针对顶层评论列表,二级评论的游标是针对某个一级评论的子列表,两者完全不同。如果你发现第二次请求返回的数据始终和第一次一样,先检查 top_comment_id 是否正确传入,再检查 cursor 是否来自上一个二级评论响应。
一个更隐蔽的问题是:某条一级评论下面如果恰好有用户删除了回复,返回的二级评论总数和 sub_comment_count 可能对不上。这时候不要试图用 sub_comment_count 去做“拉够了就停”的硬校验,否则数据会多一条或少一条。正确的做法是只依赖 has_more 和相关游标字段判断是否终止。
3.2 全量评论遍历的并发设计与节流
拿到二级评论的接口调用方式后,有一个现实问题摆在面前:一篇热门笔记可能有几百条一级评论,每条下面又有几十条二级评论,如果一条一条串行请求,耗时非常可怕。
我自己在项目里做过一个统计:单笔记一级评论数500条,其中有二级评论的约200条,每条二级评论平均2页数据,串行请求需要400到500个HTTP请求,按每个请求1秒算,就是8分钟以上。如果遇到响应变慢或网络抖动,半小时都跑不完。
所以并发是必须考虑的。常用的方案是用固定大小的线程池,例如控制在8到12个并发请求。这样总耗时可以从8分钟压到1分钟以内。
并发带来的问题也随之而来:小红书接口对单账号请求频率有明确的风控逻辑。我在测试中发现,如果把并发提到20以上,通常跑到第30个请求左右就会触发验证码或返回频率限制状态码。所以并发不是越大越好,必须配合节流策略。
下面是一个简单的节流控制思路:
- 全局维护一个最近请求时间戳队列,每次请求前检查当前时间与队列中最早时间戳的差值,控制每秒请求数不超过3到5次。
- 使用随机延时,让请求间隔在0.2到0.5秒之间抖动,避免形成机械节奏。
- 遇到频率限制响应时,立刻停止新请求,休眠5到10秒再恢复,而不是继续重试堆积。
拦截到一条一级评论后,先判断 sub_comment_count == 0 就跳过,这是最基础也最有效的性能优化。避免对没有二级评论的评论发送无意义请求。
3.3 把评论组装成树:数据落地时容易忽略的问题
很多人拿到二级评论后,顺手存成一个扁平JSON数组,等到要展示“楼中楼”效果时才发现父子关系丢了。正确的做法是在拉取每条二级评论时,把它的一级评论父ID存储下来。
我建议的数据落地格式是这样:
python复制comment_data = {
"note_id": "xxx",
"parent_comment_id": None, # 一级评论为空
"comment_id": "一级评论ID",
"content": "一级评论内容",
"reply_list": [
{
"parent_comment_id": "一级评论ID",
"comment_id": "二级评论ID",
"content": "二级评论内容"
}
]
}
如果你用的是关系型数据库,可以建一张 comments 表,包含 note_id、comment_id、parent_comment_id、content、created_at 等字段,用 parent_comment_id 关联层级关系。如果只是本地分析,JSON 文件或者 SQLite 都是不错的选择。先把 comment_id 设置为唯一索引,避免重复写入。
这里有一个我在实际处理时踩过的坑:同一篇笔记如果跑了两次全量采集,二级评论中出现了重复数据。原因是我把每个一级评论的“二级评论第一页”当成全量了,后面翻页失败后又以另一个游标重新拉了一遍。所以翻页逻辑里必须有去重机制,最简单的方案是把每次返回的 comment_id 存入一个集合,追加前先判断是否已存在。
4. 高频报错排查链路:从400到空数据
4.1 400错误:别急着怀疑签名,先查参数格式
做接口对接,最常撞见的就是400错误。热搜词里大量出现“api error 400”相关的内容,说明这是一个普遍问题。我的排错习惯是:先查参数,再查Header,最后才查签名。
按照这个顺序,依次确认以下事情:
note_id是否真是笔记ID而不是分享口令或短链接。cursor是否传入了数字类型的页码而不是字符串游标。top_comment_id是否对应真实存在的一级评论ID。num是否超过服务端上限,超出的直接截断或报错。- 请求头里的
Content-Type是否和请求方法匹配。
我见过一个非常隐蔽的400:请求参数里出现了未转义的特殊字符。如果某个用户的评论内容或一级评论ID被直接拼到URL里,遇到特殊符号就会导致服务端解析失败。解决方法是所有参数都通过请求库的 params 或 data 字段传入,不要自己拼接查询字符串。
如果以上都没问题,再看 x-s 签名是否过期。签名和 x-t 时间戳是绑定的,如果你的脚本在发出请求前做了多次重试,而 x-t 取自第一次请求的时间,等真正发出时可能已经不在服务端接受的时间窗口内了。解决方法是让签名参数从带外接口获取,并且在每次请求前重新取。
4.2 429与接口配额:调用量限制和频控不是一回事
社交平台的接口通常有两层限制:配额限制和频控限制。配额限制指的是单位时间内的总请求量,例如每小时多少次;频控限制则是单账号、单IP维度上的短时间请求频率。遇到429或类似的频率限制响应时,我个人的处理方式是:
- 停止当前线程,记录触发限制的请求上下文。
- 以指数退避策略等待,比如第一次2秒,第二次4秒,第三次8秒,最多等60秒。
- 恢复请求前,主动降低全局并发数,比如从10降到4。
- 为当前账号设置单日用量上限,到达后直接停止而不是反复试探。
很多人习惯“遇到429就重启脚本”,这是最差的做法。频繁重启不仅不能解决问题,反而会因为同一时间点涌入大量请求而把账号推向更严厉的风控状态。
数据量预估:在做大规模采集前,我会先用接口的返回字段估算总量。例如一篇笔记有100条一级评论、500条二级评论,以每页20条计算,需要大约30个请求。如果计划采集1000篇笔记,那总请求量就是30000次,这个量级务必要提前设计好配额分配。
4.3 只有一层评论,没有二级评论:三个隐藏原因
有时候接口调用看起来“成功”了,一级评论都拿到了,但所有二级评论都是空的。这时候不要怀疑是自己的代码烂,多半是以下三种情况之一:
第一,某条一级评论本身确实没有回复,sub_comment_count 为0,跳过是正常的。这不是Bug,是数据本身如此。
第二,接口版本问题。v1 接口有时会把二级评论放在类似 sub_comments 的子字段里,而用 v2 的解析方式去读自然拿不到。建议在代码里做一次兼容处理:如果 comments 数组里没有 sub_comment_count,尝试从 sub_comments 字段读取,或者直接统一升级到 v2 接口。
第三,评论被折叠。小红书对部分低质量或风险评论会做折叠处理,折叠的评论不会出现在普通接口返回里。这种情况属于平台策略,接口调用方无法强行获取。
| 现象 | 可能原因 | 处理方式 |
|---|---|---|
| 二级评论接口返回空数组 | 该评论无任何回复 | 跳过,属于正常情况 |
一级评论有 sub_comment_count 但二级全是空 |
接口版本字段不一致 | 检查响应结构,统一用 v2 |
| 明显有评论但接口拉不到 | 评论被折叠或风控 | 放弃,不强行绕过 |
| 返回结果总比页面少 | 部分评论被过滤 | 接受数据偏差 |
4.4 数据一致性:为什么评论数对不上
这个问题在评论区数据采集里几乎一定会遇到。我用一个案例来说明:某条一级评论在页面上显示“共8条回复”,但接口只返回了5条,并且 has_more 已经是 false。
排查后发现的规律是:差掉的3条回复都属于风险折叠或用户删除的状态,接口不会返回。这时候不要试图通过翻页补数据,因为服务端根本没有下发这些数据,重试再多次也没有意义。
正确的做法是在数据入库时保留 sub_comment_count 原始值,在展示层或分析层用这个原始值做标注,而不是把实际拉取到的一级评论树当成“全场最佳”的权威数据源。
5. 合规边界:调用这类接口前,先想明白这几件事
5.1 平台协议与个人信息保护的基本底线
小红书官方目前没有向普通开发者开放公开的笔记评论API,市面上能调用的评论接口都是通过分析Web端或移动端请求得到的非公开接口。这决定了任何人对这些接口的使用都必须格外谨慎。
合规问题的核心有两点:一是遵守平台用户协议和Robots协议,二是遵守个人信息保护相关的法律法规。评论区数据中包含用户昵称、头像、评论内容、点赞数等信息,其中昵称和评论内容结合后足以识别到特定个人,属于个人信息范畴。拿到这些数据后如果用于公开传播、买卖或精准营销,都存在法律风险。
我个人的原则是:评论数据只能用于内部统计分析和学术研究,不做任何形式的公开转售。导出到公开文档或报告时,必须对用户名进行脱敏处理,或者只展示脱敏后的统计数据。
5.2 个人开发者可以做和不能做的事
先说不建议做的事:大规模批量抓取、逆向破解签名算法、绕过验证码、提供付费“监控服务”给第三方,这些行为既让账号风险极高,也面临法律合规问题,职业声誉上更是不值得。
再说什么场景相对安全:个人学习研究接口调用原理、小规模地分析自己账号笔记下的评论、在明确合规的前提下为甲方提供舆情统计服务(并且数据做脱敏处理)。这些场景的调用量小、频次低,对平台服务影响小,风险相对可控。
我在多个项目里的做法是:单个账号单日调用量控制在几百次以内,并且所有请求都分散在自然时间点,不做半夜批量突击。数据落地后尽快做匿名化处理。技术能力应该用来解决问题,而不是制造麻烦,想清楚边界再动手,才不会把自己推向“拿到数据但惹上官司”的尴尬处境。
如果你真的需要长期、稳定、全量的小红书评论数据,建议优先考虑官方合作的第三方数据服务商,他们有正规的数据授权链路,虽然贵一些,但省心、安全。自己写脚本拿数据,更适合探索技术、跑通流程、做小规模验证。
最后,分享一个我自己的落地经验
我在项目里把二级评论获取逻辑封装成了一个通用函数后,最深刻的体会是:这个任务80%的精力不是在“调通接口”,而是在“稳定地处理分页和去重”。评论接口的结构其实不复杂,但游标、父子关系、空数据判断这些细节会在你跑到第5000条评论时变成致命伤。
我建议你在写代码前,先花半小时手工请求一个最小样本:一篇笔记的一级评论、其中一条有二级评论的评论、该评论下的翻页响应。把这三类响应完整打印出来,对着字段名写解析代码,会比你直接在网上找现成封装库再改要快得多,因为别人的封装大概率和你遇到的实际字段版本不一致。数据落到数据库后,再随机抽查两个笔记页面,人工核对一下你的树形结构是否和页面显示一致。这一步能过滤掉大多数潜在问题。
