// ==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);
}
});
})();