| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106 |
- /**
- * 把 /replies/*.json 的子回复合并进顶级评论 JSON
- * 用法: node merge_replies.js <comments.json> <repliesDir> <outDir> [awemeId]
- */
- const fs = require('fs');
- const path = require('path');
- const inFile = process.argv[2];
- const repliesDir = process.argv[3];
- const outDir = process.argv[4] || path.dirname(inFile);
- const awemeId = process.argv[5] || '7552202593888603410';
- const comments = JSON.parse(fs.readFileSync(inFile, 'utf8'));
- const byId = {};
- comments.forEach(c => { byId[c.cid] = c; c.replies = c.replies || []; });
- const stats = { files: 0, badFiles: 0, rawReplies: 0, uniqueReplies: 0, commentsMatched: 0, orphan: 0 };
- let files = [];
- if (fs.existsSync(repliesDir)) {
- files = fs.readdirSync(repliesDir).filter(f => f.endsWith('.json'));
- }
- console.log('回复页面文件:', files.length, '个');
- // 按 cid 分组,保证按 cursor 顺序处理
- const byComment = {};
- files.forEach(f => {
- const m = /^(\d+)_(\d+)\.json$/.exec(f);
- if (!m) return;
- const cid = m[1], cursor = parseInt(m[2], 10);
- (byComment[cid] = byComment[cid] || []).push({ f, cursor });
- });
- console.log('涉及评论数:', Object.keys(byComment).length);
- const seenReply = {};
- for (const cid of Object.keys(byComment)) {
- byComment[cid].sort((a, b) => a.cursor - b.cursor);
- const parent = byId[cid];
- if (!parent) { stats.orphan++; continue; }
- let n = 0;
- for (const { f } of byComment[cid]) {
- stats.files++;
- let j;
- try { j = JSON.parse(fs.readFileSync(path.join(repliesDir, f), 'utf8')); }
- catch (e) { stats.badFiles++; continue; }
- if (j.code !== 0 || !j.data) continue;
- for (const r of (j.data.comments || [])) {
- stats.rawReplies++;
- if (!r || !r.id || seenReply[r.id]) continue;
- seenReply[r.id] = 1;
- stats.uniqueReplies++;
- const u = r.user || {};
- parent.replies.push({
- cid: r.id,
- text: r.text || '',
- user: u.unique_id || u.nickname || '匿名',
- nickname: u.nickname || '',
- userId: u.id || '',
- region: u.region || '',
- likes: parseInt(r.digg_count, 10) || 0,
- createTime: r.create_time || 0,
- replyCount: parseInt(r.reply_total, 10) || 0
- });
- n++;
- }
- }
- if (n > 0) stats.commentsMatched++;
- }
- console.log('合并统计:', JSON.stringify(stats, null, 2));
- // 排序:每个父评论的回复按点赞降序
- comments.forEach(c => c.replies.sort((a, b) => b.likes - a.likes));
- const withR = comments.filter(c => (c.replies || []).length > 0);
- const totalReplies = comments.reduce((s, c) => s + (c.replies || []).length, 0);
- fs.mkdirSync(outDir, { recursive: true });
- const base = path.join(outDir, 'tiktok_' + awemeId);
- fs.writeFileSync(base + '_comments.json', JSON.stringify(comments, null, 2), 'utf8');
- const L = [];
- L.push('视频ID: ' + awemeId);
- L.push('来源: tikwm镜像(www.tikwm.com) 顶级评论 ' + comments.length + ' 条');
- L.push('子回复: ' + totalReplies + ' 条(覆盖 ' + withR.length + ' 条评论)');
- L.push('抓取时间: ' + new Date().toISOString());
- L.push('');
- comments.forEach((c, i) => {
- L.push((i + 1) + '. [' + c.user + ' | 赞' + c.likes + ' | 回复' + c.replyCount + ' | 地区' + c.region + '] ' + c.text);
- (c.replies || []).forEach(r => {
- L.push(' └─ [' + r.user + ' | 赞' + r.likes + '] ' + r.text);
- });
- });
- fs.writeFileSync(base + '_comments.txt', L.join('\n'), 'utf8');
- const summary = JSON.parse(fs.readFileSync(base + '_stats.json', 'utf8'));
- summary.repliesFetched = totalReplies;
- summary.commentsWithRepliesFetched = withR.length;
- summary.replyFiles = stats.files;
- summary.mergedAt = new Date().toISOString();
- summary.note = 'tikwm 顶级评论上限约 10369;子回复按热度抓取,可在云机断点续传';
- fs.writeFileSync(base + '_stats.json', JSON.stringify(summary, null, 2), 'utf8');
- console.log('\n已写出:');
- ['_comments.json', '_comments.txt', '_stats.json'].forEach(s => {
- const p = base + s;
- console.log(' ' + p + ' (' + fs.statSync(p).size + ' bytes)');
- });
|