/** * KS 快手快聘 VOC 数据分析模块 * 提供加载、筛选、统计分析功能 */ const fs = require('fs'); const path = require('path'); const ROOT = path.resolve(__dirname, '..', '..', '..'); const RAW_DIR = path.join(ROOT, 'docs', 'raw', 'KS'); function esc(s) { return String(s ?? '').replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c])); } function loadRawFiles() { const files = fs.readdirSync(RAW_DIR).filter(f => f.endsWith('.json') && f !== '-.json' && f !== '_merged.json' && f !== 'audit.log'); const allItems = []; const byKeyword = {}; const hypotheses = { 'H1': 0, 'H2': 0, 'H3': 0, 'H4': 0, 'H5': 0, 'H6': 0, 'H7': 0, 'H8': 0 }; const keywordHypothesisMap = { '快聘': ['H1', 'H2'], '直播带岗': ['H1', 'H2'], '蓝领找工作': ['H1'], '工资靠谱': ['H3'], '真实薪资': ['H3', 'H8'], '工资日结': ['H8'], '包吃包住': ['H4'], '当天入职': ['H5'], '求职被骗': ['H6'], '入职被坑': ['H6'], '黑中介': ['H7'], '押金不退': ['H7'] }; let totalVideos = 0; for (const file of files) { try { const filepath = path.join(RAW_DIR, file); const data = JSON.parse(fs.readFileSync(filepath, 'utf-8')); const keyword = data.keyword || path.basename(file, '.json'); const hs = keywordHypothesisMap[keyword] || []; const videoCount = data.top_videos?.length || 0; totalVideos += videoCount; byKeyword[keyword] = { videos: videoCount, comments: 0, items: [] }; const commentsObj = data.comments || {}; let keywordCommentCount = 0; for (const [videoId, comments] of Object.entries(commentsObj)) { for (const comment of comments) { const item = { id: comment.id, content: comment.content, likes: comment.like_count || 0, ip_location: comment.ip_location || '', create_time: comment.create_time, nickname: comment.user?.nickname || '匿名用户', keyword: keyword, video_id: videoId, platform: 'kuaishou' }; allItems.push(item); byKeyword[keyword].items.push(item); byKeyword[keyword].comments++; keywordCommentCount++; if (item.likes >= 3 && item.content && item.content.length >= 8) { hs.forEach(h => { if (hypotheses[h] !== undefined) hypotheses[h]++; }); } } } } catch (e) { console.warn(`⚠️ 跳过损坏文件: ${file}`); } } return { items: allItems, byKeyword, hypotheses, totalVideos, totalComments: allItems.length, keywords: Object.keys(byKeyword).length, collectedAt: new Date().toISOString().slice(0,10) }; } function getMeta(data) { if (!data) return { comments: 0, videos: 0, keywords: 0, hypotheses: {}, collectedAt: new Date().toISOString().slice(0,10) }; return { comments: data.totalComments, videos: data.totalVideos, keywords: data.keywords, hypotheses: data.hypotheses, collectedAt: data.collectedAt }; } function getFilteredItems(data, minChars = 8, minLikes = 3) { if (!data || !data.items) return []; return data.items.filter(item => { const content = item.content || ''; if (content.length < minChars) return false; if ((item.likes || 0) < minLikes) return false; return true; }); } function getByHypothesis(data) { const keywordHypothesisMap = { '快聘': 'H1', '直播带岗': 'H1', '蓝领找工作': 'H1', '工资靠谱': 'H3', '真实薪资': 'H3', '工资日结': 'H8', '包吃包住': 'H4', '当天入职': 'H5', '求职被骗': 'H6', '入职被坑': 'H6', '黑中介': 'H7', '押金不退': 'H7' }; const byH = { H1:[], H2:[], H3:[], H4:[], H5:[], H6:[], H7:[], H8:[] }; const filtered = getFilteredItems(data); filtered.forEach(item => { const h = keywordHypothesisMap[item.keyword] || 'H1'; if (byH[h]) byH[h].push(item); }); return byH; } function getTopByKeyword(data, n = 10) { const filtered = getFilteredItems(data); const byKw = {}; filtered.forEach(item => { const kw = item.keyword || '未知'; if (!byKw[kw]) byKw[kw] = []; byKw[kw].push(item); }); const result = []; Object.entries(byKw).forEach(([kw, arr]) => { const sorted = arr.sort((a, b) => (b.likes || 0) - (a.likes || 0)).slice(0, n); result.push({ keyword: kw, items: sorted, total: arr.length }); }); return result.sort((a, b) => b.total - a.total); } function getTopLiked(data, n = 20) { return getFilteredItems(data) .sort((a, b) => (b.likes || 0) - (a.likes || 0)) .slice(0, n); } function getHighRiskVOC(data, n = 10) { const riskKeywords = ['黑中介', '押金不退', '求职被骗', '入职被坑']; const filtered = getFilteredItems(data); return filtered .filter(item => riskKeywords.includes(item.keyword)) .sort((a, b) => (b.likes || 0) - (a.likes || 0)) .slice(0, n); } module.exports = { loadRawFiles, getMeta, getFilteredItems, getByHypothesis, getTopByKeyword, getTopLiked, getHighRiskVOC };