Files
douyin_video_download/PROGRESS.md
whale-fall-code 2bb83f3685 初始提交:抖音视频批量采集流水线
App 端免签名接口(设备注册 + detail + CDN 直链),一体化流水线
下载→抽音→传COS→记录→删本地,AI 作品四档判定,四份文档:
README(使用指南)、PROGRESS(批次进度)、RE(逆向全过程)、AI(判定规则)。

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-29 16:31:10 +08:00

23 KiB
Raw Blame History

任务进度 / 踩坑记录

项目介绍和各代码文件说明见 README.md。本文档记录接口踩坑、 各批筛选与下载进度、已知问题与待办,随批次滚动更新。

抖音视频批量采集 → 音频提取 → COS 上传

把一批 douyin.com/video/... 链接跑成 ASR 可用的数据集: 下载无水印 mp4 → 抽 mp3 → 探时长 → 传腾讯 COS → 出清单(jsonl / xlsx)。

全程纯 Python,不依赖第三方解析服务、不依赖 JS 运行时、不需要登录。


1. 结论先行:接口怎么走通的

这些结论的验证过程和完整时间线见 RE.md(逆向全过程复盘)。 这里只放结论,改代码前先看。

路线 状态 说明
web /aweme/v1/web/aweme/detail/ 要登录 匿名请求返回 HTTP 200 空 body。真实原因在响应头 X-Whale-Throughput-Abort-Data(base64 JSON):{"content":"anonymous","name":"强制登录"}。跟 a_bogus 签名对不对无关——伪造的随机 a_bogus 和完全不签名,表现一模一样。
App /aweme/v1/aweme/detail/ 可用,且不用签名 先打 /service/2/device_register/ 拿 device_id / install_id,带上就能取详情。不需要 X-Gorgon、不需要 Cookie、不需要登录。 这是本项目默认路径。
CDN play_addr 直链 免鉴权 aweme.video.play_addr.url_list 里的 douyinvod 地址没有防盗链,裸 requests 直接下。
/aweme/v1/play/?video_id={uri} 免鉴权 302 跳 CDN,零鉴权。下载走的就是它。

两层限流(跑批量必然撞上)

  1. 设备级 detail 配额:单个 device_id 约 575 次 detail 调用后被限,表现为 HTTP 200 空 body。 → douyin/batch.py 的 DevicePool 到配额就换设备;用 generation 标记当前设备,避免多个 worker 同时发现失败把设备连换好几次。
  2. IP 级注册配额:短时间内注册约 11~12 次后,所有 register host 一律返回 device_id='0'。 → Device.register() 指数退避重试(默认 5 次,30s 起步翻倍),让批处理停下来等,而不是空转着把剩余任务全标成失败。

第 2 条是踩过的坑:早期没有退避,一次注册受限直接烧掉 678 条任务。

作品拿不到时怎么区分

看 filter_detail.filter_reason:

  • images_base → 图文作品(图集),本项目不处理,抛 NotAVideo
  • status_self_see / status_audit_self_see → 仅作者自见,实际不可用

2. 目录结构

