| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178 |
- /**
- * 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
- };
|