/** * XReader — 解析 X (Twitter) 官方导出的 tweets.js 文件 * * 文件格式:window.YTD.tweets.part0 = [{tweet: {...}}, ...] * 提取字段:id_str, created_at, full_text, entities.hashtags[] */ const fs = require('fs'); class XReader { constructor(filePath) { this.filePath = filePath; } /** * 解析 tweets.js,返回推文数组 * @returns {Array<{id, createdAt, text, hashtags}>} */ parseTweets() { const raw = fs.readFileSync(this.filePath, 'utf-8'); // 去掉 window.YTD.tweets.part0 = 前缀,取出 JSON 数组部分 const jsonStart = raw.indexOf('['); if (jsonStart === -1) { throw new Error('tweets.js 格式错误:找不到 JSON 数组起始符 ['); } let jsonStr = raw.slice(jsonStart).trim(); // 某些导出版本末尾有分号 if (jsonStr.endsWith(';')) { jsonStr = jsonStr.slice(0, -1); } let rawTweets; try { rawTweets = JSON.parse(jsonStr); } catch (e) { throw new Error('tweets.js JSON 解析失败:' + e.message); } const tweets = []; for (const item of rawTweets) { const t = item.tweet || item; // 兼容两种格式 if (!t) continue; const hashtags = (t.entities?.hashtags || []).map(h => typeof h === 'string' ? h : (h.text || h.tag || '') ).filter(Boolean); tweets.push({ id: t.id_str || t.id || '', createdAt: t.created_at || '', text: t.full_text || t.text || '', hashtags, }); } return tweets; } } module.exports = { XReader };