Files
douyin_video_download/README.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

261 lines
15 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 抖音视频批量采集 → 音频提取 → COS 上传
把一批 `douyin.com/video/...` 链接跑成 ASR 可用的数据集:
**下载无水印 mp4 → 抽 mp3 → 探时长 → 传腾讯 COS → 出清单(jsonl / xlsx)**。
全程纯 Python,不依赖第三方解析服务、不依赖 JS 运行时、**不需要登录**。
核心走 App 端接口:先注册设备拿 `device_id`,用它取作品详情,CDN 直链下载。
任务进度、各批下载情况见 [PROGRESS.md](PROGRESS.md);
接口是怎么逆出来的(web 端为何弃用、App 端免签名链路、两层限流)见 [RE.md](RE.md)。
---
## 快速开始
```bash
pip install -r requirements.txt # 另需系统装有 ffmpeg / ffprobe
```
```powershell
$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](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/` | 各批跑批日志 |
---
## 使用指南
### 环境准备
```bash
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 凭证只走环境变量(不进命令行、不进代码):
```powershell
$env:COS_SECRET_ID = "..."
$env:COS_SECRET_KEY = "..."
```
### 典型工作流:从一批源数据到交付
```bash
# ① 拿到外部抓的源 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,复核用 |
### 导出参数速查
```bash
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)。