Build in Public · LF-03
抖音评论数据怎么获取:从视频 URL 到保留回复关系的 JSON
从 douyin.comments 的实时参数开始,用最小请求读取评论,保留视频来源、评论 ID 与回复关系,并处理数量限制、空结果、时间解析和重复观察。
抖音评论导出成 JSON 以后,有一个问题很容易被忽略:文件里的一行,可能是独立评论,也可能是对另一条评论的回复。把它们都当成彼此独立的用户意见,会丢掉上下文;把点赞数、回复数和评论条数混在一起,又会让讨论热度的口径变得含糊。
因此,从视频 URL 获取评论,不能停在“接口返回了一个数组”。完整的接入流程需要确认视频对象、读取当前参数、保留评论身份与回复关系,并说明这次观察覆盖了什么、没有覆盖什么。本文用 EveryInfra 的 douyin.comments 逐步说明,适合准备将评论接入研究表、内容分析或人工审核流程的开发者。
本篇参数与字段说明依据 2026-09-04 公开目录及源码核对。请求模板没有在本轮执行付费业务;后面的合成 JSON 只用来演示归一化,不是任何真实视频、用户或评论样本。
先确定接入的是哪一种接口
如果你正在开发抖音授权登录、投稿或其他官方开放能力,应从抖音开放平台的接口列表进入相应流程,核对应用类型与权限。官方 access_token、参数和错误码属于官方接口契约,不能直接套到本文的 EveryInfra 数据请求中。
本文只讨论已经确定视频目标之后的评论读取。EveryInfra 的视频详情、评论、搜索和文字转写分别是不同 action;想分析“评论区在讨论什么”,不应拿视频标题或语音转写冒充评论正文。对私密、删除或受限制的内容,也不能因为有一个视频 URL 就假设仍然有权读取。
第一步:确认 comments 的现行参数
curl -fsS --max-time 30 \
'https://api.everyinfra.com/api/v1/social/catalog?platform=douyin' \
| jq -e '.capabilities[] | select(.action == "comments") | {
action, required_params, optional_params, param_meanings,
mode, available, returns_list,
default_limit, max_limit, response_fields
}'此次目录显示,douyin.comments 必须提供 url,采用同步返回,结果是列表,默认条数为 20,上限为 100。列表能力可以使用通用 limit;这些值约束请求规模,不是“必定返回一百条”或“视频评论最多一百条”。
这个 action 的当前声明没有提供可供调用方使用的 page、cursor 或 has_more,也没有公开 sort 筛选参数。不要为了实现“最新一百条”就自行添加 sort: newest;其他 action 支持的参数,不代表 comments 也支持。需要的排序或分页无法从契约确认时,应该明确需求暂未满足,而不是让程序静默降级成任意样本。
目录中的 response_fields 描述评论记录,不是分页协议。即使将来记录里新增某个名为 cursor 的字段,也要先确认它属于分页机制,才能用来推进请求,不能只靠字段名编写循环。
第二步:在请求前确定视频身份
输入应是该能力接受的完整视频链接。实际工作中,运营同事交过来的可能是一段分享文案、一个短链接,或者同一视频带不同查询参数的多个地址。先整理目标再调用,比把任何字符串都传给 url 更容易排错。
建议在业务层保存三项:用户最初提交的引用、经过确认的请求 URL,以及你已可靠识别的视频 ID。若无法可靠取得视频 ID,就暂用经过确认的来源引用做受控关联,不凭数字长度猜 ID,也不把分享短码当成视频身份。
同一个视频出现在多个研究清单中,可以复用同一次获准观察,但要保留它属于哪些研究任务。反过来,标题一样或出镜人物相同的两个视频,也不能自动合并。去重依据是内容身份,不是文案相似。
链接规范化只处理你已经确认无关的表示差异。不要一概删除查询参数;原始输入与实际提交值应分别留存。无法确认的重定向、内容不可访问或链接指向其他对象时,进入人工核对,而不是不断换形状重试。
第三步:用一个视频做最小请求
假设你已经在服务端配置 EVERYINFRA_API_KEY,以及获准视频的 EVERYINFRA_DOUYIN_VIDEO_URL。下面只发送一次请求,用少量结果核对字段,不启用自动重试,也不声称会返回满额。
: "${EVERYINFRA_API_KEY:?请先配置 API Key}"
: "${EVERYINFRA_DOUYIN_VIDEO_URL:?请先配置获准的视频完整链接}"
jq -cn --arg url "${EVERYINFRA_DOUYIN_VIDEO_URL}" '{
platform: "douyin",
action: "comments",
params: {url: $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 @-正常同步结果的外层用于描述请求,评论记录位于 results 中;同时需要关注 id、count、billing 等信息。把外层 id 记录为 API 请求标识,不要覆盖 results 内的评论 id。180 秒是这个示例的客户端等待设置,不是接口性能承诺;超时后只能先判定结果未知。
首次接入不要急着只保留 text。检查评论是否来自预期视频、ID 是否稳定、回复标志与关系字段是否出现,再把它接进数据库。如果响应不是可解析的 JSON,或业务对象不是预期列表,就保留失败证据并停止该次解析,不能返回一个空数组假装成功。
第四步:区分评论内容、互动与回复关系
curl -fsS --max-time 30 \
'https://api.everyinfra.com/api/v1/social/fields?platform=douyin&action=comments' \
| jq '{fields, undocumented}'当前声明包括 id、text、like_count、reply_count、author_name、author_id、ip_location、liked_by_author、is_reply、reply_to_id、posted_at 与 platform。可以按用途分为四组,而不是把所有列都强制填满:
- 身份与来源:id、platform,以及应用从本次请求关联的视频来源。作者昵称不充当评论身份。
- 内容与时间:text 与 posted_at;另外由你记录 observed_at,表示这次实际观察的时刻。
- 互动记录:like_count、reply_count、liked_by_author。缺失值不是零,作者点赞也不等于转化效果或商业价值。
- 回复关系:is_reply 与 reply_to_id。关系缺失时留待核对,不根据文本相似度或数组位置自动补父评论。
代码会把取得的评论和回复整理为列表,并在有依据时保留关联 ID。因此,数组本身是扁平的,并不意味着语义上所有行都位于同一级。读取时应先建立评论 ID 索引,再处理回复关系,而不是假设“下一行就是上一行的回复”。
reply_count 描述回复数量信息,不是已交付回复数组的长度保证。is_reply 为 true 但没有 reply_to_id,或 reply_to_id 指向本次结果里不存在的评论,都可能成为需要保留的关系缺口。不能用虚构父评论把树补齐;报告也不应因为树能画出来就声称线程完整。
第五步:设计不会丢失身份的业务 JSON
视频来源应从已经确认的请求上下文关联到记录,不依赖评论里必然再返回一份视频 URL。数据库可使用“平台 + 视频身份 + 评论 ID”的组合作为键;同一评论再次出现时追加观察或更新可变字段,而不是重复计入新增量。
ID 建议按原始字符串保留。如果把很长的十进制 ID 先转成 JavaScript Number,再转回字符串,可能已经无法恢复原值。JSON 规范说明了数字互操作的精度边界;对于不需要算术的标识符,保持字符串可以避免无意义的数值转换。
下面的 JavaScript 是一个刻意收窄的离线转换器:仅接收已确认是字符串的评论 ID;不认识的类型拒绝自动入库。它只保留本文需要的字段和关系状态,不是完整 SDK,也没有发起网络请求。合成文本明确写为“合成示例”,不能当作真实用户意见使用。
function mapComment(row, source) {
if (!row || typeof row.id !== "string" || !row.id.trim()) {
throw new Error("string comment id required");
}
if (row.reply_to_id != null && typeof row.reply_to_id !== "string") {
throw new Error("string parent id required");
}
const parentId = row.reply_to_id || null;
const isReply = typeof row.is_reply === "boolean" ? row.is_reply : null;
const relation = isReply === true
? parentId ? "reply_with_reference" : "reply_parent_unknown"
: isReply === false && !parentId ? "top_level"
: "needs_review";
return {
platform: "douyin",
video_ref: source.video_ref,
comment_id: row.id,
text: typeof row.text === "string" ? row.text : null,
posted_at_raw: row.posted_at ?? null,
observed_at: source.observed_at,
like_count: typeof row.like_count === "number"
&& Number.isFinite(row.like_count) && row.like_count >= 0
? row.like_count : null,
reply_to_id: parentId,
relation
};
}
console.log(mapComment(
{
id: "synthetic-reply",
text: "合成示例,不是真实评论",
is_reply: true,
like_count: null
},
{
video_ref: "synthetic-video",
observed_at: "2026-09-04T00:00:00Z"
}
));这个例子保留 posted_at_raw,暂不强行转换时间。真实入库时,可另设解析后的时间、所用时区和解析状态;无法确认单位或格式时不要自动猜成秒、毫秒或本地时间。缺失文本同样保持 null,不让模型“补全原话”。
reply_with_reference 只表示有父项引用,仍需在同一视频的已取得记录中检查父项是否存在;不存在就保留外部或未取得引用。top_level 也只是依据当前字段作出的分类,并非证明原平台讨论结构已全部还原。
确认来源时间的单位和含义后,可以按 RFC 3339 记录解析后的时间,并保留 Z 或明确的 UTC 偏移。规范解决的是表示格式,不会替你判断一串来源数字是秒还是毫秒,也不能把未知来源时区补成当前机器时区。
第六步:把定时观察与全量分页分开
当前公开请求没有可用的分页推进契约,所以不断重复同一个请求,得到的是多次观察,不是第二页、第三页。就算几次结果恰好不同,也不能据此断言已经遍历了评论区。
建立定时任务时,可以按评论身份合并相邻观察,并保留首次、最近一次见到它的时间。迟到评论、互动量变化和回复关系补充都可以更新,但“本轮没再见到”不等于“评论已被删除”。除非有明确删除信号或单独核对依据,否则只记录这次未观察到。
如果业务需要比较两个时间段,先对齐视频集合、调用参数、观察频率和成功覆盖。前一周检查全部目标、后一周只有少数目标成功时,原始评论数量不能直接作为热度下降的证据。把覆盖差异写进报告,往往比再增加一种情绪标签更重要。
还要明确统计单位:是在数评论行、讨论线程,还是去重作者。一个线程中多人回复,与一个人连续留言,是不同的讨论结构。报告中“独立用户”不能只由行数代替;缺少可靠作者标识时,也不应宣称已经统计独立人数。
第七步:错误、空结果与费用各自核对
处理失败时至少区分输入不合法、鉴权或权限失败、额度不足、暂时服务失败、返回空列表和客户端没有拿到完整响应。它们对应的行动不同,统一写成“暂无评论”会同时误导用户和监控。
对于 422,先按 error.code 与提示修正请求,不要无次数上限地重试。对于暂时性问题,先确认原请求是否可能已经执行,再决定是否重试;超时的 POST 不能默认视为未提交。对已经明确不可访问的内容,停止自动尝试并复核范围,不寻求绕过限制。
账单也要关联同一次请求。记录实际 billing 与 API 请求 id,不拿抓到几条评论直接计算应付金额。数据解析失败是你本地处理的失败,不能单凭它推断服务端已执行退款;同样,响应里没有找到计费字段时,应标记待核对,而不是自行写成免费。
将评论交给模型前,保留上下文和人工回看入口
评论分类最好以线程或有明确边界的片段为输入。若一条回复只写“并不是”,单独看这三个字无法知道它反对什么;父评论未取得时,应保留上下文不足的状态,而不是强行得到一个正面或负面标签。
派生结果单独保存,例如 topic、sentiment、判定规则版本、模型版本与人工修正。不要把这些字段写回原评论事实列,也不要把模型给出的置信度解释为对用户真实态度的测量。报告保留足够的来源关联,供有权限的人回看;公开材料仅呈现确有权展示的必要内容。
扩大规模之前的检查清单
- 目标:视频身份与请求 URL 对应,范围与用途经过确认,没有私密内容绕过步骤。
- 结构:results 是预期列表,评论 ID 未丢精度,记录能关联回具体视频。
- 关系:回复字段保留,父项缺失与未知状态可见,没有补造线程。
- 观察:缺失不补零,发布时间与观察时间分开,不宣称无依据的全量覆盖。
- 运行:超时不自动重发,错误与账单留有请求标识,重复观察不会重复计为新增。
- 保存:只留必要字段,原文和作者信息有访问控制、保留期限与删除安排。
为适配后的业务 JSON 写 schema 时,留意“字段被描述”“字段必须出现”和“值允许为 null”是不同条件。JSON Schema 的 properties 默认不强制字段出现;用 required 和类型约束分别表达你的要求,不能用一个空字符串替所有未知值过关。
交付的是一份可解释的数据集,不只是 JSON 文件
一份能继续使用的评论数据集,应当让接手的人看懂它的目标范围、来源、观察时间、回复关系和缺失状态。做到这些,JSON 才能可靠地进入分析、审核与报告流程;否则,即使导出了很多行,也只是把原先的疑问换了一种文件格式保存。
从一个获准视频开始,先验证结构与关系,再逐步增加目标。下面的文档与实时目录可以帮助你确认当前能力;不要用旧文章中的参数替代接入时的核对。