// ==UserScript==
// @name Bahamut Forum Comment Scraper with AI Analysis
// @namespace http://tampermonkey.net/
// @version 1.2
// @description 爬取巴哈姆特論壇特定樓層留言並在新分頁跳轉到ChatGPT或Grok進行分析,或下載為JSON
// @author You
// @match https://forum.gamer.com.tw/C.php?bsn=*&snA=*
// @match https://chatgpt.com/*
// @match https://grok.com/*
// @grant GM_xmlhttpRequest
// @grant GM_download
// @grant GM_setValue
// @grant GM_getValue
// ==/UserScript==
(function() {
'use strict';
// 預設提示詞
const PROMPT_TEMPLATE = `你是一個專業的論壇討論分析工具。請分析以下巴哈姆特論壇留言資料:
{comments}
留言格式為:b+序號+(發言人暱稱:發言內容)
請提供:
1. 主要討論主題(例如遊戲特色、問題反饋)。
2. 情緒分析(計算正面、負面、中立留言的比例)。
3. 3-5個關鍵觀點(例如熱門意見或爭議點)。
4. 活躍用戶分析(列出留言最多的用戶)。
輸出格式為清晰的報告,包含標題和分段。`;
// 提取特定主樓層回覆
function scrapeMainReplies(targetFloor) {
let replies = [];
let blocks = document.querySelectorAll('section[id^="post_"]');
if (!blocks.length) {
console.error('未找到主樓層,可能頁面無內容或需要登入');
return replies;
}
blocks.forEach(block => {
let floorElement = block.querySelector('.floor, .c-section__title--reply');
let contentElement = block.querySelector('.c-article__content, .reply-content__article');
let floor = floorElement ? floorElement.dataset.floor || floorElement.innerText.trim() : '未知';
let content = contentElement ? contentElement.innerText.trim() : '無內容';
let snB = block.id.replace('post_', '');
if (!targetFloor || floor === targetFloor) {
replies.push({ floor: floor, content: content, subComments: [], snB: snB });
}
});
console.log('主樓層數量:', replies.length);
return replies;
}
// 提取子留言(支援分頁)
function scrapeSubComments(bsn, snB, snC = '', callback) {
let url = `https://forum.gamer.com.tw/ajax/moreCommend.php?bsn=${bsn}&snB=${snB}`;
if (snC) url += `&snC=${snC}`;
GM_xmlhttpRequest({
method: 'GET',
url: url,
headers: {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.92 Safari/537.36'
},
onload: function(response) {
try {
let data = JSON.parse(response.responseText);
let comments = [];
for (let key in data) {
if (key.match(/^\d+$/)) {
comments.push({
user: data[key].nick || '未知用戶',
content: data[key].comment || '無內容',
time: data[key].wtime || '未知時間'
});
}
}
if (data.next_snC) {
scrapeSubComments(bsn, snB, data.next_snC, nextComments => {
callback(comments.concat(nextComments));
});
} else {
callback(comments);
}
} catch (e) {
console.error(`解析子留言失敗 (snB=${snB}):`, e);
callback([]);
}
},
onerror: function() {
console.error(`子留言請求失敗: bsn=${bsn}, snB=${snB}`);
callback([]);
}
});
}
// 簡化留言為文字格式(限制長度)
function simplifyComments(replies) {
let text = '';
const maxLength = 8000; // 限制總長度以避免URL或localStorage溢出
let num = 0;
replies.forEach(reply => {
let entry = `${reply.floor}樓主文:\n ${reply.content.replace(/https?:\/\/\S+/g, "").replace(/#.*?#/g, "").replace(/\s+/g, " ").trim()}\n留言:\n`;
if (text.length + entry.length < maxLength) text += entry;
reply.subComments.reverse().forEach(sub => {
num+=1;
entry = `b${num}(${sub.user}:${sub.content.replace(/https?:\/\/\S+/g, "").replace(/#.*?#/g, "").replace(/\s+/g, " ").trim()})\n`;//${sub.time}
if (text.length + entry.length < maxLength) text += entry;
});
});
if (text.length >= maxLength) text = text.substring(0, maxLength - 3) + '...';
return text;
}
// 添加輸入框和按鈕(左下角,垂直疊放)
function addAnalysisButtons() {
const container = document.createElement('div');
container.style.position = 'fixed';
container.style.bottom = '10px';
container.style.left = '10px';
container.style.zIndex = '9999';
container.style.display = 'flex';
container.style.flexDirection = 'column';
container.style.gap = '10px';
// 樓層輸入框
const floorInput = document.createElement('input');
floorInput.type = 'text';
floorInput.placeholder = '輸入樓層號 (留空爬取所有樓層)';
floorInput.style.padding = '10px';
floorInput.style.border = '1px solid #ccc';
floorInput.style.borderRadius = '5px';
floorInput.style.width = '200px';
const chatgptButton = document.createElement('button');
chatgptButton.innerText = '使用ChatGPT分析';
chatgptButton.style.padding = '10px 20px';
chatgptButton.style.backgroundColor = '#4CAF50';
chatgptButton.style.color = 'white';
chatgptButton.style.border = 'none';
chatgptButton.style.borderRadius = '5px';
chatgptButton.style.cursor = 'pointer';
const grokButton = document.createElement('button');
grokButton.innerText = '使用Grok分析';
grokButton.style.padding = '10px 20px';
grokButton.style.backgroundColor = '#007BFF';
grokButton.style.color = 'white';
grokButton.style.border = 'none';
grokButton.style.borderRadius = '5px';
grokButton.style.cursor = 'pointer';
const downloadButton = document.createElement('button');
downloadButton.innerText = '下載JSON';
downloadButton.style.padding = '10px 20px';
downloadButton.style.backgroundColor = '#FF5733';
downloadButton.style.color = 'white';
downloadButton.style.border = 'none';
downloadButton.style.borderRadius = '5px';
downloadButton.style.cursor = 'pointer';
container.appendChild(floorInput);
container.appendChild(chatgptButton);
container.appendChild(grokButton);
container.appendChild(downloadButton);
document.body.appendChild(container);
return { floorInput, chatgptButton, grokButton, downloadButton };
}
// 爬取並跳轉(新分頁)
function scrapeAndRedirect(button, platform, floorInput) {
button.innerText = '爬取中...';
button.disabled = true;
let targetFloor = floorInput.value.trim();
let urlParams = new URL(window.location.href).searchParams;
let bsn = urlParams.get('bsn');
let snA = urlParams.get('snA');
let replies = scrapeMainReplies(targetFloor);
if (!replies.length) {
console.error('無主樓層資料,無法繼續爬取');
alert('無主樓層資料,請檢查頁面或輸入的樓層號是否正確');
button.innerText = platform === 'chatgpt' ? '使用ChatGPT分析' : '使用Grok分析';
button.disabled = false;
return;
}
let completed = 0;
replies.forEach(reply => {
scrapeSubComments(bsn, reply.snB, '', comments => {
reply.subComments = comments;
completed++;
console.log(`樓層 ${reply.floor} 子留言數: ${comments.length}`);
if (completed === replies.length) {
const commentsText = simplifyComments(replies);
const prompt = PROMPT_TEMPLATE.replace('{comments}', commentsText);
// 儲存提示詞到localStorage
GM_setValue('analysis_prompt', prompt);
// 新分頁跳轉
let targetUrl = platform === 'chatgpt' ? 'https://chatgpt.com/' : 'https://grok.com/';
window.open(targetUrl, '_blank');
button.innerText = platform === 'chatgpt' ? '使用ChatGPT分析' : '使用Grok分析';
button.disabled = false;
}
});
});
}
// 爬取並下載JSON
function scrapeAndDownload(button, floorInput) {
button.innerText = '爬取中...';
button.disabled = true;
let targetFloor = floorInput.value.trim();
let urlParams = new URL(window.location.href).searchParams;
let bsn = urlParams.get('bsn');
let snA = urlParams.get('snA');
let replies = scrapeMainReplies(targetFloor);
if (!replies.length) {
console.error('無主樓層資料,無法繼續爬取');
alert('無主樓層資料,請檢查頁面或輸入的樓層號是否正確');
button.innerText = '下載JSON';
button.disabled = false;
return;
}
let completed = 0;
replies.forEach(reply => {
scrapeSubComments(bsn, reply.snB, '', comments => {
reply.subComments = comments;
completed++;
console.log(`樓層 ${reply.floor} 子留言數: ${comments.length}`);
if (completed === replies.length) {
const jsonData = JSON.stringify(replies, null, 2);
const blob = new Blob([jsonData], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const filename = `bahamut_comments_${bsn}_${snA}_${timestamp}.json`;
GM_download({
url: url,
name: filename,
saveAs: true,
onload: () => {
URL.revokeObjectURL(url);
button.innerText = '下載JSON';
button.disabled = false;
},
onerror: () => {
console.error('下載失敗');
alert('下載JSON失敗,請重試');
button.innerText = '下載JSON';
button.disabled = false;
}
});
}
});
});
}
// 模擬自然輸入事件
function dispatchInputEvent(element, value) {
console.log('Dispatching input event to element:', element);
const setValue = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, 'value').set;
const inputEvent = new InputEvent('input', { bubbles: true, data: value });
const changeEvent = new Event('change', { bubbles: true });
setValue.call(element, value);
element.dispatchEvent(inputEvent);
element.dispatchEvent(changeEvent);
element.focus();
}
// 自動填充提示詞(ChatGPT或Grok頁面)
function autoFillPrompt() {
const prompt = GM_getValue('analysis_prompt', '');
if (!prompt) {
console.log('無提示詞,跳過自動填充');
return;
}
if (window.location.href.includes('chatgpt.com')) {
console.log('進入ChatGPT頁面,開始嘗試填充提示詞');
const tryFillChatGPT = (attempt = 1, maxAttempts = 5) => {
if (attempt > maxAttempts) {
console.error('ChatGPT自動填充失敗,超過最大重試次數');
GM_setValue('analysis_prompt', '');
return;
}
const inputBox = document.querySelector('#prompt-textarea, textarea[data-id="root"], textarea, [contenteditable="true"]');
console.log('找框');
//const inputBox = document.querySelector('textarea, [contenteditable="true"]');
console.log('找按鈕');
const submitButton = document.querySelector('button[data-testid="send-button"], button[aria-label="Send prompt"], button[aria-label*="send"], button[type="submit"]');
if (inputBox) {
console.log('找到ChatGPT輸入框和,開始填充 (嘗試次數:', attempt, ')');
console.log(prompt);
console.log(inputBox);
if (true && inputBox.tagName === 'DIV') {
inputBox.innerText = prompt;
inputBox.dispatchEvent(new Event('input', { bubbles: true }));
inputBox.dispatchEvent(new Event('change', { bubbles: true }));
} else {
inputBox.className = 'ProseMirror';
dispatchInputEvent(inputBox, prompt);
}
setTimeout(() => {
if (submitButton && !submitButton.disabled) {
console.log('提交按鈕可用,點擊提交');
submitButton.click();
GM_setValue('analysis_prompt', '');
// observer.disconnect();
} else {
// console.log('提交按鈕禁用,重試... (嘗試次數:', attempt, ')');
console.log('提交按鈕未找到或禁用,嘗試模擬Enter鍵');
const enterEvent = new KeyboardEvent('keydown', {
key: 'Enter',
code: 'Enter',
which: 13,
keyCode: 13,
bubbles: true,
cancelable: true
});
inputBox.dispatchEvent(enterEvent);
GM_setValue('analysis_prompt', '');
// setTimeout(() => tryFillChatGPT(observer, attempt + 1, maxAttempts), 2000);
}
}, 1500);
} else {
console.log('ChatGPT輸入框或按鈕未找到,重試... (嘗試次數:', attempt, ')');
setTimeout(() => tryFillChatGPT( attempt + 1, maxAttempts), 2000);
}
};
setTimeout(() => tryFillChatGPT(), 3000); // 初始延遲5000ms
} else if (window.location.href.includes('grok.com')) {
console.log('進入Grok頁面,開始嘗試填充提示詞');
const tryFill = (attempt = 1, maxAttempts = 5) => {
if (attempt > maxAttempts) {
console.error('Grok自動填充失敗,超過最大重試次數');
GM_setValue('analysis_prompt', '');
return;
}
const inputBox = document.querySelector('textarea, [contenteditable="true"]');
const submitButton = document.querySelector('button[type="submit"], button[aria-label*="send"], button[aria-label*="Send"]');
if (inputBox) {
console.log('找到Grok輸入框,開始填充');
if (inputBox.tagName === 'DIV') {
inputBox.innerText = prompt;
inputBox.dispatchEvent(new Event('input', { bubbles: true }));
inputBox.dispatchEvent(new Event('change', { bubbles: true }));
} else {
dispatchInputEvent(inputBox, prompt);
}
setTimeout(() => {
if (submitButton && !submitButton.disabled) {
console.log('提交按鈕可用,點擊提交');
submitButton.click();
GM_setValue('analysis_prompt', '');
} else {
console.log('提交按鈕未找到或禁用,嘗試模擬Enter鍵');
const enterEvent = new KeyboardEvent('keydown', {
key: 'Enter',
code: 'Enter',
which: 13,
keyCode: 13,
bubbles: true,
cancelable: true
});
inputBox.dispatchEvent(enterEvent);
GM_setValue('analysis_prompt', '');
}
}, 1500);
} else {
console.log('Grok輸入框未找到,重試... (嘗試次數:', attempt, ')');
setTimeout(() => tryFill(attempt + 1, maxAttempts), 1500);
}
};
setTimeout(() => tryFill(), 2000);
}
}
// 主邏輯
if (window.location.href.includes('forum.gamer.com.tw')) {
console.log('進入巴哈姆特論壇頁面,添加按鈕');
const { floorInput, chatgptButton, grokButton, downloadButton } = addAnalysisButtons();
chatgptButton.addEventListener('click', () => {
console.log('點擊ChatGPT分析按鈕');
scrapeAndRedirect(chatgptButton, 'chatgpt', floorInput);
});
grokButton.addEventListener('click', () => {
console.log('點擊Grok分析按鈕');
scrapeAndRedirect(grokButton, 'grok', floorInput);
});
downloadButton.addEventListener('click', () => {
console.log('點擊下載JSON按鈕');
scrapeAndDownload(downloadButton, floorInput);
});
} else if (window.location.href.includes('chatgpt.com') || window.location.href.includes('grok.com')) {
console.log('檢測到ChatGPT或Grok頁面,執行自動填充');
autoFillPrompt();
}
})();