做抖音解析时,我差点把别人的视频数据写进去

一篇关于「让模型读懂一条视频」的踩坑记录。代码开源:github.com/taoyao-dev/agent-senses


前言:一个很蠢的起点

我给模型发了一条抖音链接,它试了半小时,一个字也没读出来。

不是它笨。它能读 mp4,但视频里没有字幕轨,它没有耳朵也没有眼睛------能拿到文件,不等于读懂内容。

于是我想写个东西:一个链接丢进去,返回文案、作者、互动数、逐句转写。做成 MCP 服务,Claude Code 挂上就能调。

写的过程中踩了四个坑,有一个差点让我把别人的数据写进去。记下来。


一、先要分清:这是「下载器」还是「理解器」

市面上做抖音解析的开源项目不少,大多是下载器:给个链接,返回一个 mp4。

但 agent 要的不是文件,是内容。文件只是中间产物。所以设计目标从一开始就不是「下得下来」,而是「读得懂」:

复制代码
链接 → 元数据(结构化) → 音频 → 转写(逐句带时间戳) → 一个对象

顺带一个反直觉的发现:yt-dlp 在抖音这条路上其实不好走。它需要新鲜的 cookie,而现在的 Chromium 系浏览器把 cookie 用 App-Bound 加密了,程序读不出来。绕开 cookie 反而是更短的路。


二、整体链路

scss 复制代码
短链 ──302──> video_id ──Playwright(系统Chrome)──> 媒体流URL + detail接口JSON
                                                      │
                                          带 Referer 下载 → ffmpeg 合流 → ASR

用 Playwright 驱动系统里已经装好的 Chrome (channel="chrome"),不用再下一份 chromium,启动快、指纹也更像真人。然后在页面加载过程中挂一个 on_response 钩子,既收元数据也收媒体地址:

python 复制代码
def on_response(resp):
    u = resp.url
    if not detail and any(k in u for k in _AWEME_API):
        ad = _pick_aweme(resp.json(), video_id)
        if ad:
            detail.update(ad)
    elif "/video/tos/" in u and "video" not in streams:
        streams["video"] = u

下面四个坑,都是在这个钩子上磨出来的。


三、坑一:别从 DOM 抠文本

第一版是解析播放页的 DOM,[data-e2e="video-desc"] 抓标题、.account-name 抓作者。能跑,但有两个问题:

  1. 拿不到精确互动数(点赞/评论/收藏),页面上显示的是「1.2万」这种四舍五入
  2. 前端一改版就崩

改成拦接口以后,/aweme/v1/web/aweme/detail/ 直接返回结构化的 aweme_detail,字段全、数字是原值、不怕改版。

但 detail 接口不是每次都会响------页面复用了前端缓存的时候请求不会重放。这时候整页 reload() 一次再等,通常能逼出来。


四、坑二(重点):播放页会轮播「下一条」

这个是差点写错数据的那个。

思路很自然:接口拿到了就用接口的,接口缺字段就用 DOM 补。听起来没毛病,直到我发现------抖音的播放页会自己往下轮播。等我的钩子收完数据、再去 DOM 抠的时候,页面可能已经滑到了下一条视频。

于是「DOM 兜底」补进来的标题和作者,是别人的。

我印象很深的一个例子:一个熊出没的 id,打开来 DOM 显示的是另一个电影的名字。而且这个错误不会报异常、不会为空------它安安静静地给你一份看起来很正常、但属于别人的数据。这种 bug 比崩了可怕得多。

两层防御,都很朴素:

第一层,接口这头按 id 校验。 post、related 这些接口里装的是一堆别人的视频,不校验就会把隔壁的当成这条:

python 复制代码
def _pick_aweme(j, video_id):
    """从接口响应里捞出「就是这条视频」的 aweme 对象。"""
    ad = j.get("aweme_detail")
    if isinstance(ad, dict):
        return ad if str(ad.get("aweme_id")) == video_id else None
    for key in ("aweme_list", "data"):
        for x in j.get(key) or []:
            if str(x.get("aweme_id")) == video_id:
                return x
    return None

第二层,DOM 这头做一致性比对。 只有当 DOM 里那段文案和接口返回的文案开头一致,才允许用 DOM 补字段:

python 复制代码
desc_api = detail.get("desc") or ""
desc_dom = dom.get("desc") or ""
same_video = bool(desc_api) and (not desc_dom or desc_dom[:15] == desc_api[:15])

item.author = (detail.get("author") or {}).get("nickname") or ""
if not item.author and same_video:      # 只有确认是同一条,才敢用 DOM
    item.author = dom.get("author", "")

宁可字段为空,也不要填错。 空值调用方一眼能看出来,错值会被当成真的用下去。


