build_from_pages.js 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110
  1. /**
  2. * 把云机上抓到的 /sdcard/Download/_pages/p_*.json 合并成最终交付文件
  3. * 输出:<outDir>/tiktok_<id>_comments.json / _comments.txt / _stats.json
  4. * 用法: node build_from_pages.js <pagesDir> <outDir> [awemeId]
  5. */
  6. const fs = require('fs');
  7. const path = require('path');
  8. const pagesDir = process.argv[2] || path.join(__dirname, 'pages');
  9. const outDir = process.argv[3] || __dirname;
  10. const awemeId = process.argv[4] || '7552202593888603410';
  11. const files = fs.readdirSync(pagesDir).filter(f => /^p_\d+\.json$/.test(f))
  12. .map(f => ({ f, cursor: parseInt(f.match(/_(\d+)\./)[1], 10) }))
  13. .sort((a, b) => a.cursor - b.cursor);
  14. console.log('页面文件:', files.length, '个, cursor 范围', files[0].cursor, '~', files[files.length - 1].cursor);
  15. function toNum(v) { const n = parseInt(v, 10); return isNaN(n) ? 0 : n; }
  16. const seen = {};
  17. const out = [];
  18. let declaredTotal = 0;
  19. let rawCount = 0;
  20. const emptyPages = [];
  21. const badPages = [];
  22. for (const { f, cursor } of files) {
  23. let j;
  24. try { j = JSON.parse(fs.readFileSync(path.join(pagesDir, f), 'utf8')); }
  25. catch (e) { badPages.push({ f, reason: 'JSON解析失败' }); continue; }
  26. if (j.code !== 0 || !j.data) { badPages.push({ f, reason: 'code=' + j.code + ' ' + j.msg }); continue; }
  27. const cs = j.data.comments || [];
  28. rawCount += cs.length;
  29. if (toNum(j.data.total) > declaredTotal) declaredTotal = toNum(j.data.total);
  30. if (cs.length === 0) { emptyPages.push(cursor); continue; }
  31. for (const c of cs) {
  32. if (!c || !c.id || seen[c.id]) continue;
  33. seen[c.id] = 1;
  34. const u = c.user || {};
  35. out.push({
  36. cid: c.id,
  37. text: c.text || '',
  38. user: u.unique_id || u.nickname || '匿名',
  39. nickname: u.nickname || '',
  40. userId: u.id || '',
  41. secUid: u.sec_uid || '',
  42. region: u.region || '',
  43. likes: toNum(c.digg_count),
  44. createTime: c.create_time || 0,
  45. replyCount: toNum(c.reply_total),
  46. replies: []
  47. });
  48. }
  49. }
  50. const withR = out.filter(c => c.replyCount > 0);
  51. const sumR = withR.reduce((s, c) => s + c.replyCount, 0);
  52. const ts = out.map(c => c.createTime).filter(t => t > 0);
  53. console.log('原始条目(含重复):', rawCount);
  54. console.log('去重后唯一评论:', out.length);
  55. console.log('空页:', emptyPages.length ? emptyPages.join(',') : '无');
  56. console.log('异常页:', badPages.length ? JSON.stringify(badPages) : '无');
  57. console.log('声明 total:', declaredTotal);
  58. console.log('有回复的评论:', withR.length, '条, reply_total 合计:', sumR);
  59. if (ts.length) {
  60. console.log('时间跨度:', new Date(Math.min(...ts) * 1000).toISOString().slice(0, 10),
  61. '~', new Date(Math.max(...ts) * 1000).toISOString().slice(0, 10));
  62. }
  63. const byRegion = {};
  64. out.forEach(c => { byRegion[c.region || '?'] = (byRegion[c.region || '?'] || 0) + 1; });
  65. console.log('地区分布 Top8:', Object.entries(byRegion).sort((a, b) => b[1] - a[1]).slice(0, 8).map(x => x[0] + ':' + x[1]).join(' '));
  66. // ---- 落盘 ----
  67. fs.mkdirSync(outDir, { recursive: true });
  68. const base = path.join(outDir, 'tiktok_' + awemeId);
  69. fs.writeFileSync(base + '_comments.json', JSON.stringify(out, null, 2), 'utf8');
  70. const L = [];
  71. L.push('视频ID: ' + awemeId);
  72. L.push('来源: tikwm镜像(www.tikwm.com)');
  73. L.push('顶级评论: ' + out.length + ' 条 | 其中 ' + withR.length + ' 条带回复, 声明回复合计 ' + sumR + ' 条');
  74. L.push('声明 total(顶级+回复口径): ' + declaredTotal);
  75. L.push('');
  76. out.forEach((c, i) => {
  77. L.push((i + 1) + '. [' + c.user + ' | 赞' + c.likes + ' | 回复' + c.replyCount + ' | 地区' + c.region + '] ' + c.text);
  78. });
  79. fs.writeFileSync(base + '_comments.txt', L.join('\n'), 'utf8');
  80. const summary = {
  81. awemeId,
  82. commentSource: 'tikwm镜像',
  83. topLevelComments: out.length,
  84. commentsWithReplies: withR.length,
  85. declaredReplySum: sumR,
  86. repliesFetched: 0,
  87. declaredTotal,
  88. pagesFetched: files.length,
  89. emptyPages: emptyPages.length,
  90. builtAt: new Date().toISOString()
  91. };
  92. fs.writeFileSync(base + '_stats.json', JSON.stringify(summary, null, 2), 'utf8');
  93. console.log('\n已写出:');
  94. ['_comments.json', '_comments.txt', '_stats.json'].forEach(s => {
  95. const p = base + s;
  96. console.log(' ' + p + ' (' + fs.statSync(p).size + ' bytes)');
  97. });