| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134 |
- // 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);
|