5ch楼主内容提取器

提取5ch帖子中楼主的所有发言并在新窗口中纯净显示

// ==UserScript==
// @name         5ch楼主内容提取器 / 5ch OP Content Extractor
// @name:zh-CN   5ch楼主内容提取器
// @name:ja      5ch スレ主発言抽出器
// @name:en      5ch OP Content Extractor
// @namespace    http://tampermonkey.net/
// @version      0.1
// @description  提取5ch帖子中楼主的所有发言并在新窗口中纯净显示 | 5chスレッドでスレ主の全発言を抽出し、新しいウィンドウできれいに表示
// @description:zh-CN  提取5ch帖子中楼主的所有发言并在新窗口中纯净显示
// @description:ja     5chスレッドでスレ主の全ての発言を抽出し、新しいウィンドウできれいに表示します
// @description:en     Extract all posts from the original poster in 5ch threads and display them cleanly in a new window
// @author       Gao + Claude
// @match        https://*/test/read.cgi/*/*
// @match        http://*/test/read.cgi/*/*
// @grant        GM_addStyle
// @grant        window.open
// @license      MIT
// @supportURL   https://greasyfork.org/scripts/你的脚本ID
// @homepageURL  https://greasyfork.org/scripts/你的脚本ID
// ==/UserScript==

