Greasy Fork

Greasy Fork is available in English.

福利吧论坛回复导出工具

导出当前页面的所有论坛回复内容

目前为 2025-04-27 提交的版本,查看 最新版本

// ==UserScript==
// @name         福利吧论坛回复导出工具
// @namespace    bbs_crewler.ai
// @version      0.2
// @description  导出当前页面的所有论坛回复内容
// @author       Chris_C
// @match        *://www.wnflb2023.com/*
// @grant        GM_setClipboard
// @grant        GM_download
// @require      https://code.jquery.com/jquery-3.6.0.min.js
// @license MIT
// ==/UserScript==

(function() {
    'use strict';

    // localStorage包装函数,简化存取操作
    const Storage = {
        set: function(key, value) {
            localStorage.setItem(key, JSON.stringify(value));
        },
        get: function(key, defaultValue) {
            const value = localStorage.getItem(key);
            return value ? JSON.parse(value) : defaultValue;
        },
        remove: function(key) {
            localStorage.removeItem(key);
        }
    };

    // 创建导出按钮
    const btn = document.createElement('button');
    btn.style.position = 'fixed';
    btn.style.right = '20px';
    btn.style.bottom = '20px';
    btn.style.zIndex = 9999;
    btn.style.padding = '8px 12px';
    btn.style.backgroundColor = '#3498db';
    btn.style.color = 'white';
    btn.style.border = 'none';
    btn.style.borderRadius = '4px';
    btn.style.cursor = 'pointer';
    btn.textContent = '导出回复';
    btn.onclick = startExport;
    document.body.appendChild(btn);

    // 创建进度指示器
    const progressIndicator = document.createElement('div');
    progressIndicator.style.position = 'fixed';
    progressIndicator.style.left = '50%';
    progressIndicator.style.top = '50%';
    progressIndicator.style.transform = 'translate(-50%, -50%)';
    progressIndicator.style.backgroundColor = 'rgba(0, 0, 0, 0.8)';
    progressIndicator.style.color = 'white';
    progressIndicator.style.padding = '20px';
    progressIndicator.style.borderRadius = '5px';
    progressIndicator.style.zIndex = 10000;
    progressIndicator.style.display = 'none';
    document.body.appendChild(progressIndicator);

    // 检查是否正在爬取过程中
    const isExporting = Storage.get('isExporting', false);
    const allContent = Storage.get('allContent', []);
    const currentPage = Storage.get('currentPage', 1);
    const totalPages = Storage.get('totalPages', 0);
    const startPageUrl = Storage.get('startPageUrl', '');

    // 如果正在爬取过程中,自动继续
    if (isExporting) {
        setTimeout(() => {
            continueExport();
        }, 1000);
    }

    // 开始导出过程
    function startExport() {
        // 重置存储的数据
        Storage.set('allContent', []);
        Storage.set('currentPage', 1);
        Storage.set('totalPages', getTotalPages());
        Storage.set('startPageUrl', window.location.href);
        Storage.set('isExporting', true);
        
        // 开始爬取当前页面
        continueExport();
    }

    // 生成1-3秒的随机延迟
    function getRandomDelay() {
        return Math.floor(Math.random() * 2000) + 1000; // 1000-3000ms之间的随机数
    }

    // 继续导出过程
    async function continueExport() {
        try {
            // 获取存储的数据
            let allContent = Storage.get('allContent', []);
            let currentPage = Storage.get('currentPage', 1);
            let totalPages = Storage.get('totalPages', 0);
            let startPageUrl = Storage.get('startPageUrl', '');
            
            // 禁用按钮
            btn.disabled = true;
            btn.style.backgroundColor = '#95a5a6';
            
            // 显示进度
            showProgress(`正在收集回复 (页面 ${currentPage}/${totalPages})...`);
            
            // 收集当前页面的回复
            const replies = extractReplies();
            allContent = allContent.concat(replies);
            
            // 更新进度和存储
            showProgress(`已收集 ${allContent.length} 条回复 (页面 ${currentPage}/${totalPages})...`);
            Storage.set('allContent', allContent);
            
            // 检查是否有下一页和是否已完成所有页面
            if (currentPage >= totalPages) {
                // 所有页面处理完毕,导出结果
                finishExport(allContent, startPageUrl);
                return;
            }
            
            // 获取下一页链接
            const nextPageLink = document.querySelector('.nxt');
            if (!nextPageLink) {
                // 没有找到下一页链接,提前结束
                finishExport(allContent, startPageUrl);
                return;
            }
            
            // 准备跳转到下一页
            currentPage++;
            Storage.set('currentPage', currentPage);
            
            // 随机等待1-3秒后再跳转到下一页
            const delay = getRandomDelay();
            showProgress(`已收集 ${allContent.length} 条回复 (页面 ${currentPage-1}/${totalPages})... ${Math.round(delay/1000)}秒后跳转至下一页`);
            
            setTimeout(() => {
                // 导航到下一页
                window.location.href = nextPageLink.href;
            }, delay);
            
        } catch (error) {
            // 错误处理
            hideProgress();
            alert(`导出失败: ${error.message}`);
            resetExportState();
        }
    }

    // 完成导出过程
    function finishExport(allContent, startPageUrl) {
        try {
            // 导出结果
            const output = formatContent(allContent);
            
            // 尝试复制到剪贴板
            try {
                GM_setClipboard(output, 'text/plain');
            } catch (e) {
                console.error('复制到剪贴板失败', e);
            }
            
            // 创建下载
            try {
                // 使用原生下载方法作为备选
                const blob = new Blob([output], {type: 'text/plain'});
                const url = URL.createObjectURL(blob);
                const a = document.createElement('a');
                a.href = url;
                a.download = `论坛回复_${Date.now()}.txt`;
                document.body.appendChild(a);
                a.click();
                document.body.removeChild(a);
                
                // 尝试使用GM_download作为首选
                try {
                    GM_download({
                        url: url,
                        name: `论坛回复_${Date.now()}.txt`,
                        saveAs: true
                    });
                } catch (e) {
                    console.log('GM_download不可用,已使用原生下载方法', e);
                }
            } catch (e) {
                console.error('下载失败', e);
                alert('下载失败,请尝试手动复制内容并保存');
            }
            
            // 重置状态
            resetExportState();
            
            // 隐藏进度
            hideProgress();
            
            // 如果不在起始页面,返回起始页面
            if (window.location.href !== startPageUrl) {
                alert(`成功导出 ${allContent.length} 条回复!即将返回起始页面。`);
                window.location.href = startPageUrl;
            } else {
                alert(`成功导出 ${allContent.length} 条回复!`);
            }
        } catch (error) {
            hideProgress();
            alert(`导出失败: ${error.message}`);
            resetExportState();
        } finally {
            // 恢复按钮状态
            btn.disabled = false;
            btn.style.backgroundColor = '#3498db';
        }
    }

    // 重置导出状态
    function resetExportState() {
        Storage.remove('isExporting');
        Storage.remove('allContent');
        Storage.remove('currentPage');
        Storage.remove('totalPages');
        Storage.remove('startPageUrl');
    }

    function getTotalPages() {
        // 查找页码信息
        const pageInfo = document.querySelector('.pg');
        if(!pageInfo) return 1;
        
        // 查找最后一页链接
        const lastPageLink = pageInfo.querySelector('.last');
        if(lastPageLink) {
            // 从文本中提取数字,例如 "... 32" 提取 32
            const match = lastPageLink.textContent.match(/\d+/);
            return match ? parseInt(match[0]) : 1;
        }
        
        // 如果没有明确的最后页链接,查找所有页码链接
        const pageLinks = pageInfo.querySelectorAll('a');
        let maxPage = 1;
        pageLinks.forEach(link => {
            const pageNum = parseInt(link.textContent);
            if(!isNaN(pageNum) && pageNum > maxPage) {
                maxPage = pageNum;
            }
        });
        
        return maxPage;
    }

    function extractReplies() {
        // 修改选择器以适配实际页面结构
        return Array.from(document.querySelectorAll('#postlist .plhin')).map(item => {
            // 排除广告和其他非回复元素
            if (!item.querySelector('.authi a') || !item.querySelector('.t_f')) {
                return null;
            }
            
            return {
                author: item.querySelector('.authi a').innerText,
                time: item.querySelector('[id^="authorposton"]')?.innerText.replace('发表于 ', '') || '未知时间',
                content: item.querySelector('.t_f')?.innerText.replace(/\s+/g, ' ') || '内容为空',
                floor: item.querySelector('[id^="postnum"]')?.innerText.trim() || '未知楼层'
            };
        }).filter(item => item !== null); // 过滤掉空值
    }

    function formatContent(replies) {
        return replies.map((r, index) => 
            `【${r.floor}】 ${r.author} 发表于 ${r.time}\n${r.content}\n${'-'.repeat(60)}`
        ).join('\n\n');
    }
    
    function showProgress(message) {
        progressIndicator.textContent = message;
        progressIndicator.style.display = 'block';
    }
    
    function hideProgress() {
        progressIndicator.style.display = 'none';
    }
})();