// ==UserScript== // @name 超星作业答案提取器 // @namespace http://tampermonkey.net/ // @version 1.1 // @description 提取超星作业答案。支持作业列表页一键批量导出、详情页单页提取。适配各种题型和判分逻辑,智能提取正确答案。 // @author 毫厘 // @match *://*.chaoxing.com/* // @grant GM_setClipboard // @grant GM_addStyle // @grant GM_setValue // @grant GM_getValue // @grant GM_registerMenuCommand // @grant GM_xmlhttpRequest // @run-at document-end // @license MIT // ==/UserScript== (function() { 'use strict'; const AUTO_EXTRACT_KEY = 'chaoxingUniversalQuizAutoExtractEnabled_v1.0'; // ========================================================= // 0. 样式注入 (集成列表页按钮样式与提取结果样式) // ========================================================= GM_addStyle(` #cx-export-btn { display: inline-block; padding: 0 12px; background: #3A8BFF; border-radius: 35px; line-height: 30px; color: #fff; font-size: 14px; cursor: pointer; margin-left: 10px; border: none; font-weight: bold; transition: background 0.3s; vertical-align: middle; z-index: 999; } #cx-export-btn:hover { background: #1f6fe0; } #extractQuizButtonGM_merged { position: fixed; top: 70px; right: 20px; z-index: 10001; padding: 10px 15px; background-color: #007bff; color: white; border: none; border-radius: 5px; cursor: pointer; box-shadow: 0 2px 5px rgba(0,0,0,0.2); } #extractQuizButtonGM_merged:hover { background-color: #0056b3; } #universalExtractorContainerGM { margin-top: 25px; padding: 15px; background-color: #f0f0f0; border-top: 1px solid #dee2e6; border-radius: 0 0 8px 8px; clear: both; } #extractionOutputAreaGM { width: 95%; min-height: 300px; max-height: 70vh; margin: 20px auto; padding: 15px; border: 1px solid #ccc; border-radius: 4px; font-family: 'Courier New', Courier, monospace; font-size: 13px; line-height: 1.6; background-color: #fdfdfd; white-space: pre-wrap; } #copyExtractionButtonGM { display: block; margin: 10px auto 20px auto; padding: 12px 20px; background-color: #6c757d; color: white; border: none; border-radius: 4px; cursor: pointer; } #customNotificationGM { position: fixed; top: 20px; left: 50%; transform: translateX(-50%); color: white; padding: 12px 25px; border-radius: 6px; box-shadow: 0 4px 10px rgba(0,0,0,0.15); z-index: 2147483647; font-size: 15px; display: none; text-align: center; } `); // ========================================================= // 1. 基础辅助函数 // ========================================================= function normalizeText(text) { if (typeof text !== 'string') return ""; return text.replace(/\s+/g, ' ').trim(); } function normalizeQuestionText(rawText) { if (!rawText) return ""; let text = rawText.replace(/^【.*?题】\s*/, '').trim(); text = text.replace(/\s*[(\(]\s*\d+(\.\d+)?\s*分\s*[)\)]\s*$/, '').trim(); text = text.replace(/\(\s*( |\s)*\)/g, '( )'); text = text.replace(/(\s*( |\s)*)/g, '( )'); text = text.replace(/\(\s*\)/g, '( )').replace(/(\s*)/g, '( )'); return normalizeText(text); } function getCleanTextFromElement(element) { if (!element) return ""; const clone = element.cloneNode(true); const scriptsAndStyles = clone.querySelectorAll('script, style'); scriptsAndStyles.forEach(el => el.remove()); let html = clone.innerHTML; html = html.replace(/]*>/gi, '\n').replace(/<\/p>/gi, '\n').replace(/]*>/gi, '\n'); html = html.replace(/]*>/gi, '\n').replace(/<\/div>/gi, '\n'); const tempDiv = document.createElement('div'); tempDiv.innerHTML = html; let text = normalizeText(tempDiv.textContent || tempDiv.innerText || ""); return text.replace(/\n\s*\n/g, '\n').trim(); } // 显示通知 let notificationElement = null; function showCustomNotification(message, isError = false, duration = 3000) { if (!notificationElement) { notificationElement = document.createElement('div'); notificationElement.id = 'customNotificationGM'; document.body.appendChild(notificationElement); } notificationElement.textContent = message; notificationElement.style.backgroundColor = isError ? '#dc3545' : '#28a745'; notificationElement.style.display = 'block'; notificationElement.style.opacity = '1'; if(notificationElement.hideTimer) clearTimeout(notificationElement.hideTimer); if (duration > 0) { notificationElement.hideTimer = setTimeout(() => { notificationElement.style.display = 'none'; }, duration); } } // ========================================================= // 2. 核心提取逻辑 (适配解析) // ========================================================= function coreExtract(context) { const zyBottom = context.querySelector('#ZyBottom'); const fanyaMarking = context.querySelector('#fanyaMarking'); let output = ""; if (fanyaMarking) { output = extractFanyaMarking(fanyaMarking); } else if (zyBottom) { output = extractZyBottom(zyBottom); } return output; } // --- A: ZyBottom (考试/测验) --- function extractZyBottom(container) { let output = ""; let currentQuestionTypeName = "未知题型"; let questionTypeHeaderFoundOverall = false; const topLevelChildren = Array.from(container.children); for (const element of topLevelChildren) { const isHeader = element.classList.contains('Cy_TItle1') || element.classList.contains('newTestType') || (element.tagName === 'H3' && element.className.includes('TestType')) || element.querySelector('.newTestType'); if (isHeader) { questionTypeHeaderFoundOverall = true; const rawHeaderText = normalizeText(element.textContent) .replace(/^[一二三四五六七八九十]+[、.]\s*/, '') .replace(/\(.*?\)$/, '') .replace(/\(.*\)$/, '') .trim(); currentQuestionTypeName = detectTypeName(rawHeaderText); output = appendHeader(output, currentQuestionTypeName); } else if (element.classList.contains('CyBottom') || element.classList.contains('TiMu')) { if (!questionTypeHeaderFoundOverall && output.indexOf("## ") === -1) { output = appendHeader(output, currentQuestionTypeName); questionTypeHeaderFoundOverall = true; } const questions = element.classList.contains('TiMu') ? [element] : element.querySelectorAll('.TiMu'); questions.forEach(q => output += processZyBottomQuestion(q)); } else { const nestedQuestions = element.querySelectorAll('.TiMu'); if (nestedQuestions.length > 0) { if (!questionTypeHeaderFoundOverall && output.indexOf("## ") === -1) { output = appendHeader(output, currentQuestionTypeName); questionTypeHeaderFoundOverall = true; } nestedQuestions.forEach(q => output += processZyBottomQuestion(q)); } } } return output; } // --- B: FanyaMarking (作业详情) --- function extractFanyaMarking(container) { let output = ""; const markItems = container.querySelectorAll('.mark_item'); if (markItems.length === 0) { const questions = container.querySelectorAll('.questionLi'); questions.forEach(q => output += processFanyaQuestion(q)); return output; } markItems.forEach(item => { const typeHeader = item.querySelector('.type_tit'); let typeName = "未知题型"; if (typeHeader) { const rawText = normalizeText(typeHeader.textContent) .replace(/^[一二三四五六七八九十]+[、.]\s*/, '') .replace(/\(.*?\)$/, '') .trim(); typeName = detectTypeName(rawText); output = appendHeader(output, typeName); } const questions = item.querySelectorAll('.questionLi'); questions.forEach(q => output += processFanyaQuestion(q)); }); return output; } // --- 单题处理 (ZyBottom) --- function processZyBottomQuestion(questionElement) { let qOutput = ""; try { const titleDiv = questionElement.querySelector('.Cy_TItle, .Zy_TItle'); let qNum = "?."; let qText = "题干未能提取"; if (titleDiv) { const numEl = titleDiv.querySelector('i.fl'); if (numEl) qNum = normalizeText(numEl.textContent) + "."; const contentEl = titleDiv.querySelector('.qtContent') || titleDiv.querySelector('div.clearfix'); if(contentEl) qText = normalizeQuestionText(getCleanTextFromElement(contentEl)); } qOutput += qNum + " " + qText + "\n"; const options = questionElement.querySelectorAll('.Cy_ulTop li, .Zy_ulTop li'); options.forEach(opt => { const label = opt.querySelector('i.fl')?.textContent.trim() || ""; let text = ""; const anchor = opt.querySelector('a'); if (anchor) text = normalizeText(anchor.textContent); else { let clone = opt.cloneNode(true); clone.querySelector('i.fl')?.remove(); text = normalizeText(clone.textContent); } qOutput += label + " " + text + "\n"; }); let finalAns = "未能提取"; let found = false; const newBx = questionElement.querySelector('.newAnswerBx'); if (newBx) { const rightNode = newBx.querySelector('.correctAnswerBx .answerCon, .correctAnswer .answerCon'); if (rightNode) { finalAns = normalizeText(rightNode.innerText); found = true; } if (!found) { const multiBlankBx = newBx.querySelector('.myAllAnswerBx'); if (multiBlankBx) { const subAnswers = multiBlankBx.querySelectorAll('.myAnswerBx'); let tempAnswers = []; let hasWrong = false; subAnswers.forEach(sub => { const ansContent = sub.querySelector('.myAnswer'); if (ansContent) { let text = normalizeText(ansContent.innerText); text = text.replace(/第[一二三四五六七八九十\d]+空[::]/g, '').trim(); const isCuo = sub.querySelector('.marking_cuo'); if (isCuo) hasWrong = true; tempAnswers.push(text); } }); if (tempAnswers.length > 0) { finalAns = hasWrong ? "暂未找到正确答案" : tempAnswers.join("; "); found = true; } } else { const myNode = newBx.querySelector('.myAnswerBx .answerCon, .myAnswer .answerCon, .myAnswer'); const statusSpan = newBx.querySelector('.answerScore .CorrectOrNot span'); if (myNode) { let myText = normalizeText(myNode.innerText); myText = myText.replace(/^我的答案[::]/, '').trim(); if (statusSpan && statusSpan.classList.contains('marking_dui')) { finalAns = myText; } else if (statusSpan && statusSpan.classList.contains('marking_cuo')) { finalAns = "暂未找到正确答案"; } else { finalAns = "(仅供参考) " + myText; } } } } } else { const rightSpan = Array.from(questionElement.querySelectorAll('.Py_answer span')).find(s => s.textContent.includes('正确答案')); if(rightSpan) finalAns = rightSpan.textContent.replace('正确答案:','').trim(); } if (finalAns === '√' || finalAns === 'true') finalAns = '对'; if (finalAns === '×' || finalAns === 'false') finalAns = '错'; qOutput += "答案:" + finalAns + "\n"; const analysis = questionElement.querySelector('.answerKeyBx .answerCon') || questionElement.querySelector('.Py_addpy .pingyu'); if(analysis) qOutput += "解析:" + getCleanTextFromElement(analysis) + "\n"; return qOutput + "\n"; } catch (e) { console.error(e); return ""; } } // --- 单题处理 (FanyaMarking - 作业详情) --- function processFanyaQuestion(qEl) { let qOutput = ""; try { const nameEl = qEl.querySelector('h3.mark_name'); let qNum = "?."; let qText = ""; if (nameEl) { const rawTitle = normalizeText(nameEl.textContent); const match = rawTitle.match(/^(\d+)[.、\s]/); if (match) qNum = match[1] + "."; const contentEl = nameEl.querySelector('.qtContent'); if (contentEl) qText = normalizeQuestionText(getCleanTextFromElement(contentEl)); } qOutput += qNum + " " + qText + "\n"; const options = qEl.querySelectorAll('.options li, .stem_answer li'); options.forEach(opt => { qOutput += normalizeText(opt.textContent) + "\n"; }); let finalAns = ""; const answerBlock = qEl.querySelector('.mark_answer'); if (answerBlock) { const fullText = normalizeText(answerBlock.textContent); const correctMatch = fullText.match(/正确答案[::]\s*([A-Za-z0-9\u4e00-\u9fa5]+)/); if (correctMatch) { finalAns = correctMatch[1]; } else { const myAnsEl = answerBlock.querySelector('.stuAnswerContent') || answerBlock.querySelector('.mark_fill dd') || answerBlock.querySelector('.mark_fill'); if (myAnsEl) { let myText = getCleanTextFromElement(myAnsEl); myText = myText.replace(/^我的答案[::]/, '').trim(); const isCorrect = qEl.querySelector('.marking_dui') || answerBlock.querySelector('.marking_dui') || answerBlock.querySelector('.icon_ok'); const isWrong = qEl.querySelector('.marking_cuo') || answerBlock.querySelector('.marking_cuo'); if (isCorrect) { finalAns = myText; } else if (isWrong) { finalAns = "暂未找到正确答案"; } else { finalAns = "(我的答案) " + myText; } } } } if (!finalAns) finalAns = "未能提取答案"; qOutput += "答案:" + finalAns + "\n\n"; } catch (e) { console.error(e); return ""; } return qOutput; } // --- 通用工具 --- function detectTypeName(text) { if (text.includes("单选题")) return "单选题"; if (text.includes("多选题")) return "多选题"; if (text.includes("判断题")) return "判断题"; if (text.includes("填空题")) return "填空题"; if (text.includes("简答题")) return "简答题"; if (text.includes("资料题") || text.includes("论述题")) return "资料题/论述题"; return text || "未知题型"; } function appendHeader(currentOutput, typeName) { let prefix = ""; if (currentOutput && !currentOutput.endsWith("\n\n")) prefix = "\n"; if (currentOutput && currentOutput.includes(`## ${typeName}`)) return currentOutput; return currentOutput + prefix + `## ${typeName}\n`; } // ========================================================= // 3. 批量导出模块 (Work List Page) // ========================================================= function isWorkListPage() { return document.querySelector('.bottomList') !== null; } // 添加导出按钮到作业列表页 function tryAddExportButton() { if (!isWorkListPage()) return; if (document.getElementById('cx-export-btn')) return; // 尝试插入到筛选栏 (.filter) 或 顶部 (.top-back) const target = document.querySelector('.filter') || document.querySelector('.top-back'); if (target) { const btn = document.createElement('button'); btn.id = 'cx-export-btn'; btn.innerText = '📥 一键导出所有作业'; btn.onclick = startBatchExport; target.appendChild(btn); } } // 开始批量导出 async function startBatchExport() { const listItems = document.querySelectorAll('.bottomList ul li'); const tasks = []; listItems.forEach(li => { const statusNode = li.querySelector('.status'); const statusText = statusNode ? statusNode.innerText.trim() : ""; const titleNode = li.querySelector('.overHidden2'); const title = titleNode ? titleNode.innerText.trim() : "未命名作业"; const url = li.getAttribute('data'); if (statusText && (statusText.includes('已') || statusText.includes('待') || statusText.includes('完成') || statusText.includes('互评'))) { if (url) { tasks.push({ title, url, status: statusText }); } } }); if (tasks.length === 0) { showCustomNotification('⚠️ 未找到可导出的作业 (请确认作业状态)', true); return; } let finalOutput = ""; showCustomNotification(`🚀 发现 ${tasks.length} 个有效作业,开始导出...`, false, 0); for (let i = 0; i < tasks.length; i++) { const task = tasks[i]; const progressMsg = `⏳ (${i + 1}/${tasks.length}) 正在获取:${task.title}`; if (notificationElement) notificationElement.textContent = progressMsg; try { const html = await fetchDetail(task.url); const doc = new DOMParser().parseFromString(html, 'text/html'); finalOutput += `# ${task.title} (${task.status})\n\n`; const extracted = coreExtract(doc); if (!extracted || extracted.trim() === "") { finalOutput += "[提示:该作业内容无法提取,可能需要手动查看]\n\n"; } else { finalOutput += extracted + "\n\n" + "=".repeat(40) + "\n\n"; } } catch (e) { console.error(e); finalOutput += `# ${task.title} [导出失败: ${e.message}]\n\n`; } await new Promise(r => setTimeout(r, 800)); } downloadFile(finalOutput, `超星作业批量导出_${new Date().toISOString().slice(0,10)}.txt`); showCustomNotification('✅ 导出完成,已触发下载!', false, 4000); } function fetchDetail(url) { return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: "GET", url: url, headers: { "Referer": window.location.href, "X-Requested-With": "XMLHttpRequest" }, onload: (res) => { if (res.status === 200) { resolve(res.responseText); } else { reject(`HTTP Error ${res.status}`); } }, onerror: (err) => reject(err) }); }); } function downloadFile(content, filename) { const blob = new Blob([content], { type: 'text/plain;charset=utf-8' }); const link = document.createElement('a'); link.href = URL.createObjectURL(blob); link.download = filename; document.body.appendChild(link); link.click(); document.body.removeChild(link); } // ========================================================= // 4. 单页提取模块 (Work View Page) // ========================================================= function createSinglePageExtractButton() { if (isWorkListPage()) return; // 列表页不显示此按钮 // 判断当前页面是否真的是考试详情页或作业页 if (!document.querySelector('#ZyBottom') && !document.querySelector('#fanyaMarking')) return; if (document.getElementById('extractQuizButtonGM_merged')) return; const btn = document.createElement('button'); btn.id = 'extractQuizButtonGM_merged'; btn.textContent = '提取本页题目'; btn.onclick = function() { const originalText = this.textContent; this.textContent = '正在提取...'; this.disabled = true; this.style.backgroundColor = '#5a6268'; setTimeout(() => { const output = coreExtract(document); if (output) { GM_setClipboard(output); showCustomNotification('题目已提取并复制!'); displayOutput(output); this.textContent = '提取完成!'; this.style.backgroundColor = '#218838'; } else { showCustomNotification('未能找到题目容器', true); this.textContent = '失败'; this.style.backgroundColor = '#dc3545'; } setTimeout(() => { this.textContent = originalText; this.style.backgroundColor = '#007bff'; this.disabled = false; }, 2500); }, 50); }; document.body.appendChild(btn); } function displayOutput(output) { let container = document.getElementById("universalExtractorContainerGM"); if (!container) { container = document.createElement('div'); container.id = "universalExtractorContainerGM"; const area = document.createElement('textarea'); area.id = 'extractionOutputAreaGM'; area.readOnly = true; container.appendChild(area); const copyBtn = document.createElement('button'); copyBtn.id = 'copyExtractionButtonGM'; copyBtn.textContent = '复制内容'; copyBtn.onclick = () => { GM_setClipboard(area.value); showCustomNotification('已复制!'); }; container.appendChild(copyBtn); const target = document.querySelector('#ZyBottom') || document.querySelector('#fanyaMarking'); if (target && target.parentNode) { target.parentNode.insertBefore(container, target.nextSibling); } else { document.body.appendChild(container); } } container.querySelector('textarea').value = output; } // ========================================================= // 5. 初始化与菜单 // ========================================================= function toggleAutoExtract() { let isEnabled = GM_getValue(AUTO_EXTRACT_KEY, false); GM_setValue(AUTO_EXTRACT_KEY, !isEnabled); alert(`自动提取功能已 ${!isEnabled ? '开启' : '关闭'} (刷新生效)`); registerMenuCommands(); } function registerMenuCommands() { let isEnabled = GM_getValue(AUTO_EXTRACT_KEY, false); GM_registerMenuCommand(`${isEnabled ? '✅' : '❌'} 自动提取 (当前: ${isEnabled ? '开' : '关'})`, toggleAutoExtract); GM_registerMenuCommand("📥 强制导出列表", () => { if(isWorkListPage()) startBatchExport(); else alert("请在作业列表页使用"); }); } // 启动逻辑 registerMenuCommands(); // 定时检查列表页,添加导出按钮 setInterval(() => { tryAddExportButton(); createSinglePageExtractButton(); }, 1500); // 自动提取逻辑 (仅在非列表页生效) window.addEventListener('load', function() { if (!isWorkListPage() && GM_getValue(AUTO_EXTRACT_KEY, false)) { setTimeout(() => { const output = coreExtract(document); if(output) { GM_setClipboard(output); showCustomNotification('自动提取成功'); displayOutput(output); } }, 1200); } }); })();