EveryInfra

Build in Public · LF-02

小红书评论怎么批量获取:从笔记清单到可核对的结果

逐步接入小红书 comments_batch:准备完整笔记链接、区分 URL 数与评论条数、识别 partial 和 missed、按笔记去重,并处理空结果、重试与目标计费。

做小红书评论研究,第一步往往不是分析情绪,而是整理笔记链接。几个人各交一份清单,同一篇笔记重复出现;过了一段时间,有些链接不能再使用;批量请求明明返回 200,结果里却只看见部分笔记。如果不先解决这些问题,后面的“高频问题”和“负面占比”可能只是采集缺口的另一种表达。

EveryInfra 的 xiaohongshu.comments_batch 接收笔记 URL 数组,用于一次提交多个目标。本篇从一条笔记开始,逐步说明如何扩成有记录、能核对的批处理:准备目标、确认参数、发送请求、按笔记归属整理评论,再决定哪些目标可以重试。

文中的输入约束与响应处理依据 2026-09-04 的公开目录和源码核对;业务请求是可改写模板,离线例子是明确标注的合成数据,不冒充本轮真实评论。调用前仍需确认你的目标、用途与保存范围获准。

先选对能力:搜索笔记、评论和回复是不同任务

search 用关键词发现笔记,note 读取某篇笔记详情,comments 面向一篇笔记,comments_batch 面向多篇笔记。需要某条评论的下级回复时,目录另有 sub_comments,输入还需要 comment_id。它们不是一个接口换个参数名就能互相代替。

先确定自己的研究对象。例如,“看这组产品相关笔记中的用户反馈”需要一份笔记清单;“获取所有提到品牌的评论”则还涉及笔记发现范围、检索遗漏、评论覆盖和时间窗口,不能仅凭批量评论能力就承诺全量。

这里也要与官方账号接入区分开。小红书官方 API 参考提供授权、令牌与用户信息等接口;若你的需求是用户登录或读取授权账号资料,应从相应官方流程开始。本文的 EveryInfra 请求契约不是那份官方账号契约,官方链接也不证明评论读取已获得平台背书。

第一步:核对目录中的输入、模式和字段

只读取 comments_batch 的当前声明
curl -fsS --max-time 30 \
  'https://api.everyinfra.com/api/v1/social/catalog?platform=xiaohongshu' \
  | jq -e '.capabilities[] | select(.action == "comments_batch") | {
      action, required_params, optional_params, param_meanings,
      mode, available, default_limit, max_limit, response_fields
    }'

此次目录中,必填参数是 urls,模式为 sync,默认条数为 30,max_limit 为 200;目标数组按目标数计费。注意这里有两种数量:urls 的长度是笔记数,结果中的 count 是交付记录数。把 200 理解成“一次提交 200 篇笔记”,会在请求设计的第一步就弄错。

当前源码对 comments_batch 另有明确的输入检查:urls 必须是非空列表,最多 20 项,每一项都要能解析出笔记标识,并包含非空的 xsec_token 参数。超过目标数或缺少必要链接信息会走参数错误路径。这个 20 来自本次实现核对,不是从 max_limit 推算出来的。

链接里有 token,不等于服务端已经验证它仍然新鲜。当前参数检查可以判断标识和参数是否存在,却不能仅靠字符串判断目标此刻是否可交付。不要写一个“包含 token 就有效”的本地检查器,然后把它的通过率当成评论采集成功率。

第二步:把请求链接与笔记身份分开保存

请求时使用通过获准途径取得的完整笔记 URL。不要为了让地址更整齐,先删除查询参数再提交;也不要猜测、伪造访问参数或把一个目标的参数拼到另一个目标上。短链接和分享文案要先整理成该能力接受的笔记链接,不能假设接口会替你解析任意一段分享文本。

与此同时,去重不能只比较完整 URL 字符串。同一篇笔记的链接可能携带不同查询参数,按字符串去重会留下多个实际相同的目标。建议把“身份键”与“本次可用链接”分开:身份键由平台与笔记 ID 组成,链接则保留原值并记录取得时间。

  • 目标清单:笔记身份键、完整请求链接、来源渠道、取得时间,以及本次研究为什么需要它。
  • 去重清单:哪些输入指向同一篇笔记、最终采用哪条经过确认的链接;不只删除重复项而丢掉来源关系。
  • 待检查清单:无法解析的链接、缺参数的链接、同一身份出现冲突来源的链接。先处理这些项,不把它们混进付费批次。

完整链接只留在受控输入或任务存储中。日志、工单与导出报告通常只需要笔记 ID 和经过处理的来源引用,不应把时效访问参数到处复制。分享可阅读来源时,另外维护适合公开展示的链接,不改坏实际请求输入。

第三步:先用一条笔记验证请求与归属

