// ==UserScript== // @name 学习通万能题目 提取器 // @namespace http://tampermonkey.net/ // @version 3.0.0 // @description 提取学习通所有题目 // @author kkkxfr // @match *://*.chaoxing.com/exam-ans/exam/test/reVersionPaperMarkContentNew* // @match *://*.chaoxing.com/exam-ans/exam/test/look* // @match *://*.chaoxing.com/mooc-ans/work/selectWorkQuestionYiPiYue* // @match *://*.mooc1.chaoxing.com/mooc-ans/mooc2/work/view?* // @match *://*.chaoxing.com/work/doHomeWorkNew* // @match *://*.chaoxing.com/mooc-ans/work/doHomeWorkNew* // @match *://*.chaoxing.com/api/selectWorkQuestionYiPiYue* // @grant GM_setClipboard // @grant GM_addStyle // @grant GM_setValue // @grant GM_getValue // @grant GM_registerMenuCommand // ==/UserScript== (function() { 'use strict'; const AUTO_EXTRACT_KEY = 'chaoxing_auto_extract_enabled'; function normalizeText(text) { if (typeof text !== 'string') return ""; return text.replace(/\s+/g, ' ').trim(); } function getCleanText(element) { if (!element) return ""; const clone = element.cloneNode(true); clone.querySelectorAll('script, style').forEach(el => el.remove()); let html = clone.innerHTML; html = html.replace(/
]*>/gi, '\n').replace(/
]*>/gi, '\n');
const tempDiv = document.createElement('div');
tempDiv.innerHTML = html;
return normalizeText(tempDiv.textContent || tempDiv.innerText || "");
}
function normalizeQuestionText(rawText) {
if (!rawText) return "";
let text = rawText.replace(/^\s*\d+[.、]\s*/, '').trim();
text = text.replace(/\s*[(\(]\s*\d+(\.\d+)?\s*分\s*[)\)]\s*$/, '').trim();
text = text.replace(/\(\s*( |\s)*\)/g, '( )').replace(/(\s*( |\s)*)/g, '( )');
return normalizeText(text);
}
function getPageTitle() {
const courseSelectors = [
'#workTitle', '.work_title', '.courseName', '.course-title', '.courseTitle',
'.mainHead', '.mark_title', '.title', 'h1', 'h2'
];
let courseName = "";
for (const sel of courseSelectors) {
const el = document.querySelector(sel);
if (el) {
const t = normalizeText(el.innerText || el.textContent || "");
if (t && t.length > courseName.length) courseName = t;
}
}
const bodyText = normalizeText(document.body.innerText || document.body.textContent || "");
const hasZhangjie = /章节测验|章节 测验|自测|章节测试|章节练习/i.test(bodyText);
if (courseName && hasZhangjie) {
const cleaned = courseName.replace(/章节测验/gi, '').trim();
return cleaned ? `${cleaned} 章节测验` : '章节测验';
}
let best = "";
['.mark_title','h1','h2','h3','.title','.mainHead','#workTitle','.work_title'].forEach(sel => {
document.querySelectorAll(sel).forEach(el => {
const txt = normalizeText(el.innerText || el.textContent || "");
if (!txt) return;
if (txt.length > best.length && /章节测验|测验|自测|练习|复习自测/i.test(txt)) best = txt;
});
});
if (best) return best;
if (courseName && courseName.length > 4) return courseName;
return "章节测验";
}
function detectLegacyType(qNode) {
try {
const txt = (qNode.innerText || qNode.textContent || '').replace(/\s+/g, ' ');
if (/单选/.test(txt)) return '单选题';
if (/多选/.test(txt)) return '多选题';
if (/判断|是非/.test(txt)) return '判断题';
if (/填空/.test(txt)) return '填空题';
} catch (e) {}
return '单选题';
}
function parseScoreFromNode(qNode) {
const scoreEl = qNode.querySelector('.mark_score, .score, .question-score');
let txt = "";
if (scoreEl) txt = getCleanText(scoreEl);
if (!txt) {
const match = qNode.innerText.match(/([\d.]+)\s*分/);
if (match) txt = match[1];
}
const numMatch = txt && txt.match(/([\d.]+)/);
if (numMatch) return parseFloat(numMatch[1]);
return null;
}
function formatOptionLabel(rawLabel) {
if (!rawLabel) return "";
const cleaned = rawLabel.replace(/[\s\.\、,,]+/g, '').trim();
const m = cleaned.match(/^([A-Z])$/i);
if (m) return m[1].toUpperCase();
return cleaned;
}
// 统一且鲁棒的正确答案提取器(保留现有策略)
function extractCorrectAnswer(qNode, optMap) {
const rightCont = qNode.querySelector('.rightAnswerContent, .right_answer, .answerRight');
if (rightCont) {
const txt = getCleanText(rightCont);
if (txt && !/未提取|未作答|暂无/.test(txt)) {
return txt;
}
}
const candidates = Array.from(qNode.querySelectorAll('span, div, p, td, li'));
for (const el of candidates) {
const t = (el.textContent || '').trim();
if (!t) continue;
if (/(?:正确答案|参考答案|答案)[::]?/.test(t) && !/我的答案/.test(t)) {
const m = t.match(/(?:正确答案|参考答案|答案)[::]?\s*([\s\S]{1,80})/);
if (m && m[1]) {
const val = m[1].trim();
if (val && !/未提取|未作答|暂无/.test(val)) return val;
}
}
}
const hiddenIcon = qNode.querySelector('.Py_answer span.element-invisible-hidden i, .element-invisible-hidden i');
if (hiddenIcon) {
const sym = hiddenIcon.innerText.trim();
if (sym) return (sym === "√") ? "对" : (sym === "×") ? "错" : sym;
}
const hiddenP = qNode.querySelector('.Py_answer span.element-invisible-hidden p, .element-invisible-hidden p');
if (hiddenP) {
let v = normalizeText(hiddenP.innerText || hiddenP.textContent || "");
if (v && !/未提取|未作答|暂无/.test(v)) {
if (v.toLowerCase() === "true") return "对";
if (v.toLowerCase() === "false") return "错";
return v;
}
}
if (!optMap) {
optMap = [];
const opts = qNode.querySelectorAll('.mark_letter li, .Cy_ulTop li, .Zy_ulTop li, .options li, li');
opts.forEach(opt => {
const label = opt.querySelector('.mark_letter_span')?.innerText?.trim()
|| opt.querySelector('i.fl')?.innerText?.trim()
|| (opt.innerText.trim().slice(0,1));
const text = normalizeText(opt.textContent || opt.innerText || "");
optMap.push({label: label ? label.replace(/[^A-Za-z0-9\u4e00-\u9fa5]/g,'') : '', text});
});
}
const optNodes = qNode.querySelectorAll('.mark_letter li, .Cy_ulTop li, .Zy_ulTop li, .options li, li');
for (let opt of optNodes) {
const hasIcon = opt.querySelector('.icon-ok, .dui, .check_answer_right, i.icon-ok, i.dui, .ok');
const inner = opt.innerText || opt.textContent || "";
if (hasIcon || /√/.test(inner)) {
const labelNode = opt.querySelector('.mark_letter_span') || opt.querySelector('i.fl') || null;
let label = labelNode ? normalizeText(labelNode.innerText || labelNode.textContent || '') : '';
label = label.replace(/[^A-Za-z0-9\u4e00-\u9fa5]/g,'').trim();
if (label) return label;
const txt = normalizeText(opt.textContent || opt.innerText || "");
const found = optMap.find(o => txt.indexOf(o.text) !== -1 || o.text.indexOf(txt) !== -1);
if (found && found.label) return found.label;
}
}
const mAll = (qNode.innerText || "").match(/(?:正确答案|参考答案)[::]?\s*([A-Za-z0-9\u4e00-\u9fa5\,\、\.\s]+)/);
if (mAll && mAll[1]) {
const val = mAll[1].trim();
if (val && !/未提取|未作答|暂无/.test(val)) return val;
}
return null;
}
function formatQuestionSimple(qNode, index, secTitle) {
const nameTag = qNode.querySelector('.mark_name') || qNode.querySelector('.Cy_TItle') || qNode;
const rawBody = getCleanText(nameTag);
const qText = normalizeQuestionText(rawBody);
const detectedType = detectLegacyType(qNode);
let showTypeLabel = true;
if (secTitle && /单选|多选|判断|填空/.test(secTitle)) {
if (secTitle.indexOf(detectedType) !== -1) showTypeLabel = false;
}
let out = "";
if (showTypeLabel) {
out = `${index}. 【${detectedType}】 ${qText}\n`;
} else {
out = `${index}. ${qText}\n`;
}
const opts = qNode.querySelectorAll('.mark_letter li, .Cy_ulTop li, .Zy_ulTop li, .options li, li');
let optMap = [];
if (opts && opts.length > 0) {
opts.forEach(opt => {
const labelNode = opt.querySelector('.mark_letter_span') || opt.querySelector('i.fl') || null;
let label = labelNode ? normalizeText(labelNode.innerText || labelNode.textContent || '') : '';
label = formatOptionLabel(label);
let clone = opt.cloneNode(true);
if (clone.querySelector('.mark_letter_span')) clone.querySelector('.mark_letter_span').remove();
if (clone.querySelector('i.fl')) clone.querySelector('i.fl').remove();
const text = normalizeText(clone.textContent || clone.innerText || "");
if (label) {
out += `${label}. ${text.replace(/^[A-D][\.\、\s]*/i, '').trim()}\n`;
} else {
out += `${text}\n`;
}
optMap.push({label, text});
});
}
// 正确答案:使用统一提取器(传入 optMap)
const rightAns = extractCorrectAnswer(qNode, optMap);
// 输出顺序:不输出我的答案;仅在 rightAns 非 null 时输出正确答案
if (rightAns !== null && rightAns !== "") {
out += `正确答案:${rightAns}\n`;
}
const analysis = getCleanText(qNode.querySelector('.analysisDiv, .analysis, .py_analyse, .Py_addpy .pingyu')) || "";
if (analysis) out += `答案解析:${analysis}\n`;
out += `\n`;
return out;
}
function extractModernExam(container) {
let output = `# ${getPageTitle()}\n\n`;
const nodes = Array.from(container.querySelectorAll('.type_tit, .questionLi'));
if (nodes.length === 0) return null;
let sections = [];
let current = null;
nodes.forEach(node => {
if (node.classList.contains('type_tit')) {
const title = normalizeText(node.innerText || node.textContent || "");
current = { title: title || "大题", questions: [] };
sections.push(current);
} else if (node.classList.contains('questionLi')) {
if (!current) {
current = { title: "章节测验题目", questions: [] };
sections.push(current);
}
current.questions.push(node);
}
});
if (sections.length === 0 && nodes.some(n => n.classList.contains('questionLi'))) {
const qs = nodes.filter(n => n.classList.contains('questionLi'));
sections.push({ title: "章节测验题目", questions: qs });
}
sections.forEach(sec => {
let secTitle = sec.title;
const firstQ = sec.questions[0];
const inferredType = firstQ ? detectLegacyType(firstQ) : null;
if (!/单选|多选|判断|填空/.test(secTitle) && inferredType) {
secTitle = inferredType;
}
const qCount = sec.questions.length;
let totalScore = 0;
let scoreKnown = false;
sec.questions.forEach(q => {
const s = parseScoreFromNode(q);
if (typeof s === 'number') {
totalScore += s;
scoreKnown = true;
}
});
let scorePart = "";
if (scoreKnown) {
scorePart = `,${+totalScore.toFixed(2)} 分`;
}
output += `## ${secTitle}(共 ${qCount} 题${scorePart})\n\n`;
sec.questions.forEach((qNode, idx) => {
output += formatQuestionSimple(qNode, idx + 1, secTitle);
});
});
return output;
}
function extractLegacyQuiz(container) {
let output = `# ${getPageTitle()}\n\n`;
const directQuestions = Array.from(container.querySelectorAll('.TiMu'));
const hasHeaders = !!container.querySelector('.Cy_TItle1, h2');
if (!hasHeaders && directQuestions.length > 0) {
const groups = {};
directQuestions.forEach(qNode => {
const type = detectLegacyType(qNode) || '单选题';
if (!groups[type]) groups[type] = [];
groups[type].push(qNode);
});
const order = ['单选题','多选题','判断题','填空题'];
const remaining = Object.keys(groups).filter(t => !order.includes(t));
const finalOrder = order.concat(remaining);
finalOrder.forEach(type => {
const arr = groups[type];
if (!arr || arr.length === 0) return;
output += `## ${type}(共 ${arr.length} 题)\n\n`;
arr.forEach((qNode, idx) => {
output += processLegacySingleQuestion(qNode, idx + 1, type);
});
});
} else {
const children = container.children;
let currentType = "题目";
for (let el of children) {
if (el.classList.contains('Cy_TItle1') || el.tagName === 'H2') {
let text = normalizeText(el.textContent || el.innerText || "");
currentType = text.replace(/^[一二三四五]+[、.]\s*/, '').trim();
output += `\n## ${currentType}\n\n`;
} else if (el.classList.contains('TiMu') || el.querySelectorAll('.TiMu').length > 0) {
const questions = el.classList.contains('TiMu') ? [el] : el.querySelectorAll('.TiMu');
questions.forEach(qNode => {
output += processLegacySingleQuestion(qNode, null, currentType);
});
}
}
}
return output;
}
function processLegacySingleQuestion(qNode, forceIndex = null, forcedType = null) {
let res = "";
try {
const titleDiv = qNode.querySelector('.Cy_TItle .clearfix, .Zy_TItle .clearfix') || qNode.querySelector('.Cy_TItle') || qNode;
if(!titleDiv) return "";
const numDom = qNode.querySelector('i.fl');
const num = numDom ? normalizeText(numDom.innerText) + "." : (forceIndex ? forceIndex + "." : "?.");
const body = normalizeQuestionText(getCleanText(titleDiv));
const detectedType = detectLegacyType(qNode);
let showTypeLabel = true;
if (forcedType && /单选|多选|判断|填空/.test(forcedType)) {
if (forcedType.indexOf(detectedType) !== -1) showTypeLabel = false;
}
if (showTypeLabel) {
res += `${num} 【${detectedType}】 ${body}\n`;
} else {
res += `${num} ${body}\n`;
}
const options = qNode.querySelectorAll('.Cy_ulTop li, .Zy_ulTop li');
const optMap = [];
options.forEach(li => {
const labelIcon = li.querySelector('i.fl') || li.querySelector('.mark_letter_span');
let label = labelIcon ? normalizeText(labelIcon.innerText || labelIcon.textContent) : "";
label = formatOptionLabel(label);
let clone = li.cloneNode(true);
if(clone.querySelector('i.fl')) clone.querySelector('i.fl').remove();
if(clone.querySelector('.mark_letter_span')) clone.querySelector('.mark_letter_span').remove();
const text = normalizeText(clone.textContent || clone.innerText || "");
optMap.push({label, text});
if (label) res += `${label}. ${text}\n`;
else res += `${text}\n`;
});
// 使用统一提取器
const answer = extractCorrectAnswer(qNode, optMap);
// 不输出“我的答案”,仅在有 answer 时输出正确答案
if (answer && answer !== "") {
res += `正确答案:${answer}\n`;
}
const analysisDiv = qNode.querySelector('.Py_addpy .pingyu, .analysis');
if (analysisDiv) {
res += `答案解析:${getCleanText(analysisDiv)}\n`;
}
res += "\n";
} catch (e) { console.error("Legacy question error:", e); }
return res;
}
function extractAndDisplay(isManual = false) {
let result = "";
let method = "";
const modernContainer = document.querySelector('.fanyaMarking') || document.querySelector('.mark_table') || document.querySelector('.mark_table_wrap');
const legacyContainer = document.querySelector('#ZyBottom');
const universalContainer = document.querySelector('.TiMu') ? document.body : null;
if (modernContainer) {
method = "新版考试";
result = extractModernExam(modernContainer);
} else if (legacyContainer) {
method = "旧版/作业";
result = extractLegacyQuiz(legacyContainer);
} else if (universalContainer) {
method = "章节测验(通用)";
result = extractLegacyQuiz(document.body);
} else {
if (isManual) showNotification("未找到题目,请确认页面已加载", true);
return;
}
if (result && result.trim().length > 0) {
GM_setClipboard(result);
renderUI(result);
showNotification(`已提取并复制 (${method})`);
} else {
if (isManual) showNotification("提取内容为空", true);
}
}
// UI & helpers (unchanged)
let notificationElement = null;
function showNotification(message, isError = false) {
if (!notificationElement) {
notificationElement = document.createElement('div');
notificationElement.id = 'cx-notification-gm';
document.body.appendChild(notificationElement);
}
notificationElement.innerHTML = `${isError ? '⚠️' : '✅'} ${message}`;
notificationElement.className = isError ? 'error' : 'success';
notificationElement.style.display = 'flex';
notificationElement.style.animation = 'none';
notificationElement.offsetHeight;
notificationElement.style.animation = 'cx-slide-in 0.3s forwards';
setTimeout(() => {
notificationElement.style.animation = 'cx-slide-out 0.3s forwards';
setTimeout(() => { notificationElement.style.display = 'none'; }, 300);
}, 2500);
}
let uiContainer = null;
function renderUI(content) {
if (!uiContainer) {
uiContainer = document.createElement('div');
uiContainer.id = 'cx-extractor-ui';
const header = document.createElement('div');
header.className = 'cx-header';
header.innerHTML = `