(function() {
    'use strict';

    // 添加按钮样式
    GM_addStyle(`
        .op-extractor-btn {
            position: fixed;
            top: 20px;
            right: 20px;
            width: 60px;
            height: 60px;
            background: rgba(0, 123, 255, 0.4);
            color: white;
            border: none;
            border-radius: 50%;
            cursor: move;
            z-index: 9999;
            font-size: 12px;
            display: flex;
            align-items: center;
            justify-content: center;
            text-align: center;
            user-select: none;
            transition: all 0.3s;
            box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
        }
        .op-extractor-btn:hover {
            background: rgba(0, 123, 255, 0.7);
            transform: scale(1.05);
        }
        .op-extractor-btn:active {
            cursor: grabbing;
        }
        .loading-indicator {
            position: fixed;
            top: 90px;
            right: 20px;
            background: rgba(0, 0, 0, 0.8);
            color: white;
            padding: 10px;
            border-radius: 5px;
            font-size: 12px;
            z-index: 9998;
            display: none;
        }
    `);

    // 检查是否在完整帖子页面
    function isFullThreadPage() {
        const url = window.location.href;
        const isLimited = url.includes('/l50') || url.includes('-') || url.match(/\/\d+-\d+/);
        console.log('页面类型检查:', { url, isLimited });
        return !isLimited;
    }

    // 自动切换到全部楼层
    function switchToAllPosts() {
        if (isFullThreadPage()) {
            console.log('已在完整页面');
            return false;
        }

        console.log('尝试跳转到完整页面...');

        // 查找"全部"链接
        const allLinks = document.querySelectorAll('a[href*="全部"], a.menuitem');
        for (let link of allLinks) {
            if (link.textContent.includes('全部')) {
                console.log('跳转到全部楼层:', link.href);
                window.location.href = link.href;
                return true;
            }
        }

        // 如果没找到,尝试构造完整URL
        const currentUrl = window.location.href;
        if (currentUrl.includes('/l50')) {
            const fullUrl = currentUrl.replace('/l50', '/');
            console.log('构造完整URL:', fullUrl);
            window.location.href = fullUrl;
            return true;
        }

        // 尝试移除URL中的分页参数
        const match = currentUrl.match(/^(.*?\/test\/read\.cgi\/[^\/]+\/\d+)/);
        if (match) {
            const baseUrl = match[1] + '/';
            if (baseUrl !== currentUrl) {
                console.log('跳转到基础URL:', baseUrl);
                window.location.href = baseUrl;
                return true;
            }
        }

        return false;
    }

    // 等待页面完全加载
    async function ensureFullPageLoad() {
        console.log('开始等待页面完全加载...');

        // 显示加载指示器
        const indicator = document.createElement('div');
        indicator.className = 'loading-indicator';
        indicator.textContent = '正在加载完整页面...';
        indicator.style.display = 'block';
        document.body.appendChild(indicator);

        // 统计当前帖子数量
        let currentPostCount = document.querySelectorAll('[data-userid]').length;
        console.log('初始帖子数量:', currentPostCount);

        // 滚动到页面底部,触发可能的懒加载
        let attempts = 0;
        const maxAttempts = 10;

        while (attempts < maxAttempts) {
            attempts++;
            indicator.textContent = `正在加载... (${attempts}/${maxAttempts})`;

            // 滚动到底部
            window.scrollTo(0, document.body.scrollHeight);

            // 等待一段时间让内容加载
            await new Promise(resolve => setTimeout(resolve, 1500));

            // 检查是否有新内容加载
            const newPostCount = document.querySelectorAll('[data-userid]').length;
            console.log(`尝试 ${attempts}: 帖子数量从 ${currentPostCount} 变为 ${newPostCount}`);

            if (newPostCount === currentPostCount) {
                // 没有新内容,再等一次确认
                await new Promise(resolve => setTimeout(resolve, 1000));
                const finalCount = document.querySelectorAll('[data-userid]').length;
                if (finalCount === newPostCount) {
                    console.log('页面加载完成,总帖子数:', finalCount);
                    break;
                }
            }

            currentPostCount = newPostCount;
        }

        // 滚动回顶部
        window.scrollTo(0, 0);

        // 隐藏加载指示器
        indicator.style.display = 'none';

        // 最后统计
        const totalPosts = document.querySelectorAll('[data-userid]').length;
        console.log('页面加载完成统计:', { totalPosts });

        return totalPosts;
    }

    // 获取楼主的用户名
    function getOPUsername() {
        // 从第一个帖子获取楼主用户名
        const firstPost = document.querySelector('[id="1"], [data-id="1"]');
        if (firstPost) {
            const username = firstPost.querySelector('.postusername')?.textContent?.trim();
            console.log('楼主用户名:', username);
            return username;
        }

        // 如果找不到第一个帖子,从任何帖子获取用户名
        const anyPost = document.querySelector('[data-userid]');
        if (anyPost) {
            const username = anyPost.querySelector('.postusername')?.textContent?.trim();
            console.log('默认用户名:', username);
            return username;
        }

        return null;
    }

    // 根据用户名提取所有发言
    function extractPostsByUsername(targetUsername) {
        console.log('根据用户名提取发言:', targetUsername);

        if (!targetUsername) {
            console.log('未指定目标用户名');
            return [];
        }

        const allPosts = document.querySelectorAll('[data-userid]');
        const matchingPosts = [];

        allPosts.forEach((post, index) => {
            try {
                const postUsername = post.querySelector('.postusername')?.textContent?.trim();
                const postContent = post.querySelector('.post-content');

                // 检查用户名是否匹配,并且有内容
                if (postUsername === targetUsername && postContent && postContent.innerHTML.trim()) {
                    const postData = {
                        id: post.getAttribute('data-id') || post.id || (index + 1),
                        userId: post.getAttribute('data-userid') || '',
                        postNumber: post.querySelector('.postid')?.textContent?.trim() || '',
                        username: postUsername,
                        date: post.querySelector('.date')?.textContent?.trim() || '',
                        uid: post.querySelector('.uid')?.textContent?.trim() || '',
                        content: postContent.innerHTML || ''
                    };

                    matchingPosts.push(postData);
                }
            } catch (e) {
                console.log('提取帖子时出错:', e);
            }
        });

        // 按帖子编号排序
        matchingPosts.sort((a, b) => {
            const numA = parseInt(a.postNumber) || 0;
            const numB = parseInt(b.postNumber) || 0;
            return numA - numB;
        });

        console.log(`找到 ${matchingPosts.length} 个匹配的帖子`);
        return matchingPosts;
    }

    // 提取楼主的所有发言
    async function extractOPPosts() {
        // 首先确保页面完全加载
        await ensureFullPageLoad();

        // 获取楼主用户名
        const opUsername = getOPUsername();
        if (!opUsername) {
            console.log('无法获取楼主用户名');
            return [];
        }

        // 统计信息
        const allPosts = document.querySelectorAll('[data-userid]');
        const usernameCounts = new Map();

        allPosts.forEach(post => {
            const username = post.querySelector('.postusername')?.textContent?.trim();
            if (username) {
                usernameCounts.set(username, (usernameCounts.get(username) || 0) + 1);
            }
        });

        console.log('用户名统计:', Array.from(usernameCounts.entries()).slice(0, 10));
        console.log(`目标用户名 "${opUsername}" 的发言数:`, usernameCounts.get(opUsername) || 0);

        // 根据用户名提取发言
        return extractPostsByUsername(opUsername);
    }

    // 将HTML转换为纯文本
    function htmlToText(html) {
        // 创建临时div来解析HTML
        const temp = document.createElement('div');
        temp.innerHTML = html;

        // 将<br>转换为换行符
        temp.querySelectorAll('br').forEach(br => {
            br.replaceWith('\n');
        });

        // 处理链接,保留URL
        temp.querySelectorAll('a').forEach(a => {
            const href = a.getAttribute('href');
            const text = a.textContent;
            if (href && href !== text) {
                a.replaceWith(`${text} (${href})`);
            }
        });

        // 获取纯文本内容
        return temp.textContent || temp.innerText || '';
    }

    // 生成TXT格式内容
    function generateTxtContent(posts, threadTitle) {
        const lines = [];
        lines.push('='.repeat(60));
        lines.push(`标题: ${threadTitle || '楼主发言汇总'}`);
        lines.push(`共 ${posts.length} 条发言`);
        lines.push(`导出时间: ${new Date().toLocaleString('zh-CN')}`);
        lines.push('='.repeat(60));
        lines.push('');

        posts.forEach((post, index) => {
            lines.push(`【${post.postNumber || (index + 1)}楼】`);
            lines.push(`用户: ${post.username}`);
            lines.push(`时间: ${post.date}`);
            lines.push(`ID: ${post.uid}`);
            lines.push('-'.repeat(40));

            // 转换HTML内容为纯文本
            const textContent = htmlToText(post.content);
            lines.push(textContent.trim());
            lines.push('');
            lines.push('');
        });

        return lines.join('\n');
    }

    // 下载文本文件
    function downloadTxtFile(content, filename) {
        const blob = new Blob([content], { type: 'text/plain;charset=utf-8' });
        const url = URL.createObjectURL(blob);
        const link = document.createElement('a');
        link.href = url;
        link.download = filename;
        document.body.appendChild(link);
        link.click();
        document.body.removeChild(link);
        URL.revokeObjectURL(url);
    }

    // 生成纯净的HTML页面
    function generateCleanHTML(posts, threadTitle) {
        const html = `
        <!DOCTYPE html>
        <html lang="ja">
        <head>
            <meta charset="UTF-8">
            <meta name="viewport" content="width=device-width, initial-scale=1.0">
            <title>${threadTitle || '楼主发言汇总'}</title>
            <style>
                body {
                    font-family: "Hiragino Kaku Gothic Pro", "ヒラギノ角ゴ Pro W3", "メイリオ", Meiryo, "MS Pゴシック", sans-serif;
                    line-height: 1.6;
                    max-width: 100%;
                    margin: 0;
                    padding: 20px;
                    background-color: #f5f5f5;
                }
                .thread-title {
                    font-size: 24px;
                    font-weight: bold;
                    margin-bottom: 30px;
                    text-align: center;
                    color: #333;
                    border-bottom: 2px solid #007bff;
                    padding-bottom: 10px;
                }
                .export-section {
                    text-align: center;
                    margin-bottom: 30px;
                }
                .export-btn {
                    background: #28a745;
                    color: white;
                    border: none;
                    padding: 10px 20px;
                    border-radius: 5px;
                    cursor: pointer;
                    font-size: 14px;
                    margin: 0 10px;
                    transition: background 0.3s;
                }
                .export-btn:hover {
                    background: #218838;
                }
                .post {
                    background: white;
                    margin-bottom: 20px;
                    padding: 15px;
                    border-radius: 8px;
                    box-shadow: 0 2px 4px rgba(0,0,0,0.1);
                    width: 100%;
                    box-sizing: border-box;
                }
                .post-header {
                    background: #f8f9fa;
                    padding: 10px;
                    margin: -15px -15px 15px -15px;
                    border-radius: 8px 8px 0 0;
                    font-size: 14px;
                    color: #666;
                    border-bottom: 1px solid #dee2e6;
                }
                .post-number {
                    font-weight: bold;
                    color: #007bff;
                    margin-right: 10px;
                }
                .post-content {
                    font-size: 16px;
                    line-height: 1.8;
                    color: #333;
                    width: 100%;
                    word-wrap: break-word;
                }
                .post-content br {
                    margin: 8px 0;
                }
                .reply_link {
                    color: #007bff;
                    text-decoration: none;
                }
                .reply_link:hover {
                    text-decoration: underline;
                }
                @media (max-width: 768px) {
                    body {
                        padding: 10px;
                    }
                    .post {
                        padding: 10px;
                    }
                    .post-header {
                        margin: -10px -10px 10px -10px;
                    }
                }
            </style>
        </head>
        <body>
            <h1 class="thread-title">${threadTitle || '楼主发言汇总'} (共${posts.length}条发言)</h1>

            <div class="export-section">
                <button class="export-btn" onclick="exportToTxt()">📄 导出为TXT文件</button>
                <button class="export-btn" onclick="window.print()">🖨️ 打印页面</button>
            </div>

            ${posts.map(post => `
                <div class="post">
                    <div class="post-header">
                        <span class="post-number">${post.postNumber}</span>
                        <span class="username">${post.username}</span>
                        <span style="float: right;">
                            <span class="date">${post.date}</span>
                            <span class="uid">${post.uid}</span>
                        </span>
                    </div>
                    <div class="post-content">${post.content}</div>
                </div>
            `).join('')}

            <script>
                // 导出数据
                const postsData = ${JSON.stringify(posts)};
                const threadTitle = "${threadTitle || '楼主发言汇总'}";

                // HTML转文本函数
                function htmlToText(html) {
                    const temp = document.createElement('div');
                    temp.innerHTML = html;

                    temp.querySelectorAll('br').forEach(br => {
                        br.replaceWith('\\n');
                    });

                    temp.querySelectorAll('a').forEach(a => {
                        const href = a.getAttribute('href');
                        const text = a.textContent;
                        if (href && href !== text) {
                            a.replaceWith(text + ' (' + href + ')');
                        }
                    });

                    return temp.textContent || temp.innerText || '';
                }

                function exportToTxt() {
                    const lines = [];
                    lines.push('${'='.repeat(60)}');
                    lines.push('标题: ' + threadTitle);
                    lines.push('共 ' + postsData.length + ' 条发言');
                    lines.push('导出时间: ' + new Date().toLocaleString('zh-CN'));
                    lines.push('${'='.repeat(60)}');
                    lines.push('');

                    postsData.forEach((post, index) => {
                        lines.push('【' + (post.postNumber || (index + 1)) + '楼】');
                        lines.push('用户: ' + post.username);
                        lines.push('时间: ' + post.date);
                        lines.push('ID: ' + post.uid);
                        lines.push('${'-'.repeat(40)}');

                        const textContent = htmlToText(post.content);
                        lines.push(textContent.trim());
                        lines.push('');
                        lines.push('');
                    });

                    const content = lines.join('\\n');
                    const blob = new Blob([content], { type: 'text/plain;charset=utf-8' });
                    const url = URL.createObjectURL(blob);
                    const link = document.createElement('a');
                    link.href = url;
                    link.download = (threadTitle || '楼主发言汇总') + '.txt';
                    document.body.appendChild(link);
                    link.click();
                    document.body.removeChild(link);
                    URL.revokeObjectURL(url);
                }
            </script>
        </body>
        </html>`;

        return html;
    }

    // 在新窗口中显示结果
    async function showCleanContent() {
        try {
            console.log('开始提取楼主内容...');
            const posts = await extractOPPosts();

            if (posts.length === 0) {
                alert(`未找到发言\n\n调试信息:\n- 页面总帖子数: ${document.querySelectorAll('[data-userid]').length}\n- 请查看控制台了解详细信息`);
                return;
            }

            console.log(`成功提取${posts.length}个帖子`);
            const threadTitle = document.querySelector('#threadtitle')?.textContent?.trim() ||
                              document.querySelector('h1')?.textContent?.trim() ||
                              '楼主发言汇总';

            const cleanHTML = generateCleanHTML(posts, threadTitle);

            const newWindow = window.open('', '_blank');
            if (newWindow) {
                newWindow.document.write(cleanHTML);
                newWindow.document.close();
                console.log('新窗口已打开,显示发言');
            } else {
                alert('无法打开新窗口,请检查浏览器弹窗设置');
            }
        } catch (error) {
            console.error('提取内容时出错:', error);
            alert('提取内容时出错: ' + error.message);
        }
    }

    // 创建可拖动按钮
    function createDraggableButton() {
        const button = document.createElement('button');
        button.className = 'op-extractor-btn';
        button.innerHTML = '楼主<br>提取';
        button.title = '点击提取楼主发言到新窗口';

        let isDragging = false;
        let dragOffset = { x: 0, y: 0 };
        let startPos = { x: 0, y: 0 };

        button.addEventListener('mousedown', (e) => {
            isDragging = true;
            startPos.x = e.clientX;
            startPos.y = e.clientY;
            dragOffset.x = e.clientX - button.offsetLeft;
            dragOffset.y = e.clientY - button.offsetTop;
            e.preventDefault();
        });

        document.addEventListener('mousemove', (e) => {
            if (isDragging) {
                button.style.left = (e.clientX - dragOffset.x) + 'px';
                button.style.top = (e.clientY - dragOffset.y) + 'px';
                button.style.right = 'auto';
            }
        });

        document.addEventListener('mouseup', (e) => {
            if (isDragging) {
                isDragging = false;
                const distance = Math.sqrt(
                    Math.pow(e.clientX - startPos.x, 2) +
                    Math.pow(e.clientY - startPos.y, 2)
                );
                if (distance < 5) {
                    showCleanContent();
                }
            }
        });

        document.body.appendChild(button);
        console.log('提取按钮已创建');
    }

    // 主函数
    function init() {
        console.log('脚本开始执行, URL:', window.location.href);

        // 检查是否需要跳转到完整页面
        if (switchToAllPosts()) {
            console.log('正在跳转到完整页面...');
            return;
        }

        // 等待页面加载完成后创建按钮
        setTimeout(() => {
            console.log('创建提取按钮...');
            createDraggableButton();
        }, 2000);
    }

    // 页面加载完成后执行
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', init);
    } else {
        init();
    }

})();