您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Greasy Fork is available in English.
将百度贴吧某帖子中楼主的所有发言保存为 HTML 文件,方便离线浏览
// ==UserScript== // @name 导出百度贴吧楼主帖子 // @namespace http://tampermonkey.net/ // @version 1.1.1 // @description 将百度贴吧某帖子中楼主的所有发言保存为 HTML 文件,方便离线浏览 // @author wiiiind // @match https://tieba.baidu.com/p/* // @grant GM_download // @license MIT // ==/UserScript== (function() { 'use strict'; // 添加按钮到页面 function addButton() { const button = document.createElement('a'); button.innerText = '保存楼主发言'; button.href = 'javascript:;'; button.className = 'btn-sub btn-small'; button.onclick = saveTiebaPosts; // 找到按钮组区域 const btnGroup = document.querySelector('.core_title_btns'); if (btnGroup) { // 插入到按钮组的第一个位置 btnGroup.insertBefore(button, btnGroup.firstChild); } } let currentPage = 1; let totalPages = 1; let posts = []; function fetchPosts(page) { const url = window.location.href.replace(/&pn=\d+/, '') + '&pn=' + page; return fetch(url) .then(response => response.text()) .then(html => { const parser = new DOMParser(); const doc = parser.parseFromString(html, 'text/html'); const postElements = doc.querySelectorAll('.l_post'); postElements.forEach(post => { try { // 获取IP属地 const ipSpan = post.querySelector('.post-tail-wrap span:not([class])'); const ip = ipSpan ? ipSpan.innerText.trim().replace(/^IP属地:/, '') : '未知IP'; // 获取其他信息 const tailInfoSpans = post.querySelectorAll('.post-tail-wrap .tail-info'); let deviceInfo = '未知设备'; let floor = '未知楼层'; let time = '未知时间'; // 遍历所有tail-info span,找到包含设备信息、楼层和时间的span tailInfoSpans.forEach(span => { const text = span.innerText.trim(); if (span.querySelector('a') && text.includes('来自')) { deviceInfo = span.querySelector('a').innerText.trim(); } else if (text.includes('楼')) { floor = text; } else if (text.match(/\d{4}-\d{2}-\d{2}/)) { time = text; } }); // 获取内容 const contentElement = post.querySelector('.d_post_content'); const content = contentElement ? contentElement.innerHTML.trim() : ''; // 修改图片获取逻辑,只获取BDE_Image类的图片 const images = Array.from(post.querySelectorAll('.d_post_content img.BDE_Image')).map(img => img.src || ''); posts.push({ ip, deviceInfo, floor, time, content, images }); } catch (error) { console.error('处理帖子时出错:', error); } }); return Promise.resolve(); }) .catch(error => { console.error(`获取第${page}页数据时出错:`, error); }); } function savePosts() { const title = document.querySelector('.core_title_txt').innerText.trim(); const date = new Date().toLocaleString('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false }); const fileName = `${title}_${date.split(' ')[0]}.html`; // 获取楼主信息 const authorElement = document.querySelector('.d_name .p_author_name'); const authorName = authorElement ? authorElement.innerText : '未知用户'; const authorLink = authorElement ? authorElement.href : '#'; const authorAvatar = document.querySelector('.p_author_face img'); const avatarSrc = authorAvatar ? authorAvatar.src : ''; const originalLink = window.location.href; // 在生成HTML前对posts进行排序 posts.sort((a, b) => { // 从楼层文本中提取数字 const getFloorNumber = (floor) => { const match = floor.match(/(\d+)/); return match ? parseInt(match[1], 10) : 0; }; return getFloorNumber(a.floor) - getFloorNumber(b.floor); }); let htmlContent = ` <html> <head> <meta charset="UTF-8"> <title>${title}</title> <script> // 免责声明弹窗 window.onload = function() { const disclaimer = \`1. 本帖子保存时间:${date} 2. 本脚本旨在为用户提供便利,用于个人备份公开访问的内容。请确保您在使用本脚本时遵守相关平台的用户协议及法律法规。 3. 本脚本仅限个人学习、研究或备份用途,禁止用于任何非法行为,包括但不限于:未经授权抓取、复制、传播受版权保护的内容或侵犯他人合法权益。 4. 使用本脚本可能涉及到技术风险,例如账号被限制或封禁等情况。请在使用前充分了解风险,并自行承担因使用脚本所引发的后果。 5. 本脚本的作者不对因脚本使用导致的任何直接或间接后果承担责任,包括但不限于数据丢失、账号封禁或其他法律责任。 6. 作者保留修改、更新或终止维护脚本的权利。\`; window.alert = function(msg) { const iframe = document.createElement('iframe'); iframe.style.display = 'none'; document.body.appendChild(iframe); const alertFrame = iframe.contentWindow; const result = alertFrame.alert(msg); iframe.parentNode.removeChild(iframe); return result; }; alert(disclaimer); } // 跳转楼层的函数 function jumpToFloor() { const targetFloor = parseInt(prompt('请输入要跳转的楼层号:')); if (!targetFloor) return; // 获取所有楼层 const floors = Array.from(document.querySelectorAll('table[data-floor]')) .map(table => ({ element: table, floor: parseInt(table.getAttribute('data-floor')) })) .sort((a, b) => a.floor - b.floor); // 找到目标楼层或最近的前一个楼层 let targetElement = null; for (let i = floors.length - 1; i >= 0; i--) { if (floors[i].floor <= targetFloor) { targetElement = floors[i].element; break; } } if (targetElement) { targetElement.scrollIntoView({ behavior: 'smooth' }); // 如果不是精确匹配,显示提示 if (parseInt(targetElement.getAttribute('data-floor')) < targetFloor) { alert('未找到该楼层,已定位到最近的前一个楼层:' + targetElement.getAttribute('data-floor') + '楼'); } } else { // 如果连第一层都大于目标楼层,就跳转到第一层 if (floors.length > 0) { floors[0].element.scrollIntoView({ behavior: 'smooth' }); alert('未找到该楼层,已定位到第一个楼层:' + floors[0].floor + '楼'); } } } </script> <style> body { margin: 20px; font-family: Arial, sans-serif; max-width: 794px; /* A4 width */ margin-left: auto; margin-right: auto; } .header { position: sticky; top: 0; background: white; width: 100%; z-index: 100; padding: 10px 0; display: flex; flex-direction: column; align-items: center; border-bottom: 1px solid #eee; } .title { font-size: 24px; font-weight: bold; margin: 10px 0; text-align: center; } .content-wrapper { margin-top: 20px; width: 100%; display: flex; flex-direction: column; align-items: center; } .author-info { display: flex; align-items: center; margin-bottom: 20px; gap: 10px; } .author-avatar { width: 48px; height: 48px; border-radius: 50%; } .links { margin-bottom: 20px; text-align: center; } .links a { color: #4CAF50; text-decoration: none; margin: 0 10px; } .links a:hover { text-decoration: underline; } table { border-collapse: collapse; width: 100%; /* Use full width of body */ margin-bottom: 20px; position: relative; } tr { display: flex; } td { padding: 10px; vertical-align: top; } .info-cell { width: 22%; border-right: 1px solid #ddd; } .content-cell { width: 78%; flex: 1; } .floor-number { font-size: 18px; font-weight: bold; margin-bottom: 10px; } .info-item { margin: 5px 0; color: #666; } img { max-width: 100%; margin: 5px 0; } .jump-btn { position: fixed; bottom: 20px; right: 20px; background: #4CAF50; color: white; border: none; padding: 10px 20px; border-radius: 5px; cursor: pointer; z-index: 1000; } .jump-btn:hover { background: #45a049; } /* 让表格有一个data-floor属性用于跳转 */ table { scroll-margin-top: 100px; /* 跳转时留出顶部空间 */ } /* 添加免责声明的样式 */ .disclaimer { white-space: pre-wrap; font-family: monospace; } .footer { text-align: center; padding: 20px; color: #666; font-size: 14px; margin-top: 40px; border-top: 1px solid #eee; } .footer a { color: #4CAF50; text-decoration: none; } .footer a:hover { text-decoration: underline; } </style> </head> <body> <button class="jump-btn" onclick="jumpToFloor()">跳转到指定楼层</button> <div class="header"> <div class="title">${title}</div> </div> <div class="content-wrapper"> <div class="author-info"> <img class="author-avatar" src="${avatarSrc}" alt="${authorName}"> <a href="${authorLink}" target="_blank">${authorName}</a> </div> <div class="links"> <a href="${originalLink}" target="_blank">查看原帖</a> <a href="${authorLink}" target="_blank">作者主页</a> </div> <!-- 帖子内容将在这里显示 --> `; posts.forEach(post => { // 从content中移除所有BDE_Image图片 const tempDiv = document.createElement('div'); tempDiv.innerHTML = post.content; tempDiv.querySelectorAll('img.BDE_Image').forEach(img => img.remove()); const contentWithoutImages = tempDiv.innerHTML; htmlContent += ` <table data-floor="${post.floor.match(/(\d+)/)?.[1] || '0'}"> <tr> <td class="info-cell"> <div class="floor-number">${post.floor}</div> <div class="info-item">IP属地: ${post.ip}</div> <div class="info-item">设备: ${post.deviceInfo}</div> <div class="info-item">时间: ${post.time}</div> </td> <td class="content-cell"> <div>${contentWithoutImages}</div> <div>${post.images.map(src => `<img src="${src}" class="BDE_Image">`).join('')}</div> </td> </tr> </table> `; }); // 获取当前登录用户信息 const userElement = document.querySelector('.u_menu_username a'); const userName = userElement ? userElement.querySelector('.u_username_title').textContent : '未登录用户'; const userLink = userElement ? userElement.href : '#'; htmlContent += ` <div class="footer"> 帖子由<a href="${userLink}" target="_blank">@${userName}</a>通过<a href="http://greasyfork.icu/zh-CN/scripts/518200-tieba-op-posts-saver" target="_blank">此脚本</a>自动抓取生成。 欲查看于移动设备,请<a href="javascript:void(0)" onclick="printToPDF()">另存为PDF</a>。 </div> <script> function printToPDF() { if (confirm('请选择"另存为PDF"打印机进行打印')) { window.print(); } } </script> </body> </html> `; // 添加打印样式 const printStyles = ` @media print { .jump-btn { display: none; /* 隐藏跳转按钮 */ } /* 确保内容完整打印 */ .content-cell img { break-inside: avoid; } /* 优化打印布局 */ table { break-inside: avoid; page-break-inside: avoid; } } `; // 将打印样式插入到现有样式中 htmlContent = htmlContent.replace('</style>', printStyles + '</style>'); const blob = new Blob([htmlContent], { type: 'text/html' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = fileName; a.click(); URL.revokeObjectURL(url); } function saveTiebaPosts() { console.log('开始保存楼主发言...'); // 检查是否在只看楼主模式 if (!window.location.href.includes('see_lz=1')) { alert('此功能需要在"只看楼主"模式下使用。\n\n请先点击帖子上方的"只看楼主"按钮,然后再次点击"保存楼主发言"。'); // 找到"只看楼主"按钮并高亮显示 const lzOnlyBtn = document.querySelector('#lzonly_cntn'); if (lzOnlyBtn) { // 保存原始样式 const originalBackground = lzOnlyBtn.style.background; const originalTransition = lzOnlyBtn.style.transition; // 添加闪烁效果 lzOnlyBtn.style.transition = 'background 0.5s'; lzOnlyBtn.style.background = '#ffd700'; // 1秒后恢复原样 setTimeout(() => { lzOnlyBtn.style.background = originalBackground; lzOnlyBtn.style.transition = originalTransition; }, 1000); } return; } // 清空之前的帖子数据 posts = []; currentPage = 1; // 获取总页数 const lastPageLink = document.querySelector('.l_pager a[href*="pn="]:last-child'); if (lastPageLink) { const match = lastPageLink.href.match(/pn=(\d+)/); if (match) { totalPages = parseInt(match[1], 10); } } console.log(`总页数: ${totalPages}`); // 创建一个加载提示 const loadingDiv = document.createElement('div'); loadingDiv.style.position = 'fixed'; loadingDiv.style.top = '50%'; loadingDiv.style.left = '50%'; loadingDiv.style.transform = 'translate(-50%, -50%)'; loadingDiv.style.padding = '20px'; loadingDiv.style.background = 'rgba(0,0,0,0.8)'; loadingDiv.style.color = 'white'; loadingDiv.style.borderRadius = '5px'; loadingDiv.style.zIndex = '10000'; document.body.appendChild(loadingDiv); // 使Promise.all和分批处理来获取所有页面 const batchSize = 5; // 每批处理5个页面 const batches = []; for (let i = 1; i <= totalPages; i += batchSize) { const batch = []; for (let j = i; j < Math.min(i + batchSize, totalPages + 1); j++) { batch.push(fetchPosts(j)); } batches.push(batch); } // 按批次处理所有页面 let processedPages = 0; const processBatch = async (batchIndex) => { if (batchIndex >= batches.length) { // 所有批次处理完成,保存文件 loadingDiv.remove(); savePosts(); return; } await Promise.all(batches[batchIndex]); processedPages += batches[batchIndex].length; loadingDiv.textContent = `正在获取帖子内容... ${Math.min(processedPages, totalPages)}/${totalPages}`; // 延迟处理下一批次,避免请求过快 setTimeout(() => processBatch(batchIndex + 1), 1000); }; loadingDiv.textContent = '正在获取帖子内容... 0/' + totalPages; processBatch(0); } // 初始化 addButton(); })();