# 抖音视频批量采集 → 音频提取 → 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)。