您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Counts and displays the number of posts made by each user in a specific IgnBoards thread.
// ==UserScript== // @name IgnBoards - Thread Analytics // @namespace Violentmonkey Scripts // @description Counts and displays the number of posts made by each user in a specific IgnBoards thread. // @author Magof // @version 1.0 // @license MIT // @match https://www.ignboards.com/threads/* // @grant GM_addStyle // @grant GM_registerMenuCommand // ==/UserScript== const style = ` .loader { width: 100%; height: 30px; position: fixed; top: 0; left: 0; z-index: 9999; background-color: #f5f5f5; overflow: hidden; } .loader-bar { height: 100%; width: 0; background-color: #4caf50; animation: progress 3s linear infinite; } @keyframes progress { 0% { width: 0; } 100% { width: 100%; } } .loader-text { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); font-size: 14px; color: #333; white-space: nowrap; } .scrape-button { display: inline-block; padding: 8px 15px; background-color: #6c757d; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 14px; text-align: center; text-decoration: none; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); transition: background-color 0.3s ease; } .scrape-button:hover { background-color: #5a6268; } .scrape-button:disabled { background-color: #cccccc; cursor: not-allowed; } `; GM_addStyle(style); // Variáveis globais let isProcessing = false; let totalPages = 0; let currentPageNum = 0; function createScrapeButton() { const headerInner = document.querySelector('.uix_headerInner--opposite'); if (!headerInner) return; const button = document.createElement('button'); button.textContent = 'Analyze Thread'; button.className = 'scrape-button'; button.id = 'scrape-button'; button.onclick = startScraping; headerInner.appendChild(button); } async function fetchPage(url) { try { const response = await fetch(url); if (!response.ok) { throw new Error(`Failed to fetch page: ${response.status} ${response.statusText}`); } const body = await response.text(); return new DOMParser().parseFromString(body, 'text/html'); } catch (error) { console.error(`Erro ao buscar página ${url}:`, error); throw error; } } function getTotalPages(doc) { const pageNavElement = doc.querySelector('.pageNav-main'); if (!pageNavElement) return 1; const pageLinks = pageNavElement.querySelectorAll('a[data-page]'); if (pageLinks.length === 0) return 1; const pageNumbers = Array.from(pageLinks).map(link => parseInt(link.getAttribute('data-page'))); return Math.max(...pageNumbers); } function updateLoader(currentPage, totalPages, text) { const loaderText = document.querySelector('.loader-text'); if (loaderText) { loaderText.textContent = text || `Analyzing page ${currentPage}...`; } } async function extractPostData(url, currentPage = 1, processedPosts = new Set()) { try { const doc = await fetchPage(url); if (currentPage === 1) { totalPages = getTotalPages(doc); } updateLoader(currentPage, totalPages); // Seletor para posts const posts = doc.querySelectorAll('article.message div.message-userDetails'); const postData = {}; posts.forEach(post => { // Verificar se já processamos este post const postArticle = post.closest('article.message'); const postId = postArticle?.getAttribute('data-content') || postArticle?.id; if (postId && processedPosts.has(postId)) { return; // Pular posts já processados } if (postId) { processedPosts.add(postId); } // Seletor para username const userElement = post.querySelector('a.username') || post.querySelector('.username span') || post.querySelector('h4.message-name span') || post.querySelector('[data-user-id]'); let username = userElement ? userElement.textContent.trim() : 'Deleted member'; // Limpar username de caracteres especiais se necessário username = username.replace(/\s+/g, ' ').trim(); postData[username] = (postData[username] || 0) + 1; }); // Verificar se há próxima página const nextPageElement = doc.querySelector('.pageNav-jump.pageNav-jump--next'); const nextPage = nextPageElement ? nextPageElement.getAttribute('href') : null; if (nextPage) { const nextUrl = `https://www.ignboards.com${nextPage}`; currentPage++; // Delay de 1 segundo entre requisições await new Promise(resolve => setTimeout(resolve, 1000)); const nextPostData = await extractPostData(nextUrl, currentPage, processedPosts); // Combinar dados das páginas for (const [user, count] of Object.entries(nextPostData)) { postData[user] = (postData[user] || 0) + count; } } return postData; } catch (error) { console.error(`Erro na página ${currentPage}:`, error); updateLoader(currentPage, totalPages, `Error on page ${currentPage}. Continuing...`); // Em caso de erro, retornar dados coletados até agora return {}; } } function displayPostData(postData) { const sortedData = Object.entries(postData).sort((a, b) => b[1] - a[1]); const totalPosts = sortedData.reduce((sum, [, count]) => sum + count, 0); const totalUsers = sortedData.length; // Função para converter data to BBCode function convertToBBCode(data) { return data.map(([user, count]) => `[tr][td]${user}[/td][td]${count}[/td][/tr]`).join('\n'); } const bbcodeContent = `[table] [tr][th]User[/th][th]Posts[/th][/tr] ${convertToBBCode(sortedData)} [/table] [b]Total Posts:[/b] ${totalPosts} [b]Total Users:[/b] ${totalUsers}`; const html = ` <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Thread Analysis Report</title> <!-- Bootstrap CSS --> <link href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/css/bootstrap.min.css" rel="stylesheet"> <style> body { padding: 20px; } .container { max-width: 800px; margin: 0 auto; } h1 { margin-bottom: 20px; text-align: center; } .bbcode-area { margin-bottom: 20px; } .stats { background-color: #f8f9fa; padding: 15px; border-radius: 5px; margin-bottom: 20px; } .copy-success { color: #28a745; font-weight: bold; } </style> </head> <body> <div class="container"> <h1>Thread Analysis Report</h1> <div class="stats"> <div class="row"> <div class="col-md-6"> <strong>Total Posts:</strong> ${totalPosts} </div> <div class="col-md-6"> <strong>Total Users:</strong> ${totalUsers} </div> </div> </div> <div class="bbcode-area"> <h4>Export Code:</h4> <textarea id="bbcode" class="form-control" rows="12" readonly>${bbcodeContent}</textarea> <button class="btn btn-primary mt-3" onclick="copyBBCode()">Copy for Forum</button> <span id="copy-status" class="ml-2"></span> </div> <div class="table-responsive"> <table class="table table-striped"> <thead class="thead-dark"> <tr> <th>#</th> <th>Username</th> <th>Posts</th> <th>%</th> </tr> </thead> <tbody> ${sortedData.map(([user, count], index) => { const percentage = ((count / totalPosts) * 100).toFixed(1); return `<tr> <td>${index + 1}</td> <td>${user}</td> <td>${count}</td> <td>${percentage}%</td> </tr>`; }).join('')} </tbody> </table> </div> </div> <!-- Bootstrap JS and dependencies --> <script src="https://code.jquery.com/jquery-3.5.1.slim.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/@popperjs/[email protected]/dist/umd/popper.min.js"></script> <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/js/bootstrap.min.js"></script> <script> function copyBBCode() { const bbcodeTextarea = document.getElementById('bbcode'); const copyStatus = document.getElementById('copy-status'); bbcodeTextarea.select(); bbcodeTextarea.setSelectionRange(0, 99999); // Para mobile try { document.execCommand('copy'); copyStatus.textContent = 'Copied!'; copyStatus.className = 'copy-success'; setTimeout(() => { copyStatus.textContent = ''; copyStatus.className = ''; }, 2000); } catch (err) { console.error('Failed to copy: ', err); copyStatus.textContent = 'Failed to copy'; copyStatus.style.color = '#dc3545'; } } </script> </body> </html> `; const newTab = window.open(); newTab.document.open(); newTab.document.write(html); newTab.document.close(); } async function startScraping() { if (isProcessing) return; isProcessing = true; const button = document.getElementById('scrape-button'); // Desabilitar botão durante processamento if (button) { button.disabled = true; button.textContent = 'Analyzing...'; } const url = window.location.href; const loader = createLoader(); // Reset variáveis globais totalPages = 0; currentPageNum = 0; try { updateLoader(1, 0, 'Initializing...'); const postData = await extractPostData(url, 1); document.body.removeChild(loader); displayPostData(postData); } catch (error) { console.error('Erro durante o scraping:', error); document.body.removeChild(loader); alert('An error occurred while processing. Check console for details.'); } finally { isProcessing = false; // Reabilitar botão if (button) { button.disabled = false; button.textContent = 'Analyze Thread'; } } } function createLoader() { const loader = document.createElement('div'); loader.className = 'loader'; loader.innerHTML = ` <div class="loader-bar"></div> <div class="loader-text">Initializing...</div> `; document.body.appendChild(loader); return loader; } // Inicializar botão quando a página carregar if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', createScrapeButton); } else { createScrapeButton(); }
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址