DEEP DIVE · 采集

抖音数据采集技术方案

从号源解析到评论入库的全链路工程实践 —— 签名快路径、异步队列、抗风控、多租户隔离。

最后更新 2026-05-06适用于 技术对接人 / 客户审计

问题陈述

全屋定制行业获客线索散落在抖音视频评论里,人工逐条筛选效率低、漏判多、跨账号管理困难。采集只是入口;真正的工程挑战是 稳定、隔离、可观测、可演进
单源平均
9.1 s
31 号源 × 176 视频实测
14 天失败率
4.9%
1206 jobs · 远低于 15% 阈值
IP 归因失败
0
代理池 0 边际收益
活跃号源
31
有大有小 org · 2026-05-05

三层架构

控制层 · Vercel

Next.js 16 App Router · SSR · Vercel Cron 触发 · 中间件 + RLS 双层鉴权。Cron 路由独立 CRON_SECRET 校验,与用户路径解耦。

采集层 · Fly Tokyo

FastAPI + Playwright + f2 签名 · 1GB VM 单 Chromium · 30s 队列 poller。对外接口全部 FLY_WORKER_SECRET bearer 校验。

数据层 · Supabase

16 个 migration · RLS tenant_scope 政策 · scrape_jobs 即队列 · result JSONB 写实时进度。三层之间只通过 DB + HTTP 通信,无中间件。

关键技术 ① · f2 签名快路径

单条评论页拉起浏览器约需 10–15 s,1GB Fly VM 只能跑一个 Chromium 实例。改用 f2.crawlers.douyin.web.abogus 对 a_bogus 算法签名,httpx 直接命中 /aweme/v1/web/comment/list/,异常时静默回退 Playwright 兜底。

# worker/services/comment_api.py endpoint = ABogusManager.model_2_endpoint(...) async with httpx.AsyncClient() as cli: r = await cli.get(endpoint, cookies=cookie_data) # exception → fallback to Playwright

性能基线(2026-05-05 实测)

指标数值
活跃号源31
子任务总数176(31 new + 145 refresh)
总耗时5 min 12 s
单号源平均9.1 s
单号源异常阈值≥ 60 s 视为 Playwright fallback 命中
失败数0

依赖锁定:f2==0.0.1.7 硬依赖 httpx==0.27.2,不得随意 bump。f2 失效预案见 claude-memory · feedback_comment_api_f2(Evil0ctal abogus 切包路径)。

关键技术 ② · 分享短链解析 sec_uid

从 Fly Tokyo 出口 IP,下列查找路径 全部失效,不要重试:

失效路径返回
/@{handle}重定向到首页
/aweme/v1/web/discover/search/data: []
/aweme/v1/web/general/search/single/data: []
user/profile/other?other_unique_idUserId 不合法
唯一可行:v.douyin.com/xxx 短链 302 跳转 —— 服务端跳转不走 SPA 路由也不受地理封锁,Playwright goto() 跟随到 /user/{sec_uid}。落地为 worker /scrape/resolve-share-batch + API /api/sources/import-batch,这是号源录入的 唯一标准路径,UI 上手动输入 sec_uid 已降级。

关键技术 ③ · Postgres 即队列

scrape_jobs 表四态状态机:pending / running / completed / failedresult JSONB 字段保存实时进度,前端 useScrapeJob hook 2 s 轮询并 localStorage 恢复。

// scrape_jobs.result { "total": 5, "done": 3, "success": 2, "failed": 0, "current_video_url": "...", "videos": [...] }

语义变更:2026-04-23 起号源抓取的「成功」≠「新增评论」,老视频 refresh 回 0 条评论是正常的(评论区无变化),UI Badge 显示的是 new_comments_inserted 累计。

抗风控加固路线图

多租户隔离 · 三层防御

L1 · RLS 数据层

Postgres tenant_scope 政策统一管控 org_idWHERE org_id IN app.accessible_org_ids()。绕不过去。

L2 · API guard

requireAuth / Staff / OrgMember / Owner 四档守卫,角色差异在路由层区分。

L3 · UI 置灰

操作按钮按角色禁用 · 第三层兜底 · 体验上避免误触。OrgProvider 注入会话上下文。

采集号硬隔离(migration 014):accounts.org_id NOT NULL,无共享池 fallback,新 org 必须自己绑卡。

失败率分布 · 为什么不买代理池

14 天 1206 jobs,失败 59 条 ——

失败原因条数归因
zombie_running 清理(管理员手动)29非真失败
no active scraping account(cookie 过期)26业务层
Page.goto Timeout2网络抖动
context destroyed2浏览器层
IP / 网络封锁0不存在
结论:代理池解决一个不存在的问题。重新评估的触发条件 —— 任一满足即恢复采购:① scrape-failure-rate.ts 连续 7 天 >15%;② 失败原因分布出现新桶(PROXY error / IP block);③ 业务量翻倍 + WDA 退役需切回 Web 发送。

已交付能力清单

f2 签名 fast path · zero-browser 抓取
scrape_jobs 异步队列 + 进度可观测
分享短链 sec_uid 解析 · 绕过地理封锁
Phase 1 健康状态机 + cooling 复活
Phase 2 stealth + 请求间隔随机化
采集号硬隔离 · org_id NOT NULL
三层防御 RLS · API guard · UI 置灰
数据驱动否决 Phase 3 代理池采购