五、坑三:whisper 的中文会在简繁之间跳

faster-whisper 转中文,同一条音频里能出现「吃點東西」和「宝头强」并存------繁体简体混着来。

第一反应是喂 initial_prompt 引导它输出简体。实测没用,反而会让别的词念错。 这条路放弃了。

最后是事后转一遍,用 opencc 的 t2s:

python 复制代码
try:
    from opencc import OpenCC
    _S2T = OpenCC("t2s")
except Exception:          # 没装就原样返回,不阻断转写
    _S2T = None

def to_simplified(text: str) -> str:
    return _S2T.convert(text) if _S2T else text

顺带说清楚定位:这是速记,不是精校。人名、专有名词、口音重的地方都会错。引用前自己核对------README 里也是这么写的,别拿它当字幕。


六、坑四:抖音有两套媒体流

同一个站,两种播放器:

  • 新版 :直接托管合成好的 mp4,路径里带 /video/tos/,自带音轨
  • 旧版 :DASH 拆开,/media-video 和 /media-audio 两条

代码里两套都认,命中合成流就跳过 ffmpeg 合流------省一次转码,也省掉合流失败的一整类错误:

python 复制代码
muxed = "/video/tos/" in streams["video"]
vpath = _download(streams["video"], wd / "video.mp4")
apath = vpath if muxed else _download(streams["audio"], wd / "audio.m4a")
...
full = vpath if muxed else media_mod.merge(vpath, apath, wd / "full.mp4")

还有个隐性的坑:分片 URL 带时效签名,抓到手里必须立刻下。隔几分钟再下就是 403。所以下载和抓取之间不能插任何耗时的活儿。


七、结构上:核心库和 MCP 外壳分开

python 复制代码
def fetch(url, *, want_video=True, want_transcript=True) -> Item:
    ...

所有能力都在 senses/ 里(纯函数,不依赖 MCP),server.py 只是一层很薄的外壳,把 fetch 注册成 douyin_get 工具。

这么切的好处很实际:MCP 协议改了,或者以后想接别的 agent 框架(LangChain、自己的 CLI、定时任务),只换外壳,核心库一行不动。 顺便也让测试好写------测 senses 不需要起 MCP 服务。


八、边界,写在明面上

  • 只读公开内容,不碰登录态,不做任何账号操作
  • 只开源「看」的部分:解析、搜索、抓取、转写。「动手」的部分不开源------发帖、评论、读私信这些一旦公开风控细节,工具几天就死
  • 转写是速记不是精校,引用前核对
  • 现在只有抖音。B站、小红书还没做,接口形状不一样,慢慢加

九、最后

项目在 GitHub 上,MIT:

github.com/taoyao-dev/...

MCP 服务,Claude Code 直接挂:

bash 复制代码
claude mcp add agent-senses --scope user -- python "<本目录>/server.py"

它现在有两副「器官」:读抖音,和搜/抓网页。我想做的事其实就一句话------模型不缺脑子,缺感官。中文内容平台这块,大厂不屑做、老外做不了,偏偏每个用中文的 agent 都缺。

如果你也在做类似的东西,评论区聊聊你踩的坑。尤其是「接口和 DOM 打架」这类问题,我挺好奇别人怎么处理的。

相关推荐
jerrywus3 小时前
普通人怎么用 Codex:从表格、文件和小工具开始
chatgpt·agent·mcp
骑着蜗牛撵大象3275 小时前
Spring Boot 4.1 + Spring AI 2.0:RAG 供文、图谱供关系、MCP 供工具的 Java Agent 平台
java·spring boot·spring·微服务·知识图谱·rag·mcp
情 九7 小时前
MCP 实战 07:让 AI 查询 SQLite,只开放一个查询入口
mcp
骑着蜗牛撵大象32719 小时前
2026 后端 AI 工程化:Spring AI 管调用、MCP 管工具、Agent 内嵌管自治
java·人工智能·后端·spring·ai·可观测性·mcp
Danny转行做跨境20 小时前
亚马逊选品接 MCP 到 Claude Code 的四个坑
跨境电商·mcp·sorftime
只睡四小时2 天前
MCP 弃用 SSE?Node 分帧漏 81.7% 事件
node.js·webassembly·实时通信·mcp·streamable http
跨境Jacky2 天前
选品数据采集,MCP比爬虫稳在哪?亚马逊到1688实测
跨境电商·mcp·sorftime
vjmap2 天前
Cesium + MCP:在 Cursor、Claude Code 里一句话生成地图视频,唯杰 AI 地图动画上线
gis·cesium·mcp
ynchyong2 天前
MCP Server 与 Function Calling
functioncalling·mcp·mcp server