App 端免签名接口(设备注册 + detail + CDN 直链),一体化流水线 下载→抽音→传COS→记录→删本地,AI 作品四档判定,四份文档: README(使用指南)、PROGRESS(批次进度)、RE(逆向全过程)、AI(判定规则)。 Co-Authored-By: Claude Code <noreply@anthropic.com>
23 KiB
任务进度 / 踩坑记录
项目介绍和各代码文件说明见 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,零鉴权。下载走的就是它。 |
两层限流(跑批量必然撞上)
- 设备级 detail 配额:单个
device_id约 575 次 detail 调用后被限,表现为 HTTP 200 空 body。 →douyin/batch.py的DevicePool到配额就换设备;用generation标记当前设备,避免多个 worker 同时发现失败把设备连换好几次。 - IP 级注册配额:短时间内注册约 11~12 次后,所有 register host 一律返回
device_id='0'。 →Device.register()指数退避重试(默认 5 次,30s 起步翻倍),让批处理停下来等,而不是空转着把剩余任务全标成失败。
第 2 条是踩过的坑:早期没有退避,一次注册受限直接烧掉 678 条任务。
作品拿不到时怎么区分
看 filter_detail.filter_reason:
images_base→ 图文作品(图集),本项目不处理,抛NotAVideostatus_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、继续下一条:
- 抽取闸门 —— mp3 用
ffprobe -count_frames实际解码算时长,与 mp4 比对, 差超过max(1s, 2%)判定截断。 注意不能用format=duration:它读的是 LAME 的 Xing 头,声明的是原始总帧数。 实测把 mp3 截断到 20%,format=duration报的仍是完整的 10.0078s,一字不差;ffmpeg -f null -同样拦不住(rc=0、无 stderr)。只有实解码能发现。 - 上传闸门 —— 不信
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):
platform是唯一能抓到"作者没标但确实是 AI"的信号。 实测 1744 条被平台标了 AIGC,文案标签里一个 ai 字都没有(脑筋急转弯、历史解说、原创动画)。纯关键词过滤 会整片漏掉。反过来 9000+ 条有明确 AI 信号却没被平台标——两路互补,都得要。Al(大写 A + 小写 L)冒充 AI。 标签抖音al出现 129 次,文案里也有#Al创作。 不归一化就漏。反过来ai是 email/detail/again/hair 的常见片段,只能认前后不接 字母数字的独立ai。- 音乐标题默认是
@<作者昵称>创作的原声,40087 条里 32555 条是这个壳。昵称带 AI 会连"创作"一起命中,等于让作者昵称从 music 这条路偷偷升到strong——实测 771 条 这样混进来。现在先剥壳,剥完只当弱信号(全库剥完只剩 16 条还有命中,本来就没价值)。 - 中文泛词不单独成立。
动画/绘画/分身/动漫必须跟 AI 词同现,否则 830 条#原创动画全被误收。剪映(375)/关键帧(314)/蒙版(203)/转场是剪辑术语,不是 AI。 - 工具名不含 ai 字样的一大票:即梦 / seedance / libtv / flovaai / 小云雀 / runninghub / oiioii / shotlab / veo3,只能靠名单,名单要跟着平台活动更新。
- 一批工具名本身是常用词,必须要求跟 ai/aigc 同现(见
_AMBIG_GEN/_AMBIG_CHAT)。 实测误判:豆包 224 条(狗名、豆沙包)、万相 17 条(《龙神万相》)、海螺 13 条 (三峰的"海螺"帐篷)、元宝 11 条(316 不锈钢元宝勺)、星流 1 条("明星流量"子串)、 清影 1 条("起舞弄清影")。而真命中的无一例外都同时带 AI 字样,所以这道门槛 不掉召回——旧文件重跑 15218 → 15204,掉的正好是这些。 - 英文工具名要挡词中词,但只挡字母,不挡数字和中文。
udio藏在Grass Studio里 17 次、flux藏在Fluxxwave曲名里。可是数据里满地都是Seedance2.0、sora2、runninghub0.21、libtv创作者、flovaai超创,一刀切加会丢 3500+ 条真命中。 - 别放开"数字人"/"换脸"/"分身"当独立信号。 数字人民币含"数字人"、微信分身含"分身"、
穿搭视频也说"这套穿搭能换脸"。实测放开只进误判,一条真的都捞不到。
同理否掉的还有:lora(撞 valorant)、tripo(撞 tripod 三脚架)、seaweed(海藻)、
度加("亏损幅度加大")、万彩("29万彩礼")、生数("诞生数副名场面")、节点("关键节点")、
抽卡(原神)、捏脸(燕云十六声)、国漫(《剑来》真人制作)、对口型(真人对口型)。
这 8 条否决理由都写成了测试,见
test_rejected_candidates_stay_none。 - AI 还有第三种冒充写法:罗马数字。
#AⅠ漫剧用的是 U+2160,跟Al一个套路。 - 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_type68/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不读命令行参数