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

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

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

全程纯 Python,不依赖第三方解析服务、不依赖 JS 运行时、不需要登录。 核心走 App 端接口:先注册设备拿 device_id,用它取作品详情,CDN 直链下载。

任务进度、各批下载情况见 PROGRESS.md; 接口是怎么逆出来的(web 端为何弃用、App 端免签名链路、两层限流)见 RE.md。


快速开始

pip install -r requirements.txt   # 另需系统装有 ffmpeg / ffprobe
$env:COS_SECRET_ID="..."; $env:COS_SECRET_KEY="..."   # COS 凭证只走环境变量

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

# 确认没问题再放开(断点续跑:重启自动跳过已完成的)
python pipeline.py --from-file lists/ai_62171.txt --region ap-chongqing --workers 8

# 自检
python tests/test_smoke.py && python tests/test_meta.py && python tests/test_aiclass.py && python tests/test_pipeline.py

流水线详解

pipeline.py 是项目核心:一条命令完成 下载 → 抽音 → 上传 → 记录 → 删本地。 和分阶段跑(全下完再全抽完再全传完)的本质区别是磁盘占用:分阶段要把整批 mp4 摊在盘上(上一批 53 GB),流水线每条走完即删,峰值只有 workers × (mp4+mp3) ≈ 100 MB,与总量无关。

每条视频的 9 步

1. fetch_detail     DevicePool 取设备 → App 端 detail → 元信息 + raw
2. 下载 mp4         play_addr 直链 / /aweme/v1/play/ 302,Range 续传
3. 抽 mp3           ffmpeg 转码(CPU 密集,信号量限到核数)
4. 探时长           ffprobe 读 mp4;mp3 用 -count_frames 实解码
5. 校验时长         mp3 实解时长 ≈ mp4 时长?          ← 抽取闸门
6. 上传 mp4         COS,覆盖同名
7. 上传 mp3         同上
8. 核对大小         put() 内部 head_object 比对字节数  ← 删除闸门
9. 落库 + 删本地    写 records.jsonl,然后才 unlink mp4/mp3

为什么第 4 步必须实解码:LAME 写的 Xing 头声明的是原始总帧数, ffprobe format=duration 直接信它不解码——实测把 mp3 截断到 20%, 它报的还是完整的 10.0078s,ffmpeg -f null - 同样拦不住(rc=0 无报错)。 只有 -count_frames 真解码能发现截断。代价可忽略:120s 的 mp3 是 0.109s 对 0.053s。两道闸门任一不过:保留本地文件、记 failed、继续下一条。

启动阶段:四件事做完才开跑

  1. 解析输入:--from-file 或命令行链接,经 urls.py 一律还原成 aweme_id,顺手去重、剔除已完成的(aid in log)、统计无法解析的。
  2. 切段(plan_batch,默认 --max-items 12000):单进程跑到 ~1.5 万条, Windows 桌面堆被几万个 ffmpeg/ffprobe 子进程耗光,之后 CreateProcess 一律失败(rc=3221225794)。症状好认:完成数冻结、失败数猛涨、work/ 暴涨。是累积量问题不是并发问题。段跑完 os.execv 重启自己, 断点记录不丢。
  3. 清扫 work/(sweep_work_dir):删两类残留——aweme_id 已在 records.jsonl 里的(早传上 COS 了)、.part 半截下载。不碰失败项的 文件:上传失败/抽音截断是故意留着的,重跑省一次下载。长批跑起来后 每 5 分钟还会自动清扫一次(光靠开跑前扫一次,跑到一半磁盘就满)。
  4. 预热设备池(DevicePool):惰性注册,先主动要一次设备。开跑就 注册不上 = IP 还在冷却期,按退避策略等待并重启(见下),而不是让每个 worker 各自撞一遍把整批标失败。

并发模型:两类资源分开限

资源 限流方式 默认值
网络(下载/上传/detail) --workers 线程池 8
CPU(ffmpeg/ffprobe) ffmpeg-workers 信号量 CPU 核数

下载用裸 okhttp/3.12.13 UA 的 thread-local session(CDN 不查设备头); 每个 worker 的 detail 请求走 DevicePool.current() 共享设备身份。

限流的两级应对

设备级 detail 配额(~575 次/设备,默认 550 就换):detail 返回「空 body」 即判定设备被限,pool.report_dead(gen) 换设备重试一次。generation 标记 保证多 worker 同时发现失败时只换一轮。

IP 级注册配额(~12 次注册后所有 host 返回 device_id='0'):换设备没用, 只能等冷却。此时拉起全局 abort:后续任务直接跳过不计失败,当前批收尾后按 next_abort_wait 指数退避(默认 30 分钟起步、连撞翻倍、上限 4 小时)等待并 os.execv 重启续跑。连撞计数存在环境变量 _DY_ABORT_STREAK 里(execv 换进程 映像后靠它记住状态);但凡这一轮出过活就说明是跑着跑着才触发的限流,下次从 底价重等。早期没有这套机制,一次注册受限直接烧掉 678 条任务。

