Akke 评论采集 → 写库 全流程(逐步 + 每步查重)

2026-06-18(北京时区)· 一条买家评论从抖音被抓回、到落进 Akke 数据库,每一步在哪查重、多久一次、花多久 · 代码坐实
📋 汇总版
🔬 事无巨细详细版
一句话:整条链是好几个「闹钟」(cron 定时任务)首尾接力、彼此不对齐,所以端到端时间 = 每一环等待时间叠加,不是某一环的频率。
查重一共 4 层:工单级、视频级、评论级、触达级 —— 其中只有「视频级」是真·数据库唯一约束,评论级全靠代码比对。

8 步全流程一屏看

0
派活 · 给每个号源生成抓取工单
Vercel cron 写 scrape_jobs 表,status=pending
全量 每小时热号 每 3 分钟 几秒查重① 5 分钟内已抓跳过
1
worker 领工单 · poller 轮询
原子认领 pending → running,按 pri 优先级排序
每 30 秒入队→开抓 中位 1.4 分 查重 原子 claim 防重领
2
发现视频 · 这个号最近发了哪些
f2 签名 API 拉作品列表,失败降级 Playwright 滚页
几秒 ~ 30 秒 查重② videos.url 数据库 UNIQUE(唯一真约束)→ upsert
3
拉评论原文 · 翻页 + 楼中楼
默认只要近 7 天评论;带回 cid / 正文 / 昵称 / 用户 ID
每视频 2~10 秒本层不去重
4
写库前建查重表(内存)
查该视频 DB 已有评论 → 建 (正文,昵称) 配对表
毫秒准备 查重主键 = (content, author)
5
内容过滤 · 标 skipped 省钱(非去重)
竞品广告 / 同行噪音 → status=skipped 不送 LLM
毫秒过滤 ≠ 去重
6
Phase 1 写顶层评论
insert(非 upsert),插完补 cid→UUID 映射
一次批量 insert 查重③ 代码比 (content,author):DB 已存 + 同批已见
7
Phase 2 写楼中楼 · 挂父评论
找不到顶层父就 skip 不孤悬
一次批量 insert 查重④ 同 (content,author) + parent 反查

4 层查重 · 一张表记牢

在哪查重 key约束类型insert / upsert
工单级第 0/1 步5 分钟内同视频 + 原子 claim机制
视频级第 2 步videos.url数据库 UNIQUE(唯一真约束)upsert
评论·顶层第 6 步(content, author)纯应用层代码insert
评论·楼中楼第 7 步(content, author) + parent_cid 反查纯应用层代码insert
最该记住:comments 表本身没有数据库唯一约束。评论去重完全压在 worker 那段 (content, author) 内存比对上。绕过这段代码直接写库,DB 拦不住。douyin_cid(抖音原生评论 ID)入库了,但没拿来做唯一键,只用于楼中楼找父评论。

端到端要多久

场景时间说明
Tier-1 好号 最快~9 分钟采集5 + 打分2 + 派单2 + 领单1(各闸不对齐叠加)
全量普通号~1 小时起tier-0 走每小时 cron
高意向慢尾中位 34 分,但 >6h 占 42%老视频覆盖盲区,已上 cold-intent 回扫止血(✅ 6-19 复诊:回扫 pri=3 实测 p99 1.9h / max 2.0h,封顶在 ≤2h 容差内、高/中意向 leads 未掉,止血有效,详见下方 pri 防饿死)
想看每个英文词的中文注释、tier/pri 等级分值表、每步代码位置、6 天延迟根因 → 点上方 🔬 事无巨细详细版

先打底:英文词中文注释

