App 端免签名接口(设备注册 + detail + CDN 直链),一体化流水线 下载→抽音→传COS→记录→删本地,AI 作品四档判定,四份文档: README(使用指南)、PROGRESS(批次进度)、RE(逆向全过程)、AI(判定规则)。 Co-Authored-By: Claude Code <noreply@anthropic.com>
261 lines
15 KiB
Markdown
261 lines
15 KiB
Markdown
# 抖音视频批量采集 → 音频提取 → 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)。
|