失败处理

  • StageError 带阶段名(detail / download / audio / probe / upload), 连同原因追加进 records/pipeline_failed.txt,一行一条:id\t阶段\t原因。
  • 顶层还有 except Exception 兜底记 unknown,一条炸不掉整批。
  • 失败项的本地文件故意保留:重跑同一命令即可续传(已完成的自动跳过, 失败项重新走全流程)。跑完退出码:0 全成,4 有失败,3 开跑即注册失败, 5 撞上 IP 限流停下等手动重跑。

进度报告

后台线程每 15 秒打一行:[已完成+失败/总数] 完成 失败 | GB 上传 | 条/分 | 剩约 N 分, 兼做周期清扫的节拍器。

入口脚本

pipeline.py 在根目录(主入口),其余脚本在 scripts/,测试在 tests/。

文件 作用
pipeline.py 核心。一体化流水线,逐步过程、闸门与限流应对见上面「流水线详解」
scripts/clean_ai.py 从外部抓来的链接 jsonl 里筛出 AI 相关作品,产出链接清单直接喂 pipeline。判定分四档(platform / strong / topic / author),先 --stats 看分布再 -o 出清单
scripts/jsonl_to_xlsx.py 把 records/records.jsonl(或老的 manifest.jsonl)清洗导出成交付用 xlsx:剔坏行、剔非法时长/前缀、按 mp4 地址去重。流式读,raw 不进内存
scripts/export_sample_xlsx.py 按甲方字段表把 records.jsonl 导成样本 xlsx(样本 ID / 标题 / 生成方式 / 工具模型 / 分辨率 / 发布时间 / AI 标识等),支持 --ai-only、--limit

测试(tests/)

文件 覆盖
test_smoke.py 离线冒烟:crypto 官方向量自检、链接解析、文件名清洗、去重 Registry
test_meta.py 标题/标签拆分(split_desc)、records.jsonl 追加写与幂等
test_aiclass.py AI 判定,用例全部取自真实数据(含 Al 冒充、罗马数字 Ⅰ 等冒充写法)
test_pipeline.py 流水线端到端:ffmpeg 真跑,网络和 COS 打桩。覆盖两道删除闸门(时长不符 / 上传大小不符必须保留本地)

douyin/ 包

文件 作用
app.py 核心。App 端接口客户端:/service/2/device_register/ 注册设备拿 device_id/install_id,带它打 /aweme/v1/aweme/detail/ 取详情。无签名、无 Cookie、无登录。设备级 detail 配额约 575 次,超限换设备
batch.py 并发批处理骨架:ThreadPoolExecutor 调度、每 worker 独立 Session、DevicePool 用 generation 标记轮换设备(避免多 worker 重复换设备)、IP 级注册限流指数退避
download.py 带 Range 的断点续传下载;落盘文件名即 aweme_id,「是否已下」看文件在不在;Registry 去重记录
meta.py 标题/标签拆分(split_desc,认 desc 正则全集不认 text_extra)+ RecordLog:records.jsonl 线程安全追加写、行首 aweme_id 快速扫描续跑。约定:aweme_id 必须是每行第一个字段
aiclass.py 判定作品是否 AI 相关,四档取最强:platform(平台 AIGC 标)> strong(文案/标签明确 AI 信号)> topic(在讲 AI)> author(仅昵称带 AI)。规则详解见 AI.md
cos.py 腾讯 COS 上传。put() 上传后必 head_object 取 Content-Length 与本地字节比对,对不上算失败——删除闸门卡在这
audio.py ffmpeg/ffprobe 薄封装:定位可执行文件、mp4 抽音轨(默认转 mp3 192k,copy 模式直接封装 m4a 无损)、-count_frames 实解码探时长(format=duration 会被 Xing 头骗过,不能用)
urls.py 链接解析:裸 id / 分享短链 / 整段分享口令 / video / note / modal_id 一律还原成 aweme_id
client.py Web 端客户端(Aweme / DouyinError 等共用类型 + web 请求路径)。类型被正常用着;web 请求路径闲置——匿名被强制登录,见 PROGRESS.md
signer.py a_bogus 签名器接口。NodeSigner 需要项目外的 node_signer/sign.js(不在仓库里);ABogusSigner 纯 Python 版没写完。web 端真要走通得先补这块
crypto.py 签名器的密码学积木:SM3 / RC4 / 自定义 Base64,带官方测试向量自检。目前只被 test_smoke 验证,未被 signer 使用

数据目录

目录 内容
data/ 源数据 jsonl/csv(外部抓取的输入,删了没法重新筛,别动)
records/ records.jsonl(流水线产物 + 断点记录,一行一条含 raw)、manifest.jsonl(第一批老格式清单)、pipeline_failed.txt(失败清单)
exports/ xlsx/csv 交付导出物
lists/ 各批的链接清单,也是「哪批下了什么」的凭据
logs/ 各批跑批日志