├── pipeline.py             【核心】一体化流水线:下载→抽音→上传→记录→删本地
├── scripts/
│   ├── clean_ai.py             从链接 jsonl 里筛出 AI 相关作品,出链接清单
│   ├── jsonl_to_xlsx.py        jsonl → xlsx(认 records/records.jsonl 和 records/manifest.jsonl 两种)
│   └── export_sample_xlsx.py   records.jsonl → 交付用样本 xlsx(支持 --ai-only / --limit)
├── tests/
│   ├── test_smoke.py           离线冒烟测试(不联网)
│   ├── test_meta.py            标题/标签拆分、记录追加的测试
│   ├── test_aiclass.py         AI 判定的测试(用例全取自真实数据)
│   └── test_pipeline.py        流水线端到端测试(ffmpeg 真跑,网络和 COS 打桩)
│
├── douyin/
│   ├── app.py              【核心】设备注册 + App 端 detail
│   ├── batch.py            并发批处理、DevicePool 设备轮换
│   ├── download.py         带 Range 续传的下载、去重 Registry
│   ├── meta.py             标题/标签拆分、records.jsonl 追加写
│   ├── aiclass.py          判定作品是不是 AI 相关,分四档,见 §4.5
│   ├── cos.py              COS 上传 + 上传后大小校验
│   ├── audio.py            ffmpeg/ffprobe 薄封装:找可执行文件、拼命令、探时长
│   ├── urls.py             短链还原、aweme_id 提取
│   ├── client.py           Aweme/DouyinError 等共用类型 + web 后端请求路径
│   ├── signer.py           a_bogus 签名(仅 web 后端用,当前跑不起来,见下)
│   └── crypto.py           SM3 / RC4 / 自定义 Base64(未完成的纯 Python 签名器的积木)
│
├── data/                   源数据(外部抓来的,clean_ai.py 的输入。删了没法重新筛,别动)
│   ├── 抖音链接20260827_40087.jsonl      40087 条
│   ├── 抖音ai视频_20260831_87110.jsonl   87110 条(其实是露营帐篷爬取,AI 占 3.3%)
│   ├── 抖音ai视频_20260901_57261.jsonl   57261 条(同上,AI 占 2.8%)
│   ├── 抖音ai视频_20260902_42391.jsonl   42391 条(9 月 AI 批,AI 占 90.9%)
│   ├── 抖音ai视频_20260904_62171.jsonl   62171 条(同上,AI 占 88.3%)
│   ├── 抖音ai视频20260908_118469.jsonl   118469 条(同上,AI 占 95.1%)
│   ├── 所有作品.csv                      4839 条,字段与 jsonl 不同,见 §4.5 第 8 条
│
├── records/                流水线产物与断点记录
│   ├── records.jsonl           全字段元信息(含 raw),断点续跑靠它
│   ├── pipeline_failed.txt     失败清单:aweme_id / 阶段 / 原因(重跑据此续传)
│   └── manifest.jsonl          老产物:第一批 5393 条的 3 字段清单,见 §5
│
├── exports/                交付/导出产物(xlsx / csv)
├── lists/                  各批的链接清单,也是"哪批下了什么"的凭据
└── logs/                   各批跑批日志
`work/` 是流水线的临时目录,跑的时候自建、传完即删,仓库里不留。

**web 后端目前是跑不起来的,别把它当后路。** 具体状态:

- `client.py` 里 `Aweme` / `DouyinClient` / `DouyinError` 这几个类型被 `app.py`、`batch.py`
  正常用着,**不是死代码**;只有它的 web 请求路径闲置。
- `signer.py` 两个后端都不可用:`NodeSigner` 要 `node_signer/sign.js`,**这个目录不在项目里**;
  `ABogusSigner` 是纯 Python 实现,它自己的文档写着"剩字节拼装顺序待从 ab.js 逆出来",没写完。
- `crypto.py` 的 SM3 / RC4 / 自定义 Base64 对过官方测试向量(`test_smoke.py` 每次都验),
  但它只是那个**没写完的** `ABogusSigner` 的积木,`signer.py` 并不 import 它。

所以真要走 web 端,得先补 `node_signer/`(或者把 ab.js 逆完)**再**准备登录 Cookie。
App 端被封的话这里没有现成退路,得先做上面这件事。

**本地媒体文件已全部删除**(`downloads/` 的 5393 个 mp4、`audio/` 的 5393 个 mp3),东西都在 COS 上。旧的分阶段脚本依赖本地文件,要用得先重新下载;`pipeline.py` 不受影响——它本来就是边跑边删。

输入链接文件 `all_video_links(1).txt` 也已不在,重跑需要重新提供。

---

## 3. 环境

