// ==UserScript== // @name 0426网页纯净文本导出TXT【修复增强版】 // @namespace https://github.com/ // @version 2.0 // @description 一键抓取网页纯净文本、过滤广告隐藏元素、去重、导出TXT、解决乱码、按钮可拖拽、全局通用 // @author Repair // @match *://*/* // @grant GM_setValue // @grant GM_getValue // @grant GM_deleteValue // @grant GM_addStyle // @grant window.close // @license MIT // @downloadURL https://update.greasyfork.icu/scripts/575511/0426%E7%BD%91%E9%A1%B5%E7%BA%AF%E5%87%80%E6%96%87%E6%9C%AC%E5%AF%BC%E5%87%BATXT%E3%80%90%E4%BF%AE%E5%A4%8D%E5%A2%9E%E5%BC%BA%E7%89%88%E3%80%91.user.js // @updateURL https://update.greasyfork.icu/scripts/575511/0426%E7%BD%91%E9%A1%B5%E7%BA%AF%E5%87%80%E6%96%87%E6%9C%AC%E5%AF%BC%E5%87%BATXT%E3%80%90%E4%BF%AE%E5%A4%8D%E5%A2%9E%E5%BC%BA%E7%89%88%E3%80%91.meta.js // ==/UserScript== (function () { 'use strict'; /* ======================================== 1. 全局配置 + 本地持久化存储 ======================================== */ const DEFAULT_CONFIG = { btnPosX: 0, btnPosY: 0, filterHidden: true, filterScriptStyle: true, filterAdText: true, addUtf8Bom: true, mergeBlankLine: true, onlySelectedText: false, showFileHead: true, btnSize: 48, primaryColor: '#1677ff' }; let GLOBAL_CFG = Object.assign({}, DEFAULT_CONFIG); // 读取本地配置 function loadConfig() { const save = GM_getValue('txtExportConfig', null); if (save && typeof save === 'object') { GLOBAL_CFG = Object.assign(DEFAULT_CONFIG, save); } } // 保存配置 function saveConfig() { GM_setValue('txtExportConfig', GLOBAL_CFG); } /* ======================================== 2. 公共工具方法 ======================================== */ // 补零 function padZero(num, len = 2) { return String(num).padStart(len, '0'); } // 时间格式化 function getTimeStr() { const d = new Date(); const y = d.getFullYear(); const m = padZero(d.getMonth() + 1); const day = padZero(d.getDate()); const h = padZero(d.getHours()); const mi = padZero(d.getMinutes()); const s = padZero(d.getSeconds()); return `${y}-${m}-${day}_${h}-${mi}-${s}`; } // 清理文件名非法字符 function cleanFileName(name) { return name.replace(/[\\/:*?"<>|#\n\r\t]/g, '').trim() || '无标题文档'; } // 防抖函数 function debounce(fn, delay = 300) { let timer = null; return function (...args) { if (timer) clearTimeout(timer); timer = setTimeout(() => fn.apply(this, args), delay); }; } /* ======================================== 3. Toast 轻提示 单例互斥 ======================================== */ let toastTimer = null; function showToast(msg) { let toast = document.getElementById('txt-export-toast'); if (!toast) { toast = document.createElement('div'); toast.id = 'txt-export-toast'; document.body.appendChild(toast); } // 清除上一次计时器 if (toastTimer) clearTimeout(toastTimer); toast.innerText = msg; Object.assign(toast.style, { position: 'fixed', left: '50%', bottom: '100px', transform: 'translateX(-50%)', background: 'rgba(0,0,0,0.75)', color: '#fff', padding: '8px 16px', borderRadius: '6px', fontSize: '13px', zIndex: '9999999', whiteSpace: 'nowrap', transition: 'opacity 0.25s ease', opacity: '1', pointerEvents: 'none' }); toastTimer = setTimeout(() => { toast.style.opacity = '0'; setTimeout(() => toast.remove(), 300); }, 2200); } /* ======================================== 4. 高阶文本清洗器 ======================================== */ function cleanTextContent(raw) { let text = raw; // 去除制表符、零宽字符、控制符 text = text.replace(/[\t\x00-\x1F\x7F\u200B-\u200D\uFEFF]/g, ''); // 全角空格、不间断空格 text = text.replace(/[\u00A0\u3000]/g, ' '); // 首尾修剪 text = text.trim(); if (GLOBAL_CFG.mergeBlankLine) { // 合并多行空行 text = text.replace(/\n\s*\n+/g, '\n\n'); } // 去除首尾空行 text = text.replace(/^\n+|\n+$/g, ''); return text; } /* ======================================== 5. 核心:纯净文本抓取(去重+过滤隐藏/广告/代码) ======================================== */ function isHideElement(el) { const style = getComputedStyle(el); return style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0' || el.offsetHeight === 0 && el.offsetWidth === 0; } function filterUnUseTag(el) { const denyTags = ['script', 'style', 'noscript', 'iframe', 'svg', 'head']; return denyTags.includes(el.tagName.toLowerCase()); } // 递归抓取可见文本 function getAllVisibleText(root = document.body) { let result = ''; const nodes = root.childNodes; for (let i = 0; i < nodes.length; i++) { const node = nodes[i]; // 文本节点 if (node.nodeType === Node.TEXT_NODE) { result += node.textContent; continue; } // 元素节点 if (node.nodeType === Node.ELEMENT_NODE) { if (GLOBAL_CFG.filterScriptStyle && filterUnUseTag(node)) continue; if (GLOBAL_CFG.filterHidden && isHideElement(node)) continue; // 递归深层遍历 result += getAllVisibleText(node); } } return result; } // 入口:获取最终需要导出的文本 function getFinalText() { // 优先取鼠标选中内容 if (GLOBAL_CFG.onlySelectedText) { const selectText = window.getSelection().toString(); if (selectText) return cleanTextContent(selectText); } // 抓取全局可见文本 const raw = getAllVisibleText(); return cleanTextContent(raw); } /* ======================================== 6. TXT 文件导出(带UTF-8 BOM 解决记事本乱码) ======================================== */ function exportToTXT(content) { return new Promise((resolve, reject) => { try { const title = cleanFileName(document.title); const url = location.href; const time = getTimeStr(); let fileBody = ''; // 头部信息 if (GLOBAL_CFG.showFileHead) { fileBody += `=============================================\r\n`; fileBody += `网页文本导出记录\r\n`; fileBody += `导出时间:${new Date().toLocaleString()}\r\n`; fileBody += `页面标题:${title}\r\n`; fileBody += `页面链接:${url}\r\n`; fileBody += `=============================================\r\n\r\n`; } fileBody += content; // 增加 UTF-8 BOM let fileData = fileBody; if (GLOBAL_CFG.addUtf8Bom) { fileData = '\ufeff' + fileData; } const blob = new Blob([fileData], { type: 'text/plain; charset=utf-8' }); const fileName = `${title}_${time}.txt`; const a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = fileName; document.body.appendChild(a); a.click(); // 释放资源 a.remove(); URL.revokeObjectURL(a.href); resolve('ok'); } catch (err) { console.error('导出失败:', err); reject(err); } }); } /* ======================================== 7. 可拖拽悬浮按钮 + CSP 安全样式注入 ======================================== */ function injectStyle() { GM_addStyle(` #txt-export-drag-btn { position: fixed; width: ${GLOBAL_CFG.btnSize}px; height: ${GLOBAL_CFG.btnSize}px; line-height: ${GLOBAL_CFG.btnSize}px; text-align: center; background: ${GLOBAL_CFG.primaryColor}; color: #ffffff; border-radius: 50%; z-index: 9999999; cursor: move; user-select: none; box-shadow: 0 2px 12px rgba(0,0,0,0.15); font-size: 12px; transition: transform 0.2s; } #txt-export-drag-btn:active { transform: scale(0.95); } `); } let dragStatus = { isMove: false, startX: 0, startY: 0, originX: 0, originY: 0 }; function createDragButton() { if (document.getElementById('txt-export-drag-btn')) return; const btn = document.createElement('div'); btn.id = 'txt-export-drag-btn'; btn.innerText = '导出TXT'; // 定位读取 if (GLOBAL_CFG.btnPosX || GLOBAL_CFG.btnPosY) { btn.style.left = GLOBAL_CFG.btnPosX + 'px'; btn.style.top = GLOBAL_CFG.btnPosY + 'px'; } else { btn.style.right = '25px'; btn.style.bottom = '25px'; } document.body.appendChild(btn); // 拖拽逻辑 btn.addEventListener('mousedown', (e) => { dragStatus.isMove = false; dragStatus.startX = e.clientX; dragStatus.startY = e.clientY; const rect = btn.getBoundingClientRect(); dragStatus.originX = rect.left; dragStatus.originY = rect.top; const moveFn = (ev) => { const dx = ev.clientX - dragStatus.startX; const dy = ev.clientY - dragStatus.startY; if (Math.abs(dx) > 5 || Math.abs(dy) > 5) { dragStatus.isMove = true; } btn.style.left = (dragStatus.originX + dx) + 'px'; btn.style.top = (dragStatus.originY + dy) + 'px'; btn.style.right = 'auto'; btn.style.bottom = 'auto'; }; const upFn = () => { document.removeEventListener('mousemove', moveFn); document.removeEventListener('mouseup', upFn); // 保存位置 if (dragStatus.isMove) { const nowRect = btn.getBoundingClientRect(); GLOBAL_CFG.btnPosX = nowRect.left; GLOBAL_CFG.btnPosY = nowRect.top; saveConfig(); } }; document.addEventListener('mousemove', moveFn); document.addEventListener('mouseup', upFn); }); // 点击导出 防抖 btn.addEventListener('click', debounce(async () => { if (dragStatus.isMove) return; btn.innerText = '处理中…'; btn.style.pointerEvents = 'none'; try { const text = getFinalText(); if (!text) { showToast('未抓取到有效文本'); return; } await exportToTXT(text); showToast('✅ TXT 导出成功'); } catch (err) { showToast('❌ 导出失败,请查看控制台'); } finally { btn.innerText = '导出TXT'; btn.style.pointerEvents = 'auto'; } }, 250)); } /* ======================================== 8. 初始化 & 防止内存泄漏 ======================================== */ let dynamicTimer = null; function init() { loadConfig(); injectStyle(); // DOM就绪创建按钮 if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', createDragButton); } else { createDragButton(); } // 兼容动态加载页面,低频率轮询,且可销毁 dynamicTimer = setTimeout(() => { createDragButton(); }, 2000); } // 脚本卸载清理 window.addEventListener('beforeunload', () => { if (dynamicTimer) clearTimeout(dynamicTimer); }); // 启动 init(); })();