下面沿用批量 action,但数组中只放一条获准笔记,便于核对评论是否确实属于该目标。假设服务端已经配置 EVERYINFRA_API_KEY 和 EVERYINFRA_XHS_NOTE_URL;后者应是实际的完整笔记链接,不是本文编造的占位链接。

单目标的批量请求模板;只提交一次
: "${EVERYINFRA_API_KEY:?请先配置 API Key}"
: "${EVERYINFRA_XHS_NOTE_URL:?请先配置获准的完整笔记链接}"
jq -cn --arg url "${EVERYINFRA_XHS_NOTE_URL}" '{
  platform: "xiaohongshu",
  action: "comments_batch",
  params: {urls: [$url], limit: 5}
}' | curl --silent --show-error --include --max-time 180 \
  'https://api.everyinfra.com/api/v1/social' \
  -H "Authorization: Bearer ${EVERYINFRA_API_KEY}" \
  -H 'Content-Type: application/json' \
  --data-binary @-

这段示例通过 jq 序列化 URL,不手工拼接 JSON;limit: 5 用于检查少量记录,客户端 180 秒超时也是示例设置。它不证明会返回五条,更不证明笔记只有五条评论。首次检查应确认响应外壳、评论 ID、note_id 或 note_url、空值与时间格式,再考虑增加目标。

第四步:切批时,不把 limit 当成每篇交付承诺

单目标核对后,可以将去重后的链接分成不超过当前约束的小批。先从较小批次开始,记录每批实际提交的目标,而不是立即把 20 项上限当成固定批量。上限说明请求允许到哪里,不说明你的等待预算、数据量或研究需要适合一次跑满。

这里存在一个必须讲清的实现细节:当前代码把 limit 用在每篇目标的采集设置中,随后还会按这个值截取最终合并列表。因此,向多个目标传 limit: 5,不能理解为“每篇都将交付五条”。即便某个目标不在 missed 中,最终返回的截断列表也未必保留它的记录。

如果你需要每篇笔记都能单独验收,初版可以按单目标请求组织任务,并分别核对返回结果;不要把“批量”理解为必须让所有笔记共用一个响应。若使用多目标批次,应逐目标验证结果归属,把数量语义加入接入验收,而不是写死“笔记数 × limit”作为应收条数。

批次的身份也应独立于 API 请求。你自己的 batch_id 表示一组业务目标,一次 API 返回的 id 表示一次调用。重试生成新的尝试记录,再关联回原批次,避免覆盖第一次响应,让问题失去追踪线索。

第五步:按目标解释 partial、missed 与 results

此能力的同步响应把评论列表放在 results,count 表示返回记录数量。部分目标没有交付时,响应会带 partial: true 与 missed;missed 用来标记未交付目标,不是“这些笔记确认没有评论”的名单。

整理结果时,先把每条评论按 note_id 或已确认的 note_url 映射回目标清单。映射不了的记录进入待核对区,不能为了填满报表而分给第一篇笔记。没有出现在 missed 中,也不等于已拿到该笔记全部评论;还有数量截取、字段缺失和未覆盖历史等边界。

下面是一个不联网的 JavaScript 合成例子,只演示目标级状态归类;note-a 等是虚构身份,不是有效平台 ID,也不是 API 响应。输入已在应用侧转换为笔记身份,真实使用时应先完成 URL 与 note_id 映射。

离线演示:有记录、明确未交付与尚未确认分开
function classifyBatch(targetIds, rows, missedIds) {
  const targets = new Set(targetIds);
  if (targets.size !== targetIds.length) throw new Error("duplicate target");
  const missed = new Set(missedIds);
  const counts = new Map(targetIds.map(id => [id, 0]));
  for (const id of missed) {
    if (!targets.has(id)) throw new Error("unknown missed target");
  }
  for (const row of rows) {
    if (!targets.has(row.note_id)) throw new Error("unmatched row");
    counts.set(row.note_id, counts.get(row.note_id) + 1);
  }
  return targetIds.map(note_id => {
    const count = counts.get(note_id);
    if (count > 0 && missed.has(note_id)) {
      throw new Error("conflicting target state");
    }
    return {
      note_id, count,
      state: count > 0 ? "records_observed"
        : missed.has(note_id) ? "not_delivered" : "unconfirmed"
    };
  });
}

console.log(classifyBatch(
  ["note-a", "note-b", "note-c"],
  [{note_id: "note-a", id: "synthetic-comment"}],
  ["note-b"]
));

第三个目标没有记录、也没有明确未交付标记,示例把它留下为 unconfirmed,而不是补成“零评论”。records_observed 同样只表示见到了记录,不是 all_comments_complete。让状态名称保留这层差别,后面的统计才不会不知不觉扩大结论。

第六步:评论去重与内容更新分两层

当前目录列出 id、text、like_count、posted_at、ip_location、author_name、author_id、sub_comment_count、note_id、note_url 和 platform。它们是字段声明,不保证每条记录都有值。保存前要检查真实返回形状,尤其是评论 ID 和所属笔记。

