voc-self-audit.js 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134
  1. // Self-audit for VOC JSON data using the real field names.
  2. const fs = require('fs');
  3. const path = require('path');
  4. const products = ['liver', 'probiotic', 'monkey'];
  5. const labels = {
  6. liver: '江中肝纯片',
  7. probiotic: '儿童乳酸菌素片',
  8. monkey: '江中猴菇饮',
  9. };
  10. const lines = [];
  11. const push = (...a) => lines.push(a.join(' '));
  12. for (const p of products) {
  13. const file = `data/jiangzhong-${p}-voc.json`;
  14. const d = JSON.parse(fs.readFileSync(file, 'utf-8'));
  15. push('\n==========', labels[p], '(' + p + ')', '==========');
  16. push('file size:', (fs.statSync(file).size / 1024).toFixed(1) + 'KB', '| collected_at:', d.collected_at);
  17. const kwRaw = d.keywords;
  18. const kwStr = Array.isArray(kwRaw)
  19. ? kwRaw.join(' / ')
  20. : (kwRaw && typeof kwRaw === 'object')
  21. ? Object.entries(kwRaw).map(([k, v]) => k + ':[' + (Array.isArray(v) ? v.join(',') : v) + ']').join(' | ')
  22. : String(kwRaw || '');
  23. push('category:', d.category, '| keywords:', kwStr);
  24. // ---- XHS ----
  25. push('\n[XHS 小红书]');
  26. const byKw = d.xiaohongshu?.by_keyword || {};
  27. const allNotes = [];
  28. const allKolMap = new Map();
  29. for (const kw of Object.keys(byKw)) {
  30. const bkt = byKw[kw];
  31. const notes = bkt?.notes || [];
  32. push(' · keyword "' + kw + '" -> notes:', notes.length, '| kols:', (bkt?.kols || []).length);
  33. allNotes.push(...notes);
  34. for (const k of (bkt?.kols || [])) {
  35. const uid = k.user_id || k.userId || k.nickname;
  36. if (!allKolMap.has(uid)) allKolMap.set(uid, k);
  37. }
  38. }
  39. const uniqNotes = new Map();
  40. for (const n of allNotes) uniqNotes.set(n.note_id || n.id || n.title, n);
  41. const unique = [...uniqNotes.values()];
  42. push(' 合并去重 notes:', unique.length, '| 合并去重 KOL:', allKolMap.size);
  43. if (unique.length) {
  44. const withLike = unique.filter(n => (n.likes || n.liked_count || 0) > 0).length;
  45. const withComment = unique.filter(n => (n.comments_count || n.comment_count || 0) > 0).length;
  46. const top = [...unique].sort((a, b) => (+(b.likes || b.liked_count) || 0) - (+(a.likes || a.liked_count) || 0))[0];
  47. push(' 有赞:', withLike, '| 有评论:', withComment);
  48. push(' top note:', (top?.title || top?.display_title || '').slice(0, 60), '| likes:', top?.likes || top?.liked_count);
  49. // 分层
  50. const tier = { '万粉+': 0, '千粉+': 0, '<1k': 0 };
  51. for (const k of allKolMap.values()) {
  52. const f = +(k.fansCount || k.fans_count || k.fans || 0);
  53. if (f >= 10000) tier['万粉+']++;
  54. else if (f >= 1000) tier['千粉+']++;
  55. else tier['<1k']++;
  56. }
  57. push(' KOL 分层:', JSON.stringify(tier));
  58. }
  59. // ---- Douyin ----
  60. push('\n[抖音]');
  61. const dy = d.douyin || {};
  62. push(' share_url:', dy.share_url, '| cookie_loaded:', dy.cookie_loaded, '| share_type:', dy.share_type);
  63. const up = dy.user_profile?.data?.user || dy.user_profile?.user || dy.user_profile || {};
  64. push(' user nickname:', up.nickname, '| sec_uid:', (up.sec_uid || '').slice(0, 16) + '...');
  65. push(' 粉丝:', up.follower_count, '| 作品:', up.aweme_count, '| 获赞:', up.total_favorited, '| 关注:', up.following_count);
  66. push(' 签名:', (up.signature || '').replace(/\n/g, ' ').slice(0, 80));
  67. const posts = dy.user_posts?.data?.aweme_list || dy.user_posts?.aweme_list || dy.user_posts || [];
  68. push(' posts:', Array.isArray(posts) ? posts.length : '不是数组');
  69. if (Array.isArray(posts) && posts.length) {
  70. const totalPlay = posts.reduce((s, po) => s + (+(po.statistics?.play_count || po.play_count || 0)), 0);
  71. const totalLike = posts.reduce((s, po) => s + (+(po.statistics?.digg_count || po.digg_count || 0)), 0);
  72. push(' 累计播放:', totalPlay.toLocaleString(), '| 累计赞:', totalLike.toLocaleString());
  73. const top = [...posts].sort((a, b) => (+(b.statistics?.digg_count || b.digg_count) || 0) - (+(a.statistics?.digg_count || a.digg_count) || 0))[0];
  74. push(' top post:', (top?.desc || '').replace(/\n/g, ' ').slice(0, 80));
  75. push(' 赞:', top?.statistics?.digg_count || top?.digg_count, '| 评论:', top?.statistics?.comment_count || top?.comment_count, '| 播放:', top?.statistics?.play_count || top?.play_count);
  76. }
  77. // 搜索分支
  78. const searches = dy.searches || {};
  79. if (searches && Object.keys(searches).length) {
  80. push(' search keywords:', Object.keys(searches).join(', '));
  81. for (const kw of Object.keys(searches)) {
  82. const items = searches[kw]?.data?.data || searches[kw]?.data || searches[kw] || [];
  83. push(' · "' + kw + '" -> hits:', Array.isArray(items) ? items.length : JSON.stringify(items).slice(0, 80));
  84. }
  85. } else {
  86. push(' searches: 空(share-url 模式不跑搜索)');
  87. }
  88. // ---- Amazon ----
  89. push('\n[Amazon]');
  90. const amz = d.amazon || {};
  91. const merged = amz.merged_products || [];
  92. const details = amz.product_details || {};
  93. const reviewsByAsin = amz.reviews_by_asin || {};
  94. push(' merged_products:', merged.length);
  95. push(' product_details:', Object.keys(details).length);
  96. const withReviews = Object.keys(reviewsByAsin).filter(a => {
  97. const r = reviewsByAsin[a];
  98. 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'));
  99. return arr.length > 0;
  100. });
  101. push(' 有评论的 ASIN:', withReviews.length);
  102. let totalReviews = 0;
  103. for (const a of Object.keys(reviewsByAsin)) {
  104. const r = reviewsByAsin[a];
  105. 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'));
  106. totalReviews += arr.length;
  107. }
  108. push(' 总评论条数:', totalReviews);
  109. // amazon by_keyword
  110. const akw = amz.by_keyword || {};
  111. push(' amazon 关键词覆盖:', Object.keys(akw).length, '个');
  112. for (const kw of Object.keys(akw)) {
  113. const v = akw[kw];
  114. const products = Array.isArray(v) ? v : (Array.isArray(v?.products) ? v.products : (Array.isArray(v?.data) ? v.data : []));
  115. push(' · "' + kw + '" -> products:', products.length);
  116. }
  117. // 前 5 合并产品
  118. push(' top5 merged products (by 评论 desc):');
  119. const top5 = [...merged].sort((a, b) => (+(b.ReviewCount || b.reviewCount || 0)) - (+(a.ReviewCount || a.reviewCount || 0))).slice(0, 5);
  120. for (const p5 of top5) {
  121. push(' ·', p5.Asin || p5.asin, '|', (p5.Title || p5.title || '').slice(0, 60), '| 评分:', p5.Star || p5.star || p5.Rating || p5.rating, '| 评论:', p5.ReviewCount || p5.reviewCount);
  122. }
  123. }
  124. const outPath = path.resolve('data', 'voc-self-audit.txt');
  125. fs.writeFileSync(outPath, lines.join('\n'), 'utf-8');
  126. console.log('Wrote:', outPath);
  127. console.log('Total lines:', lines.length);