# Phase 4 · 采集执行 Collection Execution > 从采集矩阵 → **真实 API 调度** + 数据归一化 + 去重 + 假设打标 —— 这是整个工作流的"事实地基"。 --- ## 🎯 目标 **输入**:P3 的采集矩阵(30-50 关键词 × 批次) **输出**: - `docs/<品类>/raw/xhs/*.json`(每关键词一个文件) - `docs/<品类>/raw/douyin/*.json` - `docs/<品类>/raw/_merged.json`(统一格式合并) - `docs/<品类>/raw/comments-flat.jsonl`(行级流,便于 grep) - `docs/<品类>/raw/audit.log`(采集审计日志) --- ## 🛠️ 采集脚本架构 ### 核心文件:`scripts/tools/<品类>-collect.js` **结构骨架**(300-500 行): ```js // ========================================================== // 1. 常量 + 路径 // ========================================================== const ROOT = path.resolve(__dirname, '..', '..'); const RAW = path.join(ROOT, 'docs', '<品类>', 'raw'); const XHS_DIR = path.join(RAW, 'xhs'); const DY_DIR = path.join(RAW, 'douyin'); const MERGED = path.join(RAW, '_merged.json'); const AUDIT = path.join(RAW, 'audit.log'); // ========================================================== // 2. API 凭据加载 // ========================================================== function loadTikHubKey() { ... } // ~/.openclaw/skills/xiaohongshu-search-notes/api-config.json function loadVocToken() { ... } // ~/.openclaw/voc-credentials.json // ========================================================== // 3. 批次定义(BATCHES) // ========================================================== const BATCHES = { 1: { xhs: [ { keyword: '本品名', max_notes: 15, max_comments_per: 10, product: '本品' }, // ... ], douyin: [ { keyword: '本品名', max_items: 25, product: '本品' }, // ... ], }, 2: { ... }, 3: { ... }, }; // ========================================================== // 4. XHS 采集函数(TikHub) // ========================================================== async function collectXhs(task) { // 4.1 检查缓存 if (existsCache(task.keyword) && !FORCE) return; // 4.2 调 TikHub search_notes const notes = await tikhubSearchNotes(task.keyword, task.max_notes); // 4.3 逐笔记调 note_comments const items = []; for (const note of notes) { const comments = await tikhubNoteComments(note.id, task.max_comments_per); for (const c of comments) { items.push(buildItem({ platform: 'xhs', product: task.product, keyword: task.keyword, type: 'comment', nickname: c.user_nickname, ip: c.ip_location, content: c.content, likes: c.like_count, note_id: note.id, note_title: note.title, // ... })); } } // 4.4 落盘 + 审计 fs.writeFileSync(path.join(XHS_DIR, `${safeName(task.keyword)}.json`), JSON.stringify(items, null, 2)); logAudit(`OK ${task.keyword} → ${items.length}`); } // ========================================================== // 5. 抖音采集函数(VOC Skill API) // ========================================================== async function collectDouyin(task) { // 调 hotSoonTopVideo / video_comments // ... } // ========================================================== // 6. 统一 Item 构造 // ========================================================== function buildItem({ platform, product, keyword, type, nickname, ip, content, likes, ...rest }) { const id = hash(platform + content + nickname); return { id, platform, product, keyword, type, nickname, ip, content, likes: Number(likes) || 0, tags: inferTags(content), hypotheses: inferHypotheses(content, keyword), sentiment: inferSentiment(content), collected_at: Date.now(), ...rest, }; } // ========================================================== // 7. 假设标注 // ========================================================== function inferHypotheses(content, keyword) { const tags = []; if (/冷链|处方|医生开/.test(content)) tags.push('H1'); if (/食品级|菌株|效果|没用|不靠谱/.test(content)) tags.push('H2'); if (/便秘|积食|挑食|腹泻|宝宝|孩子/.test(content)) tags.push('H3'); if (/入园|开学|抗生素|换季/.test(content)) tags.push('H4'); if (/喂|吐出|苦|难吃|咀嚼|水果味/.test(content)) tags.push('H5'); if (/包装|颜值|独立|一次一|礼盒/.test(content)) tags.push('H6'); if (/吸收|营养糖|瓶瓶罐罐|DHA|补钙/.test(content)) tags.push('H7'); if (/副作用|长期|依赖|糖|添加/.test(content)) tags.push('H8'); return tags; } // ========================================================== // 8. 合并函数 // ========================================================== function mergeAll() { const items = []; scanDir(XHS_DIR, 'xhs', items); scanDir(DY_DIR, 'douyin', items); // 去重(同 id 合并) const dedup = new Map(); for (const it of items) dedup.set(it.id, it); const merged = { meta: { count: dedup.size, platforms: { xhs: ..., douyin: ... }, hypotheses: tally H1-H8, collected_at: Date.now(), }, items: [...dedup.values()], }; fs.writeFileSync(MERGED, JSON.stringify(merged, null, 2)); // 同时生成 JSONL 流 fs.writeFileSync(path.join(RAW, 'comments-flat.jsonl'), [...dedup.values()].map(x => JSON.stringify(x)).join('\n')); } // ========================================================== // 9. 主入口 + CLI // ========================================================== async function main() { const args = process.argv.slice(2); const batch = args.find(a => a.startsWith('--batch='))?.split('=')[1]; if (args.includes('--merge')) return mergeAll(); if (batch === 'all') { for (let i of [1,2,3]) await runBatch(i); } else if (batch) await runBatch(Number(batch)); else if (args.includes('--test')) await testSingle(); mergeAll(); } main().catch(console.error); ``` 详见 [`templates/collect.template.js`](./templates/collect.template.js)。 --- ## 📋 API 速查 ### TikHub · 小红书 **凭据**:`~/.openclaw/skills/xiaohongshu-search-notes/api-config.json` ```json { "currentToken": "gqsZHfMW..." } ``` **接口**: - `POST https://api.tikhub.io/api/v1/xiaohongshu/web_v1/search_notes` - body: `{ keyword, sort_type: "general", note_type: "normal", total_number: N }` - `POST https://api.tikhub.io/api/v1/xiaohongshu/web_v1/note_comments` - body: `{ note_id, cursor: "" }` **关键响应字段**: - Note: `id, user.nickname, title, desc, liked_count, comment_count, cover` - Comment: `content, like_count, user_info.nickname, ip_location, sub_comment_count` **Rate Limit**: - 约 **3-5 请求/秒** - 建议每请求间隔 **200-300ms** - 失败 **retry 2 次**,指数退避(1s → 3s → 失败放弃) --- ### VOC Skill · 抖音 **凭据**:`~/.openclaw/voc-credentials.json`(Parse session token,余额 ≥ 1) **接口**:通过 VOC Skill 封装(参考 `~/.openclaw/skills/social-*-comments/`) **关键调用**(伪代码): ```js const videos = await vocApiSearch('抖音', keyword, { max: 20 }); for (const video of videos) { const comments = await vocApiComments('抖音', video.id, { max: 10 }); // ... } ``` **Token 预飞**:跑 `node ~/.openclaw/tools/voc-token-preflight.js` 确认 status=valid 才能跑。 --- ### Amazon(跨境项目才需要) **凭据**:同 TikHub token(TikHub 也提供 Amazon 接口) **接口**: - `POST https://api.tikhub.io/api/v1/amazon/...` - 参数按 TikHub Amazon 文档 **场景**:肝纯片项目用了 Amazon 采集海外竞品(milk thistle / liver detox) --- ## 🔒 凭据与安全 ### 绝对不能 - ❌ 把 token 写死在代码里 - ❌ commit `.json` 凭据文件到 git - ❌ 在日志里打印完整 token ### 标准做法 ```js const tokenSnippet = (token || '').slice(0, 8) + '...'; console.log(` TikHub token: ✓ ${tokenSnippet}`); ``` --- ## 🚨 Token 失效与余额检查 ### 运行前预飞 ```powershell node ~/.openclaw/tools/voc-token-preflight.js ``` **5 种 status**: | status | 含义 | 处理 | |---|---|---| | `valid` | 正常 | 继续跑采集 | | `missing` | 凭据文件不存在 | `node ~/.openclaw/tools/set-voc-token.js ` | | `expired` | token 过期 | 去 apig-pay 登录拿新 token | | `insufficient_balance` | 余额 < 1 | 去 apig-pay 充值 | | `error` | 网络/API 异常 | 等 5 分钟重试 | --- ## 🛠️ 执行 Runbook ### 1. 首次启动检查 ```powershell # 1.1 凭据 Test-Path ~/.openclaw/skills/xiaohongshu-search-notes/api-config.json Test-Path ~/.openclaw/voc-credentials.json # 1.2 Token 预飞 node ~/.openclaw/tools/voc-token-preflight.js # 1.3 目录 mkdir -p docs/<品类>/raw/xhs docs/<品类>/raw/douyin ``` ### 2. 测试单 kw(MVP 验证) ```powershell node scripts/tools/<品类>-collect.js --test # 预期:采 1-2 个 kw 出 10-30 条真实评论 ``` ### 3. 正式批次 ```powershell # Batch 1(P0 基础盘) node scripts/tools/<品类>-collect.js --batch=1 # 预期:20-30 分钟,~500-1200 条 # Batch 2(P1 长尾 + 场景) node scripts/tools/<品类>-collect.js --batch=2 # 预期:15-25 分钟,~400-800 条 # Batch 3(P2 决策 + 相邻) node scripts/tools/<品类>-collect.js --batch=3 # 预期:12-20 分钟,~300-600 条 ``` ### 4. 合并 ```powershell node scripts/tools/<品类>-collect.js --merge # 生成 _merged.json + comments-flat.jsonl ``` ### 5. 查 audit.log ```powershell Get-Content docs/<品类>/raw/audit.log # 应该看到 OK 行,不能有 ERR 未处理 ``` --- ## 🔁 补采逻辑(Batch 4+) 如果 P5 发现 H6 / H7 覆盖不足: ```js // 在 BATCHES[4] 追加新 kw BATCHES[4] = { xhs: [ { keyword: '儿童无糖益生菌', max_notes: 10, max_comments_per: 8, product: '补采·H6' }, { keyword: '宝宝专用', max_notes: 8, max_comments_per: 5, product: '补采·H6' }, ], }; ``` ```powershell node scripts/tools/<品类>-collect.js --batch=4 --force node scripts/tools/<品类>-collect.js --merge node scripts/tools/gen-<品类>.js ``` --- ## 🧪 数据质量自检 采完后必须跑的 5 项检查: ```powershell node -e " const d = require('./docs/<品类>/raw/_merged.json'); const items = d.items; console.log('总量:', items.length); console.log('平台分布:', JSON.stringify(d.meta.platforms)); console.log('H 假设分布:'); for (let i=1; i<=8; i++) { const n = items.filter(it => (it.hypotheses||[]).includes('H'+i)).length; console.log(' H'+i+':', n, n < 100 ? '⚠️ 不足' : '✓'); } console.log('空内容:', items.filter(it => !it.content || it.content.length < 5).length); console.log('重复 content 前 40 字:', Array.from(new Map(items.map(it => [it.content.slice(0,40), it])).values()).length, '/', items.length); " ``` --- ## 🚫 常见失误 ### 失误 1 · 一次跑完所有 batch 不 merge - ❌ 症状:采了 50 kw 才发现 API 返回格式不对,全部废掉 - ✅ 做法:Batch 1 完先 `--merge` + 查看 `_merged.json` 验证格式 ### 失误 2 · 忘记去重 - ❌ 症状:同一条高赞评论在多个 kw 里都出现,在报告里刷 3 次 - ✅ 做法:`buildItem` 时用 `hash(platform + content + nickname)` 做 id + merge 时 Map 去重 ### 失误 3 · 对 `--force` 过于激进 - ❌ 症状:每次调试都加 `--force`,白白消耗 API 额度 - ✅ 做法:只在**关键词配置改变**时用 `--force`,日常调试用缓存 ### 失误 4 · 没有 audit.log - ❌ 症状:某个 kw 抓失败不知道,最终数据不完整 - ✅ 做法:每个 kw 必写一行 `OK/SKIP/ERR/keyword/count/ms` ### 失误 5 · 假设标注太宽泛 - ❌ 症状:每条 VOC 都打 H1-H8 全部(标签失去区分度) - ✅ 做法:严格关键词匹配 + `inferSentiment` 辅助(差评 / 好评 / 中性) --- ## ✅ 退出门控 Phase 4 完成的 **6 个条件**: 1. ☐ 总量 **≥ 目标量**(1500 / 2500 / 4000 按商业盘) 2. ☐ **每条 H 假设 ≥ 100 条证据**(不足就进 Batch 4 补采) 3. ☐ **平台分布合理**(XHS 40-70%,抖音 20-50%,电商按需) 4. ☐ `_merged.json` **可加载**,结构符合 `{meta, items}` 5. ☐ `audit.log` **无 ERR 未处理** 6. ☐ 去重后无 **完全相同 content 的 item** 超过 5 条 --- ## 📚 模板 - [`templates/collect.template.js`](./templates/collect.template.js) - [`prompts/P4-analysis.prompt.md`](./prompts/P4-analysis.prompt.md)