cron(克朗)= 定时任务,按时间表自动跑;*/3 * * * * 这种叫 cron 表达式。
scrape(斯克瑞普)= 抓取 / 采集,把抖音评论扒回来。
worker(沃克)= 跑在 Fly.io 云上的 Python 后台程序,专管抓取。
poller(破勒)= 轮询器,worker 里一直转的循环,每隔几十秒看一眼有没有活。
tier(提尔)= 号源分层等级(这个号值不值钱)。
pri / priority(普赖 / 普赖奥瑞提)= 抓取工单的优先级分值(谁先被抓)。
dedup(迪迪普,de-duplicate)= 去重 / 查重。
upsert = update + insert,存在就更新、不存在才插,不重复。
UUID = 数据库里每行的唯一主键字符串。cid = 抖音原生评论 ID。sec_uid = 抖音用户唯一 ID。

第 0 步 · 派活:给每个号源生成抓取工单

谁干Vercel(韦赛尔,跑前端和定时任务的云平台)上的 cron
全量派活/api/cron/scrape · 10 * * * * = 每小时的第 10 分跑(每小时 1 次)
热号派活/api/cron/scrape-hot · */3 * * * * = 每 3 分钟一次(只派 Tier-1 优质号)
干啥每个 active(活跃)源账号写一行 scrape_jobs(抓取工单表),status=pending(待办)
耗时几秒,纯写库

这里分 tier 等级(号源分层,只有 2 级)

tier含义走哪个 cron默认 pri速度
tier=0默认普通号源(约 1019 个长尾号)scrape 每小时pri=5 慢道
tier=1优质好号(约 176 个;近 3 天高意向 ≥5 自动升,只升不降)scrape-hot 每 3 分钟pri=1 快道
升 tier1 两条路:① 运营手工标;② auto-tier1 cron(0 1 * * * 每天凌晨 1 点)自动升。每次升档写一行 source_tier_change_log 留痕。
查重①(工单级)SCRAPE_SKIP_RECENT_MINUTES(最近多少分钟内不重抓,野荞机器上已从 30 调到 5)跳过 5 分钟内已抓过的视频,防白抓。

第 1 步 · worker 领工单:poller 每 30 秒抢一次

谁干Fly.io(弗莱,东京机)上的 worker/services/queue.py
频率POLL_INTERVAL_SECONDS = 30 = 每 30 秒转一圈(空队列睡 30 秒,有活立刻干)
怎么领调数据库函数 claim_pending_scrape_jobs_batch(原子批量认领),pending → runningattempts(尝试次数)+1
重试上限MAX_ATTEMPTS = 3 = 抓 3 次失败就排除(变「墓碑」永久 pending)
实测延迟入队 → 开抓 中位 1.4 分钟(worker 有大余量,每小时空转约 50 分钟)

pri 优先级体系(查代码坐实,无配置表,硬编码 7 个入队点)

