ks-analyze.js 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178
  1. /**
  2. * KS 快手快聘 VOC 数据分析模块
  3. * 提供加载、筛选、统计分析功能
  4. */
  5. const fs = require('fs');
  6. const path = require('path');
  7. const ROOT = path.resolve(__dirname, '..', '..', '..');
  8. const RAW_DIR = path.join(ROOT, 'docs', 'raw', 'KS');
  9. function esc(s) { return String(s ?? '').replace(/[&<>"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c])); }
  10. function loadRawFiles() {
  11. const files = fs.readdirSync(RAW_DIR).filter(f => f.endsWith('.json') && f !== '-.json' && f !== '_merged.json' && f !== 'audit.log');
  12. const allItems = [];
  13. const byKeyword = {};
  14. const hypotheses = {
  15. 'H1': 0, 'H2': 0, 'H3': 0, 'H4': 0,
  16. 'H5': 0, 'H6': 0, 'H7': 0, 'H8': 0
  17. };
  18. const keywordHypothesisMap = {
  19. '快聘': ['H1', 'H2'],
  20. '直播带岗': ['H1', 'H2'],
  21. '蓝领找工作': ['H1'],
  22. '工资靠谱': ['H3'],
  23. '真实薪资': ['H3', 'H8'],
  24. '工资日结': ['H8'],
  25. '包吃包住': ['H4'],
  26. '当天入职': ['H5'],
  27. '求职被骗': ['H6'],
  28. '入职被坑': ['H6'],
  29. '黑中介': ['H7'],
  30. '押金不退': ['H7']
  31. };
  32. let totalVideos = 0;
  33. for (const file of files) {
  34. try {
  35. const filepath = path.join(RAW_DIR, file);
  36. const data = JSON.parse(fs.readFileSync(filepath, 'utf-8'));
  37. const keyword = data.keyword || path.basename(file, '.json');
  38. const hs = keywordHypothesisMap[keyword] || [];
  39. const videoCount = data.top_videos?.length || 0;
  40. totalVideos += videoCount;
  41. byKeyword[keyword] = { videos: videoCount, comments: 0, items: [] };
  42. const commentsObj = data.comments || {};
  43. let keywordCommentCount = 0;
  44. for (const [videoId, comments] of Object.entries(commentsObj)) {
  45. for (const comment of comments) {
  46. const item = {
  47. id: comment.id,
  48. content: comment.content,
  49. likes: comment.like_count || 0,
  50. ip_location: comment.ip_location || '',
  51. create_time: comment.create_time,
  52. nickname: comment.user?.nickname || '匿名用户',
  53. keyword: keyword,
  54. video_id: videoId,
  55. platform: 'kuaishou'
  56. };
  57. allItems.push(item);
  58. byKeyword[keyword].items.push(item);
  59. byKeyword[keyword].comments++;
  60. keywordCommentCount++;
  61. if (item.likes >= 3 && item.content && item.content.length >= 8) {
  62. hs.forEach(h => { if (hypotheses[h] !== undefined) hypotheses[h]++; });
  63. }
  64. }
  65. }
  66. } catch (e) {
  67. console.warn(`⚠️ 跳过损坏文件: ${file}`);
  68. }
  69. }
  70. return {
  71. items: allItems,
  72. byKeyword,
  73. hypotheses,
  74. totalVideos,
  75. totalComments: allItems.length,
  76. keywords: Object.keys(byKeyword).length,
  77. collectedAt: new Date().toISOString().slice(0,10)
  78. };
  79. }
  80. function getMeta(data) {
  81. if (!data) return {
  82. comments: 0, videos: 0, keywords: 0, hypotheses: {},
  83. collectedAt: new Date().toISOString().slice(0,10)
  84. };
  85. return {
  86. comments: data.totalComments,
  87. videos: data.totalVideos,
  88. keywords: data.keywords,
  89. hypotheses: data.hypotheses,
  90. collectedAt: data.collectedAt
  91. };
  92. }
  93. function getFilteredItems(data, minChars = 8, minLikes = 3) {
  94. if (!data || !data.items) return [];
  95. return data.items.filter(item => {
  96. const content = item.content || '';
  97. if (content.length < minChars) return false;
  98. if ((item.likes || 0) < minLikes) return false;
  99. return true;
  100. });
  101. }
  102. function getByHypothesis(data) {
  103. const keywordHypothesisMap = {
  104. '快聘': 'H1', '直播带岗': 'H1', '蓝领找工作': 'H1',
  105. '工资靠谱': 'H3', '真实薪资': 'H3',
  106. '工资日结': 'H8',
  107. '包吃包住': 'H4',
  108. '当天入职': 'H5',
  109. '求职被骗': 'H6', '入职被坑': 'H6',
  110. '黑中介': 'H7', '押金不退': 'H7'
  111. };
  112. const byH = { H1:[], H2:[], H3:[], H4:[], H5:[], H6:[], H7:[], H8:[] };
  113. const filtered = getFilteredItems(data);
  114. filtered.forEach(item => {
  115. const h = keywordHypothesisMap[item.keyword] || 'H1';
  116. if (byH[h]) byH[h].push(item);
  117. });
  118. return byH;
  119. }
  120. function getTopByKeyword(data, n = 10) {
  121. const filtered = getFilteredItems(data);
  122. const byKw = {};
  123. filtered.forEach(item => {
  124. const kw = item.keyword || '未知';
  125. if (!byKw[kw]) byKw[kw] = [];
  126. byKw[kw].push(item);
  127. });
  128. const result = [];
  129. Object.entries(byKw).forEach(([kw, arr]) => {
  130. const sorted = arr.sort((a, b) => (b.likes || 0) - (a.likes || 0)).slice(0, n);
  131. result.push({ keyword: kw, items: sorted, total: arr.length });
  132. });
  133. return result.sort((a, b) => b.total - a.total);
  134. }
  135. function getTopLiked(data, n = 20) {
  136. return getFilteredItems(data)
  137. .sort((a, b) => (b.likes || 0) - (a.likes || 0))
  138. .slice(0, n);
  139. }
  140. function getHighRiskVOC(data, n = 10) {
  141. const riskKeywords = ['黑中介', '押金不退', '求职被骗', '入职被坑'];
  142. const filtered = getFilteredItems(data);
  143. return filtered
  144. .filter(item => riskKeywords.includes(item.keyword))
  145. .sort((a, b) => (b.likes || 0) - (a.likes || 0))
  146. .slice(0, n);
  147. }
  148. module.exports = {
  149. loadRawFiles,
  150. getMeta,
  151. getFilteredItems,
  152. getByHypothesis,
  153. getTopByKeyword,
  154. getTopLiked,
  155. getHighRiskVOC
  156. };