// ==UserScript== // @name 吾志日记导出器 (Wuzhi.me Exporter) // @namespace http://tampermonkey.net/ // @version 2.0 // @description 通过日历精确导出吾志 (wuzhi.me) 的所有日记 // @author karate3008 // @match *://wuzhi.me/* // @match *://www.wuzhi.me/* // @grant none // ==/UserScript== (function () { 'use strict'; // ========================================== // 状态变量 // ========================================== let isExporting = false; let allDiaries = []; let processedMonths = new Set(); // 避免重复处理月份 let totalDays = 0; // ========================================== // 创建悬浮 UI // ========================================== // ========================================== // 初始化逻辑 // ========================================== function init() { if (document.getElementById('wz-export-ui')) return; // 防止重复初始化 const ui = document.createElement('div'); ui.id = 'wz-export-ui'; ui.innerHTML = `

📔 吾志导出

v2.1
⚠️ 提示:请先从日历跳转到有日记的月份,程序将自动向前回溯导出全部数据。
准备就绪,点击开始...
当前月份
-
处理天数
0
收集日记
0
已回溯
0
Wecho (回响)
每一缕思绪,都是一个漂流瓶
永久免费 · 端到端加密 · 纯粹记录
`; document.body.appendChild(ui); // 获取元素引用 statusDiv = document.getElementById('wz-status'); startBtn = document.getElementById('wz-start-btn'); stopBtn = document.getElementById('wz-stop-btn'); importBtn = document.getElementById('wz-import-btn'); introDiv = document.getElementById('wz-intro'); pMonth = document.getElementById('wz-p-month'); pDays = document.getElementById('wz-p-days'); pCount = document.getElementById('wz-p-count'); pMonths = document.getElementById('wz-p-months'); progressDiv = document.getElementById('wz-progress'); // 绑定事件 startBtn.onclick = startExport; stopBtn.onclick = stopExport; importBtn.onclick = () => window.open('https://wecho.me', '_blank'); console.log('[吾志导出] UI 初始化完成'); } // 全局变量声明 (在 init 中赋值) let statusDiv, startBtn, stopBtn, importBtn, introDiv, pMonth, pDays, pCount, pMonths, progressDiv; // ========================================== // 启动逻辑 // ========================================== console.log('[吾志导出] 脚本已注入,准备初始化...'); function tryInit() { // 如果 UI 已存在,不再重复 if (document.getElementById('wz-export-ui')) return; // 如果 body 还没准备好,延迟重试 if (!document.body) { console.log('[吾志导出] document.body 未就绪,500ms 后重试...'); setTimeout(tryInit, 500); return; } console.log('[吾志导出] document.body 就绪,执行 init()...'); init(); } // 尝试策略 1: 立即尝试 tryInit(); // 尝试策略 2: 页面加载完成后 window.addEventListener('load', tryInit); // 尝试策略 3: DOM 内容加载后 document.addEventListener('DOMContentLoaded', tryInit); // 尝试策略 4: 如果当前已经是完成状态 if (document.readyState === 'complete' || document.readyState === 'interactive') { tryInit(); } // ========================================== // 工具函数 // ========================================== function updateStatus(msg) { statusDiv.innerHTML = msg; console.log(`[吾志导出] ${msg.replace(/<[^>]*>/g, ' ')}`); } function updateProgress(month, days, count, months) { if (month !== null) pMonth.textContent = month; if (days !== null) { totalDays += days; pDays.textContent = totalDays; } pCount.textContent = allDiaries.length; if (months !== null) pMonths.textContent = months; } function sleep(ms) { return new Promise(r => setTimeout(r, ms)); } // ========================================== // 核心逻辑 // ========================================== async function startExport() { if (isExporting) return; isExporting = true; allDiaries = []; processedMonths.clear(); totalDays = 0; startBtn.style.display = 'none'; stopBtn.style.display = 'block'; importBtn.style.display = 'none'; progressDiv.style.display = 'block'; updateStatus('正在获取日历...'); updateProgress('-', null, 0, 0); try { // 检查当前页面日历是否有日记日期 const calendarLinks = document.querySelectorAll('.calendar td a[href*="/archive/day/"]'); if (calendarLinks.length === 0) { updateStatus('⚠️ 当前日历无日记,请先跳转到有日记的月份'); isExporting = false; startBtn.style.display = 'block'; stopBtn.style.display = 'none'; return; } // 从当前页面开始,获取日历 await processCalendarPage(window.location.href); } catch (e) { console.error(e); updateStatus('出错: ' + e.message); } if (isExporting) { stopExport(); } } function stopExport() { isExporting = false; startBtn.style.display = 'block'; startBtn.textContent = '重新导出'; stopBtn.style.display = 'none'; introDiv.style.display = 'block'; importBtn.style.display = 'block'; updateStatus(`✅ 完成!共 ${allDiaries.length} 篇`); downloadData(); } /** * 处理一个包含日历的页面 * 1. 提取日历中所有有日记的日期链接 (/archive/day/YYYY-MM-DD) * 2. 逐个获取每天的日记 * 3. 找到上一个月的链接,继续处理 */ async function processCalendarPage(url) { if (!isExporting) return; try { const response = await fetch(url, { credentials: 'include' }); const text = await response.text(); const parser = new DOMParser(); const doc = parser.parseFromString(text, 'text/html'); // 提取当前日历的年月标识(用于防止重复处理) const monthId = extractMonthId(doc); if (monthId && processedMonths.has(monthId)) { console.log(`[吾志导出] 月份 ${monthId} 已处理过,跳过`); return; } if (monthId) { processedMonths.add(monthId); } updateStatus(`正在处理: ${monthId || '当前月'}`); updateProgress(monthId || '当前月', null, null, processedMonths.size); // 找到日历中所有有日记的日期 // 格式: 13 const dayLinks = doc.querySelectorAll('.calendar td a[href*="/archive/day/"]'); console.log(`[吾志导出] 找到 ${dayLinks.length} 天有日记`); // 逐个处理每天 for (const link of dayLinks) { if (!isExporting) return; const dayUrl = link.href; // 从 URL 提取日期: /archive/day/2015-01-13 const dateMatch = dayUrl.match(/\/archive\/day\/(\d{4}-\d{2}-\d{2})/); if (!dateMatch) continue; const dateStr = dateMatch[1]; // 2015-01-13 updateStatus(`获取: ${dateStr}`); await fetchDayDiaries(dayUrl, dateStr); updateProgress(null, 1, null, null); await sleep(800); // 每天间隔 0.8 秒 } // 找到"上个月"链接 // 格式: < 01月... const prevMonthLink = doc.querySelector('.calendar_month a[href*="/archive/month/"]'); if (prevMonthLink && prevMonthLink.href) { updateStatus(`等待后跳转上个月...`); await sleep(1000); if (!isExporting) return; // 获取上个月的页面(它也会包含日历) await processCalendarPage(prevMonthLink.href); } else { console.log('[吾志导出] 没有更早的月份了'); } } catch (err) { console.error('处理日历页面失败:', err); updateStatus('请求失败: ' + err.message); } } /** * 从页面提取月份标识,用于防重复 */ function extractMonthId(doc) { // 尝试从 URL 或日历标题提取 const monthLink = doc.querySelector('.calendar_month a[href*="/archive/month/"]'); if (monthLink && monthLink.href) { const match = monthLink.href.match(/\/archive\/month\/(\d{4}-\d{2})/); if (match) { // 这个链接是"上个月",所以当前月份是它的下个月 const [year, month] = match[1].split('-').map(Number); const nextMonth = month === 12 ? 1 : month + 1; const nextYear = month === 12 ? year + 1 : year; return `${nextYear}-${String(nextMonth).padStart(2, '0')}`; } } return null; } /** * 获取某一天的所有日记 * @param {string} url - /archive/day/YYYY-MM-DD 页面 * @param {string} dateStr - YYYY-MM-DD 格式的日期 */ async function fetchDayDiaries(url, dateStr) { try { const response = await fetch(url, { credentials: 'include' }); const text = await response.text(); const parser = new DOMParser(); const doc = parser.parseFromString(text, 'text/html'); // 查找 days_middle 容器,其中包含多个 note_time + note_content 对 const daysMiddle = doc.querySelector('.days_middle, .days'); if (daysMiddle) { // 获取所有 note_time 和 note_content 元素 const timeEls = daysMiddle.querySelectorAll('.note_time'); const contentEls = daysMiddle.querySelectorAll('.note_content'); // 它们应该是成对出现的 const count = Math.min(timeEls.length, contentEls.length); for (let i = 0; i < count; i++) { const time = timeEls[i].innerText.trim(); const content = contentEls[i].innerText.trim(); if (content) { allDiaries.push({ date: dateStr, time: time, content: content }); } } // 如果 content 比 time 多,补充剩余的 for (let i = count; i < contentEls.length; i++) { const content = contentEls[i].innerText.trim(); if (content) { allDiaries.push({ date: dateStr, time: '', content: content }); } } return; } // 回退:尝试其他选择器 const entries = doc.querySelectorAll('.index_list, .note_list'); entries.forEach(entry => { const timeEl = entry.querySelector('.note_time'); const contentEl = entry.querySelector('.note_content'); const time = timeEl ? timeEl.innerText.trim() : ''; const content = contentEl ? contentEl.innerText.trim() : ''; if (content) { allDiaries.push({ date: dateStr, time: time, content: content }); } }); } catch (err) { console.error(`获取 ${dateStr} 失败:`, err); } } /** * 下载数据为 JSON 和 TXT 文件 */ function downloadData() { if (allDiaries.length === 0) { alert('没有收集到任何日记。请确保您已登录。'); return; } // 按日期排序(最新在前) allDiaries.sort((a, b) => { const da = a.date + ' ' + a.time; const db = b.date + ' ' + b.time; return db.localeCompare(da); }); const dateStamp = new Date().toISOString().slice(0, 10); // 下载 JSON const jsonStr = JSON.stringify(allDiaries, null, 2); downloadFile(jsonStr, `wuzhi_diaries_${dateStamp}.json`, 'application/json;charset=utf-8'); // 生成 TXT 内容 let txtContent = `吾志日记导出 - ${dateStamp}\n`; txtContent += `共 ${allDiaries.length} 篇日记\n`; txtContent += '='.repeat(50) + '\n\n'; allDiaries.forEach((diary, idx) => { txtContent += `【${diary.date}${diary.time ? ' ' + diary.time : ''}】\n`; txtContent += diary.content + '\n'; txtContent += '-'.repeat(30) + '\n\n'; }); // 下载 TXT downloadFile(txtContent, `wuzhi_diaries_${dateStamp}.txt`, 'text/plain;charset=utf-8'); updateStatus(`✅ 已保存 ${allDiaries.length} 篇日记 (JSON + TXT)`); } function downloadFile(content, filename, mimeType) { const blob = new Blob([content], { type: mimeType }); const blobUrl = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = blobUrl; a.download = filename; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(blobUrl); } })();