pri哪种活含义
0 最急高意向注入(#363) / VIP 视频复扫 / UI 手动「立即抓」插队最前
1常规单视频 fresh-catch(新鲜捕捞)
3tier-1 整号活 / rescan-recent / cold-intent 回扫
5 最慢tier-0 每小时(默认值 priority INT DEFAULT 5)/ hot-rotation
领单排序 ORDER BY priority ASC, scheduled_at ASC(pri 数字小的先,同 pri 先到先抓)。
已知坑(pri 防饿死):原来严格抢占 → 只要 pri=1 源源不断,pri=5 的 tier-0 会被饿死。6/11 出过 P1 事故:普通号连续 27 小时一条没抓到。修法 migration 20260615144043aging(等久升档):每等 6 小时有效优先级升 1 级,等满 24 小时追平 pri=1 但永不超过。
⚠️ 同一个坑又复发(2026-06-19 实测,更隐蔽):#413 早停修复上线后,快道 pri=1 量暴涨 +59%(视频不再被判空 → 不再退避 → 每 tick 更多候选入队),把低优先级 scrape_video(含 cold-intent 老视频回扫 pri=3)挤到队尾饿死 —— 6-18→19 全天 p99 领单等待 6202 秒(≈1.7h)。aging 最终能把它们捞上来,但要先等一两小时,这段等待就是这条尾巴。aging 是「等久了再救」,挡不住「一开始就被插队」。
✅ 2026-06-19 复诊(pri 防饿死配额暂不做):复跑 36h(覆盖 6-18→19)按 pri 段拆 scrape_video,印证又收窄了上面的判断——cold-intent pri=3 全部认领,claim-wait p50 27m / p99 1.9h / max 2.0h、0 条超 6h、pending 仅 15 条且都没等满 1h。也就是说那条 6202s≈1.7h 的尾巴是真的,但封顶在 2.0h,正好踩在 cold-intent 自己「激进追平 ≤2h」的设计容差里,没有超出 2h 的长尾。同期 tier-0 pri=5 已被 aging 压到 p99 1.1h / max 13.4h(仅 8 条,0.01%),不再是早先的 12h / 27h 积压。高/中意向入库近 7 天稳在 370–463/天(446→380 是日间方差、不是这条尾巴的连带伤)。结论:pri 防饿死配额(worker 槽给 pri≥3 留位)是高危改动,当前无指标支撑,降级为留观;真要再压这条 1.7h 尾,对症杠杆是 env COLD_INTENT_RESCAN_INTERVAL_MIN(120→60) / COLD_INTENT_RESCAN_MAX,纯 fly secret、不开 PR。

第 2 步 · 发现视频:这个号最近发了哪些

函数worker/services/discoverer.pydiscover_videos_from_profile()
快路径f2(抖音 API 签名库)签名调 fetch_user_posts()(拉作品列表),快
慢降级快路径失败才启 Playwright(普雷赖特,浏览器自动化)滚 DOM(页面元素)收视频卡
抓几条默认每号最近 ~20 条;tier1 好号抓到第 11–20 条仍贡献约 26% 高意向
写库_persist_discovered_videos():先查 DB 已有 url → 过滤新的 → videos.upsert(rows, on_conflict="url")
耗时快路径几秒;Playwright 降级每号约 10–30 秒
查重②(视频级,全链唯一一处真·数据库约束)videos.url001_initial_schema.sql 上有 UNIQUE(唯一)约束。同一视频 url 撞了走 upsert,只刷新 scraped_at(抓取时间)。

第 3 步 · 拉评论原文:翻页 + 楼中楼

函数worker/services/comment_api.pyfetch_comments()
怎么拉f2 签名调抖音 web 评论 API,循环翻页(cursor 游标分页)
时间窗默认只要近 7 天评论(cutoff 截止线丢老评论,这是过滤不是去重
楼中楼顶层评论有子回复的,再调 _fetch_replies() 拉嵌套
带回字段cid(原生评论 ID)/ parent_cid(父评论 cid)/ content(正文)/ author(昵称)/ douyin_user_id(sec_uid)/ ip_location(IP 属地)
耗时每视频约 2–10 秒(看评论数与翻页轮数)
去重本层不去重,纯拿数据
6/13 最大发现:抖音发视频接口本身有 ~6 天延迟,导致 99% 评论一抓回来就已超 7 天窗、过期。这是端到端慢尾真因之一(不是排队问题)。

第 4 步 · 写库前在内存里建查重表

worker/services/scraper.pyscrape_video_comments() 后,查该 video_id 下 DB 已有评论,建两张内存表:

第 5 步 · 内容过滤:标 skipped 省 LLM 钱(不是去重)

每条过 _shape_row() 跑两个判定:

背景:7 天窗数据里「无关」占 57%,主要是这类噪音,先砍掉省钱。

第 6 步 · Phase 1 写顶层评论:查重落地

怎么查重每条算 key=(content, author),命中 existing_by_ca(DB 已有) seen_in_batch(本批已见)就跳过
写库城市富化后 comments.insert(top_rows) —— 是 insert 不是 upsert
插完把新行 douyin_cid → id 补进 cid_to_uuid,给 Phase 2 用
查重③(评论·顶层)纯应用层代码(content, author) 双重比对 —— 既比 DB 已存、也比同批翻页重叠。
为什么要 seen_in_batch(本批已见集合):f2 翻页 cursor + 顶层/嵌套返回会重叠,2026-05-20 实测有 13.5% 入库行是同毫秒完全相同的重复,靠这个集合挡住。

第 7 步 · Phase 2 写楼中楼:同款查重 + 挂父评论

查重(content, author) 比对(DB + seen_in_batch)
挂父parent_uuid = cid_to_uuid.get(parent_cid),找不到顶层父就 skip 不孤悬
写库comments.insert(nested_rows),带 parent_comment_id(父评论外键)
查重④(评论·楼中楼):同 (content, author) 应用层逻辑 + parent_cid 反查兜底。至此评论落库,status=pending,等下游打分。

下游(采集之外,顺带看清全链时间)

cron / 机制频率干啥
②打分 analyze/api/cron/analyze*/2 每 2 分钟给新评论打意向分(高/中/低/无关),高/中意向生成草稿
③派单 dispatch/api/cron/cloud-pc-dispatch* * * * * 每分钟喂 DM 队列给云电脑 agent
库存补单/api/cron/feed-backlog每 2 小时喂未触达高意向库存
云电脑领单wuying_poll_agent.py60 秒领队列 → GUI 自动发

端到端实测时间

场景时间说明
Tier-1 好号 最快~9 分钟采集5 + 打分2 + 派单2 + 领单1(各闸不对齐叠加)
全量普通号~1 小时起tier-0 走每小时 cron
高意向慢尾中位 34 分,>1h 占 47% / >6h 占 42% / >12h 占 38%就是日报里标 expired_at_claim(领单时已过期)被跳过那批
慢尾真因 = 老视频覆盖盲区(tier-1 只重扫最近 ≤20 条视频,老视频被挤出就不再定期扫),已上 cold-intent-rescan*/20 每 20 分钟回扫近 14 天仍冒意向的老视频)止血。
✅ 2026-06-19 复诊更正:早先一版担心「止血被快道洪水在排队层抵消」——6-19 复跑 36h 实测没那么严重。cold-intent 回扫 pri=3 的排队尾巴封顶在 p99 1.9h / max 2.0h、0 条超 6h,在它「激进追平 ≤2h」的容差内;高/中意向入库近 7 天稳在 370–463/天,没掉。所以慢尾真治还是靠「找回老视频」(cold-intent 做的,pri=3 也确实排得动),pri 防饿死配额(高危 PR)暂不做、留观;要再压尾用 env COLD_INTENT_RESCAN_INTERVAL_MIN / MAX。详见 时效深挖页 ④ 终判

触达层还有第二套查重(和采集无关,别混)

查重点key机制
lead 锁(org_id, douyin_user_id)lead_claims 表锁 4 小时,防多人重复 claim 同一用户
派单在途(org_id, douyin_user_id) where status in (pending,claimed)dispatch_queue 唯一索引,撞了报 23505(PG 唯一冲突码)→ 标 dedup
消息幂等(conversation_id, content, role=ai, status=sent) 15 分钟内mark_lead_contacted RPC,重复调不双发
一句话总览查重在哪:
1. 工单级:5 分钟内不重抓 + 原子 claim 防重领。
2. 视频级videos.url 数据库 UNIQUE(唯一真约束)→ upsert。
3. 评论级(顶层+楼中楼)纯代码 (content, author) 配对,DB 比 + 同批比。comments 表本身没有数据库唯一约束 —— 最该记住的一点。
4. 触达级:lead_claims + dispatch_queue(23505) + 消息 15 分钟幂等,三重。
Akke · 评论采集 → 写库全流程与查重 · 2026-06-18 · 代码坐实(vercel.json / queue.py / scraper.py / comment_api.py / discoverer.py / supabase migrations)。频率与等级随代码漂移,引用前以仓内为准。