使用指南

环境准备

pip install -r requirements.txt
依赖 用途 备注
Python ≥ 3.11 实测 3.11.0
ffmpeg / ffprobe 抽音轨、探时长 需在 PATH,实测 2025-07-31 gyan.dev full build
requests 接口/下载
cos-python-sdk-v5 COS 上传 延迟导入,不传 COS 可不装
openpyxl xlsx 导出 延迟导入,不出 xlsx 可不装

COS 凭证只走环境变量(不进命令行、不进代码):

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

典型工作流:从一批源数据到交付

# ① 拿到外部抓的源 jsonl,先放 data/,看 AI 分布(别信文件名,先 --stats)
python scripts/clean_ai.py -i data/抖音ai视频_20260904_62171.jsonl --stats

# ② 筛出 platform+strong 档,出链接清单到 lists/
python scripts/clean_ai.py -i data/抖音ai视频_20260904_62171.jsonl --tier platform,strong -o lists/ai_62171

# ③ 小批试跑 20 条,--keep-local 不删本地,人工检查 work/ 里的 mp4/mp3
python pipeline.py --from-file lists/ai_62171.txt --region ap-chongqing --limit 20 --keep-local

# ④ 正式跑批。断点续跑靠 records/records.jsonl,中断/重启/换机器都能续
python pipeline.py --from-file lists/ai_62171.txt --region ap-chongqing --workers 8

# ⑤ 失败的重跑:同一条命令再跑一遍即可(已完成的自动跳过,
#    失败项的本地文件还留着,省一次下载)
python pipeline.py --from-file lists/ai_62171.txt --region ap-chongqing --workers 8

# ⑥ 出交付表
python scripts/jsonl_to_xlsx.py -i records/records.jsonl --columns full -o "exports/全量.xlsx"
python scripts/export_sample_xlsx.py --ai-only -o "exports/AI视频样本.xlsx"

pipeline.py 参数速查

参数 默认 说明
target... / --from-file 链接或 aweme_id,二选一或混用;文件里 # 开头行视为注释
--region 必填 COS 地域,如 ap-chongqing
--bucket / --prefix asr-dig-data-1253181912 / 0005/zh/ COS 落点
--workers 8 网络并发数
--bitrate 192k mp3 码率
--ffmpeg-workers CPU 核数 ffmpeg 并发上限
--device-quota 550 单设备 detail 调用上限(实测 ~575 被限,留余量)
--limit 0 只跑前 N 条,试跑用
--max-items 12000 单进程跑满即自动重启接着跑,防 Windows 桌面堆耗尽;0=不切
--abort-wait / --abort-wait-cap 1800s / 14400s 撞 IP 级注册限流后的等待/上限,连撞翻倍
--keep-local 关 上传后不删本地文件(试跑检查用)
--records / --failed records/ 下两个文件 元信息落点 / 失败清单

退出码:0 全成;4 有失败(见 failed 文件,重跑同命令续传);3 开跑即注册失败; 5 撞上 IP 限流已停下,等几小时冷却后重跑同命令。

clean_ai.py 参数速查

参数 默认 说明
-i 必填 源 jsonl
--stats 只看分布不落盘,拿到新文件第一步
--tier platform,strong 保留哪些档(platform/strong/topic/author/none)
-o 空 输出前缀,生成 <前缀>.txt(要 --with-jsonl 才另出 jsonl)
--platform-needs-text 关 platform 档要求文案也有 AI 信号。会误伤文案零信号的 AI 古风作者,见 AI.md §8
--exclude-forbidden 关 剔 allow_download=false 的。不需要——aigc_forbidden 只限 web 端
--samples N 0 每档打印 N 条样例到 stderr,复核用

导出参数速查

python scripts/jsonl_to_xlsx.py --list-columns          # 先看有哪些列可选
python scripts/jsonl_to_xlsx.py --columns full ...      # full = 标题/作者/标签/时长等常用全字段
python scripts/export_sample_xlsx.py --ai-only --limit 500 ...

常见问题

  • 跑到一半卡住、完成数冻结、失败猛涨:撞 Windows 桌面堆上限了。Ctrl-C 后重跑 同命令即可(断点续跑),或调低 --max-items 让重启更频繁。
  • 大量「空 body」失败:设备配额到了属正常(会自动换设备);若报 「IP 级注册配额耗尽」,流水线会自己等待续跑,放着不管即可。
  • 跑批前几百条失败率 15%:正常。图文作品(图集)扎堆在 aweme_id 排序的头部, 后面会掉到 2-3%。
  • 想只重试失败项:把 failed 文件第一列抠出来当输入重跑,或直接重跑原命令 (已完成项跳过,代价只是启动时扫一遍 records)。
Description
抖音视频下载
Readme 2.9 MiB
Languages
Python 100%