// Self-audit for VOC JSON data using the real field names. const fs = require('fs'); const path = require('path'); const products = ['liver', 'probiotic', 'monkey']; const labels = { liver: '江中肝纯片', probiotic: '儿童乳酸菌素片', monkey: '江中猴菇饮', }; const lines = []; const push = (...a) => lines.push(a.join(' ')); for (const p of products) { const file = `data/jiangzhong-${p}-voc.json`; const d = JSON.parse(fs.readFileSync(file, 'utf-8')); push('\n==========', labels[p], '(' + p + ')', '=========='); push('file size:', (fs.statSync(file).size / 1024).toFixed(1) + 'KB', '| collected_at:', d.collected_at); const kwRaw = d.keywords; const kwStr = Array.isArray(kwRaw) ? kwRaw.join(' / ') : (kwRaw && typeof kwRaw === 'object') ? Object.entries(kwRaw).map(([k, v]) => k + ':[' + (Array.isArray(v) ? v.join(',') : v) + ']').join(' | ') : String(kwRaw || ''); push('category:', d.category, '| keywords:', kwStr); // ---- XHS ---- push('\n[XHS 小红书]'); const byKw = d.xiaohongshu?.by_keyword || {}; const allNotes = []; const allKolMap = new Map(); for (const kw of Object.keys(byKw)) { const bkt = byKw[kw]; const notes = bkt?.notes || []; push(' · keyword "' + kw + '" -> notes:', notes.length, '| kols:', (bkt?.kols || []).length); allNotes.push(...notes); for (const k of (bkt?.kols || [])) { const uid = k.user_id || k.userId || k.nickname; if (!allKolMap.has(uid)) allKolMap.set(uid, k); } } const uniqNotes = new Map(); for (const n of allNotes) uniqNotes.set(n.note_id || n.id || n.title, n); const unique = [...uniqNotes.values()]; push(' 合并去重 notes:', unique.length, '| 合并去重 KOL:', allKolMap.size); if (unique.length) { const withLike = unique.filter(n => (n.likes || n.liked_count || 0) > 0).length; const withComment = unique.filter(n => (n.comments_count || n.comment_count || 0) > 0).length; const top = [...unique].sort((a, b) => (+(b.likes || b.liked_count) || 0) - (+(a.likes || a.liked_count) || 0))[0]; push(' 有赞:', withLike, '| 有评论:', withComment); push(' top note:', (top?.title || top?.display_title || '').slice(0, 60), '| likes:', top?.likes || top?.liked_count); // 分层 const tier = { '万粉+': 0, '千粉+': 0, '<1k': 0 }; for (const k of allKolMap.values()) { const f = +(k.fansCount || k.fans_count || k.fans || 0); if (f >= 10000) tier['万粉+']++; else if (f >= 1000) tier['千粉+']++; else tier['<1k']++; } push(' KOL 分层:', JSON.stringify(tier)); } // ---- Douyin ---- push('\n[抖音]'); const dy = d.douyin || {}; push(' share_url:', dy.share_url, '| cookie_loaded:', dy.cookie_loaded, '| share_type:', dy.share_type); const up = dy.user_profile?.data?.user || dy.user_profile?.user || dy.user_profile || {}; push(' user nickname:', up.nickname, '| sec_uid:', (up.sec_uid || '').slice(0, 16) + '...'); push(' 粉丝:', up.follower_count, '| 作品:', up.aweme_count, '| 获赞:', up.total_favorited, '| 关注:', up.following_count); push(' 签名:', (up.signature || '').replace(/\n/g, ' ').slice(0, 80)); const posts = dy.user_posts?.data?.aweme_list || dy.user_posts?.aweme_list || dy.user_posts || []; push(' posts:', Array.isArray(posts) ? posts.length : '不是数组'); if (Array.isArray(posts) && posts.length) { const totalPlay = posts.reduce((s, po) => s + (+(po.statistics?.play_count || po.play_count || 0)), 0); const totalLike = posts.reduce((s, po) => s + (+(po.statistics?.digg_count || po.digg_count || 0)), 0); push(' 累计播放:', totalPlay.toLocaleString(), '| 累计赞:', totalLike.toLocaleString()); const top = [...posts].sort((a, b) => (+(b.statistics?.digg_count || b.digg_count) || 0) - (+(a.statistics?.digg_count || a.digg_count) || 0))[0]; push(' top post:', (top?.desc || '').replace(/\n/g, ' ').slice(0, 80)); push(' 赞:', top?.statistics?.digg_count || top?.digg_count, '| 评论:', top?.statistics?.comment_count || top?.comment_count, '| 播放:', top?.statistics?.play_count || top?.play_count); } // 搜索分支 const searches = dy.searches || {}; if (searches && Object.keys(searches).length) { push(' search keywords:', Object.keys(searches).join(', ')); for (const kw of Object.keys(searches)) { const items = searches[kw]?.data?.data || searches[kw]?.data || searches[kw] || []; push(' · "' + kw + '" -> hits:', Array.isArray(items) ? items.length : JSON.stringify(items).slice(0, 80)); } } else { push(' searches: 空(share-url 模式不跑搜索)'); } // ---- Amazon ---- push('\n[Amazon]'); const amz = d.amazon || {}; const merged = amz.merged_products || []; const details = amz.product_details || {}; const reviewsByAsin = amz.reviews_by_asin || {}; push(' merged_products:', merged.length); push(' product_details:', Object.keys(details).length); const withReviews = Object.keys(reviewsByAsin).filter(a => { const r = reviewsByAsin[a]; const arr = Array.isArray(r) ? r : (Array.isArray(r?.data) ? r.data : Object.values(r?.data?.reviews || r?.reviews || r || {}).filter(x => x && typeof x === 'object')); return arr.length > 0; }); push(' 有评论的 ASIN:', withReviews.length); let totalReviews = 0; for (const a of Object.keys(reviewsByAsin)) { const r = reviewsByAsin[a]; const arr = Array.isArray(r) ? r : (Array.isArray(r?.data) ? r.data : Object.values(r?.data?.reviews || r?.reviews || r || {}).filter(x => x && typeof x === 'object')); totalReviews += arr.length; } push(' 总评论条数:', totalReviews); // amazon by_keyword const akw = amz.by_keyword || {}; push(' amazon 关键词覆盖:', Object.keys(akw).length, '个'); for (const kw of Object.keys(akw)) { const v = akw[kw]; const products = Array.isArray(v) ? v : (Array.isArray(v?.products) ? v.products : (Array.isArray(v?.data) ? v.data : [])); push(' · "' + kw + '" -> products:', products.length); } // 前 5 合并产品 push(' top5 merged products (by 评论 desc):'); const top5 = [...merged].sort((a, b) => (+(b.ReviewCount || b.reviewCount || 0)) - (+(a.ReviewCount || a.reviewCount || 0))).slice(0, 5); for (const p5 of top5) { push(' ·', p5.Asin || p5.asin, '|', (p5.Title || p5.title || '').slice(0, 60), '| 评分:', p5.Star || p5.star || p5.Rating || p5.rating, '| 评论:', p5.ReviewCount || p5.reviewCount); } } const outPath = path.resolve('data', 'voc-self-audit.txt'); fs.writeFileSync(outPath, lines.join('\n'), 'utf-8'); console.log('Wrote:', outPath); console.log('Total lines:', lines.length);