查看评论字段定义与尚未解释的字段
curl -fsS --max-time 30 \
  'https://api.everyinfra.com/api/v1/social/fields?platform=xiaohongshu&action=comments_batch' \
  | jq '{fields, undocumented}'

有稳定 ID 时,可以使用“平台 + 笔记 ID + 评论 ID”作为业务去重键,重复采到同一条评论时更新观察时间与可变字段。不要用昵称做键:昵称可能变化,也不是唯一标识。也不要仅凭文本相同就删除记录;不同用户可能确实写了同样的话。

若缺少稳定 ID,可以保留“作者标识 + 时间 + 文本摘要”等组合用于近似排重,但要标为低置信度,不把它当成可靠身份。互动量变化与文字修改也应和新增评论分开:同一评论的新版观察不是一条新评论。

sub_comment_count 是回复数量信息,不能单凭它声称已取得全部楼中楼正文。需要下级回复时,另按 sub_comments 的目录和实际返回验证。做时间分析时也要分开 posted_at 与你记录的 observed_at;解析失败不能悄悄替换为请求当天。

若把这套身份规则落在自己的 PostgreSQL 中,可用多列唯一约束表达组合身份,但要单独处理缺失 ID。官方文档指出,默认唯一约束下的 NULL 不按普通相等值比较;“加了 unique”并不自动解决身份不全的重复记录。这是本地存储设计示例,不涉及 API 的内部数据库。

第七步:按目标核对费用,只重试状态明确的目标

这类批次按提交目标计量,不是按一次 HTTP 调用或返回评论条数计费。提交前先去重,返回后保留 billing。当前实现对部分未交付目标有相应退款字段,包括适用时的 total_targets、billed_targets 与 refunded_credits;不要自己假设所有账单都具备这三个字段,也不要用 count 代替 billed_targets。

两个计数尤其不能混用:一篇笔记返回多条评论,仍然只是一个目标;一篇笔记返回不到预期条数,也不能直接按条数计算应退比例。核对时以此次响应和账单记录为依据,缺字段时标注无法从响应确认。本文没有给出固定单价,实际价格和账号条件以调用时说明为准。

重试前先决定失败属于哪一类。422 通常需要修改输入,例如修正链接或缩小目标数组;明确未交付的目标,只有在原因已检查、用途仍获准且重试有意义时才进入有限重试。不要把已经返回记录的目标再次混进失败批次。

网络超时与 missed 不同:超时意味着你没有拿到完整响应,不能确认本批哪些目标被执行。此时应保留未知尝试并查询调用记录或寻求核对,而不是自动重发全批。客户端 batch_id 不提供服务端幂等保证;非幂等请求的重试限制可参考 HTTP 官方规范。

拿到评论以后,再建立研究数据集

分析表最好能回到具体笔记与评论,但不需要保留所有可得字段。研究产品问题时,评论文本、来源引用、时间和主题标签可能已足够;昵称、作者标识与 ip_location 是否落库,应由实际目的决定,不能因为返回了就全部长期保存。

把模型判断放到另一层,并保留模型版本、分类规则版本与人工修正。对于反讽、否定、引用别人的话等样本,标签可能不稳定;生成报告时应展示可核对的例子与取样范围,而不是把模型分数包装成用户真实态度。

尤其不要用成功返回的部分评论作为未知全量的分母。一份可解释的报告会说明:本次检查多少个已去重目标、哪些有记录、哪些未交付或未确认、覆盖哪个观察窗口。它回答的是这份样本里的问题,而不是未经证明的全平台口碑。

范围与保存规则,应在扩大规模前确定

公开可见不自动解决内容权利、个人信息处理和再展示权限。你需要核对适用的平台规则与具体用途,确定谁能访问原文、保留多久、何时删除,以及是否有权将结果交给其他系统处理。本文不提供绕过登录、隐私设置或访问限制的方法。

正式批量运行前,至少检查一条可交付目标、一个输入错误、一次部分结果和一次未知状态恢复;空结果的业务含义应单独验证。如果还不能解释这些情况,就先保持小范围,不用扩大请求量来掩盖流程缺口。

保存范围还应延伸到诊断日志。OWASP 建议移除或处理访问令牌、会话标识和敏感个人数据;因此排错优先留下请求引用、目标数和状态摘要,不把完整评论批次当成默认日志字段。

一份可靠的批次,结束时应该留下什么

最后留下的不应只有一个评论文件,还应有去重后的目标清单、每次尝试的请求标识、目标级采集状态、评论身份与来源、实际计费记录,以及仍需复核的事项。这样下次运行才能明确接着处理哪些目标,而不是从头再抓一遍。

先用一篇笔记把这条链走完整,再扩成小批次。批量接口能减少重复的请求装配工作;真正让评论研究可信的,是你能说明每条记录的来处,也能如实说明没有拿到的部分。