```bash
pip install -r requirements.txt

另需系统里有 ffmpeg / ffprobe。实测环境:Python 3.11.0 + ffmpeg 2025-07-31 (gyan.dev full build)。

COS 凭证走环境变量,别写进命令行(会留在 shell history 里):

$env:COS_SECRET_ID  = "..."
$env:COS_SECRET_KEY = "..."

4. 一体化流水线(推荐)

$env:COS_SECRET_ID="..."; $env:COS_SECRET_KEY="..."

# 先小批试跑,加 --keep-local 不删本地文件,人工看一眼产出
python pipeline.py --from-file links.txt --region ap-chongqing --limit 20 --keep-local

# 确认没问题再放开
python pipeline.py --from-file links.txt --region ap-chongqing --workers 8

# 出 xlsx(--columns full 带上标题、作者、标签、发布时间)
python scripts/jsonl_to_xlsx.py -i records/records.jsonl --columns full -o "exports/抖音-四川话.xlsx"

# 出交付样本 xlsx(--ai-only 只导 AI 档,--limit 限量)
python scripts/export_sample_xlsx.py --ai-only -o "exports/AI视频样本.xlsx"

每条视频端到端走完 9 步才算完:下载 → 抽 mp3 → 探时长 → 校验 → 传两个对象 → 核对大小 → 写 record → 删本地。磁盘峰值只有 workers × (mp4+mp3), 约 100 MB,与总量无关——分阶段跑得先摊 53 GB。

元信息全进 records/records.jsonl,一行一条,raw 内嵌,字段见下。断点续跑靠它, 重跑同一批会跳过已完成的(连下载都不会发生)。COS 上同名对象一律覆盖。 pipeline.py 的 --records / --failed 默认就指向 records/ 目录。

两道闸门

删本地文件是唯一不可逆的操作,所以卡两道,任一不过就保留文件、记 failed、继续下一条:

  1. 抽取闸门 —— mp3 用 ffprobe -count_frames 实际解码算时长,与 mp4 比对, 差超过 max(1s, 2%) 判定截断。 注意不能用 format=duration:它读的是 LAME 的 Xing 头,声明的是原始总帧数。 实测把 mp3 截断到 20%,format=duration 报的仍是完整的 10.0078s,一字不差; ffmpeg -f null - 同样拦不住(rc=0、无 stderr)。只有实解码能发现。
  2. 上传闸门 —— 不信 upload_file 没抛异常,而是 head_object 取回 Content-Length 跟本地字节数比对,两个对象都对上才删。

records.jsonl 字段

{"aweme_id":"...","title":"剥掉标签的纯标题","desc":"服务端原样",
 "tags":["..."],"author":{"nickname":"...","uid":"...","sec_uid":"..."},
 "create_time":1582635538,"create_time_str":"2020-02-25 21:38:58",
 "duration_sec":41.702993,"duration_ms_api":41750,
 "stats":{"digg":0,"comment":0,"collect":0,"share":0,"play":0},
 "music":{...},"cover_url":"...","video":{"uri":"...","width":1280,"height":720},
 "cos_mp4":"cos://...","cos_mp3":"cos://...","size_mp4":0,"size_mp3":0,
 "source_url":"...","fetched_at":0,"raw":{ ...服务端原始 JSON... }}

三条要注意的:

  • aweme_id 必须是每行第一个字段。续跑扫描用正则从行首抠 id,不解析整行 (几百 MB 的文件差别是 ~1 秒对十几秒)。重排字段不会报错,只会让启动悄悄变慢。
  • duration_sec 和 duration_ms_api 不是一回事。前者是 ffprobe 实测,后者是 服务端声明值,实测同一条 41703 vs 41750 ms。切分要用 duration_sec。
  • tags 取自 desc 的正则全集,不是 text_extra。实测 12 条样本里有 2 条的 标签在 desc 里有、text_extra 里没有,以后者为准会静默丢标签。

raw 内嵌导致单行几十 KB、整个文件十几 GB,所以读取一律逐行流式, jsonl_to_xlsx.py / export_sample_xlsx.py 已经改成流式了。


4.5 从链接 jsonl 里筛 AI 作品

判定规则的逐条详解(词表选词标准、误收数据、否决清单、维护指南)见 AI.md。

clean_ai.py 吃的是外部抓来的链接 jsonl(带 desc / hashtags / author / ai 等字段), 判定逻辑在 douyin/aiclass.py,分四档,取最强的一档:

档 依据 含义
platform ai.is_aigc 抖音自己打的 AIGC 标
strong 文案/标签有 AIGC、AI 生成工具名、ai+创作类词、AI 赛事标签 作品声明是 AI 做的
topic 只提到 AI 本身(ai 教程、人工智能、大模型、ChatGPT) 在讲 AI,不一定是 AI 做的
author 只有作者昵称或音乐标题带 AI 最弱,会误收
python scripts/clean_ai.py -i 抖音链接xxx.jsonl --stats                       # 先看分布
python scripts/clean_ai.py -i 抖音链接xxx.jsonl --tier platform,strong -o ai   # 出 ai.jsonl + ai.txt
python pipeline.py --from-file ai.txt --region ap-chongqing            # 直接接流水线

要注意的(都是从真实数据里挖出来的,改规则前先看 test_aiclass.py):

  1. platform 是唯一能抓到"作者没标但确实是 AI"的信号。 实测 1744 条被平台标了 AIGC,文案标签里一个 ai 字都没有(脑筋急转弯、历史解说、原创动画)。纯关键词过滤 会整片漏掉。反过来 9000+ 条有明确 AI 信号却没被平台标——两路互补,都得要。
  2. Al(大写 A + 小写 L)冒充 AI。 标签 抖音al 出现 129 次,文案里也有 #Al创作。 不归一化就漏。反过来 ai 是 email/detail/again/hair 的常见片段,只能认前后不接 字母数字的独立 ai。
  3. 音乐标题默认是 @<作者昵称>创作的原声,40087 条里 32555 条是这个壳。昵称带 AI 会连"创作"一起命中,等于让作者昵称从 music 这条路偷偷升到 strong——实测 771 条 这样混进来。现在先剥壳,剥完只当弱信号(全库剥完只剩 16 条还有命中,本来就没价值)。
  4. 中文泛词不单独成立。 动画/绘画/分身/动漫 必须跟 AI 词同现,否则 830 条 #原创动画 全被误收。剪映(375)/关键帧(314)/蒙版(203)/转场 是剪辑术语,不是 AI。
  5. 工具名不含 ai 字样的一大票:即梦 / seedance / libtv / flovaai / 小云雀 / runninghub / oiioii / shotlab / veo3,只能靠名单,名单要跟着平台活动更新。
  6. 一批工具名本身是常用词,必须要求跟 ai/aigc 同现(见 _AMBIG_GEN / _AMBIG_CHAT)。 实测误判:豆包 224 条(狗名、豆沙包)、万相 17 条(《龙神万相》)、海螺 13 条 (三峰的"海螺"帐篷)、元宝 11 条(316 不锈钢元宝勺)、星流 1 条("明星流量"子串)、 清影 1 条("起舞弄清影")。而真命中的无一例外都同时带 AI 字样,所以这道门槛 不掉召回——旧文件重跑 15218 → 15204,掉的正好是这些。
  7. 英文工具名要挡词中词,但只挡字母,不挡数字和中文。 udio 藏在 Grass Studio 里 17 次、flux 藏在 Fluxxwave 曲名里。可是数据里满地都是 Seedance2.0、sora2、 runninghub0.21、libtv创作者、flovaai超创,一刀切加  会丢 3500+ 条真命中。
  8. 别放开"数字人"/"换脸"/"分身"当独立信号。 数字人民币含"数字人"、微信分身含"分身"、 穿搭视频也说"这套穿搭能换脸"。实测放开只进误判,一条真的都捞不到。 同理否掉的还有:lora(撞 valorant)、tripo(撞 tripod 三脚架)、seaweed(海藻)、 度加("亏损幅度加大")、万彩("29万彩礼")、生数("诞生数副名场面")、节点("关键节点")、 抽卡(原神)、捏脸(燕云十六声)、国漫(《剑来》真人制作)、对口型(真人对口型)。 这 8 条否决理由都写成了测试,见 test_rejected_candidates_stay_none。
  9. AI 还有第三种冒充写法:罗马数字。 #AⅠ漫剧 用的是 U+2160,跟 Al 一个套路。
  10. csv 的原声标题多一截 - 昵称(@X创作的原声 - X),剥壳正则要一起覆盖, 否则第 3 条那个作者昵称升档的 bug 会在 csv 输入上复发。

platform 档要不要求文案佐证

抖音给 AI 配音 / AI 封面的普通带货视频也打 AIGC 标。露营帐篷那两批里, platform 档有一半(2045 条)文案和作者昵称都零信号,点开是"四脚伞帐篷带广告位" "抖音私密账号是什么"。--platform-needs-text 就是卡这个的,作者昵称带 AI 也算佐证。

但这个开关会误伤一类真 AI:@栖光 那种做 AI 古风美图的,文案只写 #古风 #国风古韵 #意境,一个 AI 字都不会写。这类只有平台标签能抓到。 所以无佐证的那批不是垃圾,得按"是不是带货"再分一刀(见 lists/platform无佐证_*.txt)。

抖音的 AIGC 字段:认 risk_infos.type,别认 is_aigc_media

raw.risk_infos.type 是数字码,一一对应:

type content 含义
44 作者声明:内容由 AI 生成 作者主动声明,19946 条
47 疑似使用了 AI 生成技术,请谨慎甄别 平台检测,措辞不确定,515 条
76 / 328 作品含 AI 生成内容 平台判定,448 条

**这几个才是 AI。**下面这些是别的免责声明,跟 AI 无关:

type content 条数
45 作者声明:虚构演绎,仅供娱乐 868
336 / 351 个人观点,仅供参考 111
54 作者声明:内容来源于网络 13
335 作者声明:内容含营销推广信息 10

已知 bug:爬虫那层把「risk_infos 有内容」就当 is_aigc=true,把上面这些 免责声明也算进 platform 档了,全库误算 1029 条。修法见 §7。

另外 raw.is_aigc_media 全库 24611 条全是 false,名字最像却完全没用,别信。

各批筛选结果

源文件 总条数 AI 占比 出的清单
抖音链接20260827_40087.jsonl 40087 39.1% ai_videos.txt 15218
抖音ai视频_20260831_87110.jsonl 87110 3.3% ai_87110_严格.txt 1366
抖音ai视频_20260901_57261.jsonl 57261 2.8% ai_57261.txt 776
抖音ai视频_20260902_42391.jsonl 42391 90.9% ai_42391.txt 37703
抖音ai视频_20260904_62171.jsonl 62171 88.3% ai_62171.txt 52360
抖音ai视频20260908_118469.jsonl 118469 95.1% ai_118469.txt 106447
所有作品.csv 4839 未筛 csv_links.txt 4839(全量下的)

前两个文件(87110 / 57261)名字叫"ai视频",实际是露营帐篷带货爬取 (标签 TOP:户外露营 10568、帐篷 9346),AI 只占 3% 左右。9 月的三批 (42391 / 62171 / 118469)才是真 AI 池,且 platform 档占绝对大头——这三批 allow_download=false 里 aigc_forbidden 多达 2.8 万 / 3.7 万 / 7.5 万条, 但 App 端接口不受此限(见下)。拿到新文件先跑 --stats 看一眼,别信文件名。

aigc_forbidden 那道限制只作用于网页端,App 端接口不受影响——已用 20 条探针 验过,9 条 web 侧 allow_download=false 的全都正常下下来了。不用 --exclude-forbidden。

lists/ 里各清单的关系

清单 条数 是什么
all_video_links(3).txt 3557 第一批输入
ai_videos.txt 15218 40087 批筛出的 AI
csv_links.txt 4839 所有作品.csv 全量(未筛)
ai_87110_严格.txt / ai_57261.txt 1366 / 776 露营两批筛出的 AI
ai_42391.txt / ai_62171.txt / ai_118469.txt 37703 / 52360 / 106447 9 月三批筛出的 AI(platform+strong 档)
ai_合并待下载.txt 66411 62171 全量 + 42391 部分 + 118469 无,合并去重后的跑批输入
ai_待下载.txt 2611 旧池剩余(platform 无佐证两档 + 零星),已下过 99%,基本作废
platform无佐证_非带货.txt 776 AI 古风/漫剧/语录图文
platform无佐证_带货.txt 1331 待下(不建议):帐篷等实拍带货

截至 2026-09-29,9 月三批的下载进度都是 97%+(118469 批 103702/106447、 42391 批 37254/37703、62171 批 50989/52360),三批合计还有约 4565 条 不在 records.jsonl 里(含图文、已删除等下不动的)。

5. 自检

python tests/test_smoke.py      # 离线冒烟:SM3/RC4/base64 自测、id 提取、Registry
python tests/test_meta.py       # 标题/标签拆分、records.jsonl 追加与续跑
python tests/test_aiclass.py    # AI 判定(用例全取自真实数据)
python tests/test_pipeline.py   # 端到端:ffmpeg 真跑,网络和 COS 打桩

老产物 manifest.jsonl

records/manifest.jsonl 是第一批 5393 条四川话数据的清单(cos_mp4 / cos_mp3 / duration 三字段), exports/抖音-四川话.xlsx 就是它的 xlsx 版。它和 records/records.jsonl 零重合——那 5393 条不在 records/records.jsonl 里,所以这个文件是这批数据在 COS 上的唯一记录,别删。

生成它的那套分阶段脚本(main.py / extract_audio.py / probe_durations.py / upload_cos.py)已经删掉,功能全被 pipeline.py 覆盖了。这个项目没有 git,删了就是真没了—— 但那四个脚本做的事 pipeline.py 一条命令全包含,唯一不能复现的是 manifest.jsonl 的三字段老格式。 其中 probe_durations.probe() 被 pipeline.py 用着,已搬进 douyin/audio.py。


6. 跑批结果

records/records.jsonl 累计 219144 条 / 8676.02 小时 / 2483.43 GB(mp4), 全部在 cos://asr-dig-data-1253181912/0005/zh/。

9 月三批 AI 池(42391 / 62171 / 118469)已基本下完,各批进度 97%+。 早期按 AI 判档的拆分(platform 有佐证 5423 条 / strong 9036 条等)是 9/1 时点的 快照,那之后 records.jsonl 又进了十几万科目的 9 月 AI 批,旧数字只作历史参考。

COS 前缀 0005/zh/ 混了八批

 5393  四川话        只在 manifest.jsonl 里,不在 records.jsonl
 3517  第一批链接
14884  AI 筛选批(40087 源)
 4813  csv 全量批
 2093  87110 + 57261 两批
37254  42391 批
50989  62171 批
103702 118469 批

按前缀取数会一次拿到全部,要区分只能走 records.jsonl。

7. 待办 / 已知问题

该修的

  • is_aigc 把免责声明当成了 AIGC,全库误算 1029 条(见 §4.5)。爬虫那层应改成 risk_infos.type in {44, 47, 76, 328},并把 type 一起存进记录——这样才能区分 「作者声明」(44) 和「平台检测」(47),两者可信度不同。改完 platform 档会掉约 1029 条, 真 AI 一条不少(那些走 strong)。
  • 图文作品(aweme_type 68/61/55/163)不支持,需要 images[] 分支。这是目前失败 的绝对大头:87110 批 38 条失败里 33 条是图文,AI 池里累计 241 条下不了。 图文在按 aweme_id 排序的清单里扎堆在头部(早期 AI 内容多是 Midjourney 出图), 所以跑批前几百条失败率会飙到 15%,后面掉到 2-3%,别被吓到。

已知取舍

  • 最长一条 3398.988 秒(约 57 分钟),若 ASR 侧对单条时长有上限需要另行切分
  • split_desc 在标签名以 #名字 形式也出现在正文里时会连正文一起删(见 test_split_repeated_tagname_in_body_is_lossy);服务端给了可信偏移时不会
  • 设备配额轮换的触发条件是每 550 次 detail,但到底是按 IP 还是按 UA/设备指纹限流 一直没验证过。实测跑到第 3 次轮换都正常,暂时不用管
  • topic / author 两档从没进过下载清单。author 档现在 14 条全部来自音乐标题 (【AI配文风景滤镜】的原声 这类抖音模板),作者昵称一条都没有——因为凡是作者叫 AI 的, 作品本身也都标了 AI,昵称这条线提供不了增量,它的价值只是给 platform 档做佐证

安全

  • 聊天记录里出现过一组 COS 密钥,建议轮换。凭证只能走 COS_SECRET_ID / COS_SECRET_KEY 环境变量,CosUploader 不读命令行参数