// ==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 = `
📝 提取结果
×
`; const body = document.createElement('div'); body.className = 'cx-body'; const textarea = document.createElement('textarea'); textarea.id = 'cx-extractor-output'; textarea.readOnly = true; textarea.value = content; body.appendChild(textarea); const footer = document.createElement('div'); footer.className = 'cx-footer'; const copyBtn = document.createElement('button'); copyBtn.innerHTML = '✨ 再次复制'; copyBtn.className = 'cx-btn cx-btn-primary'; copyBtn.onclick = () => { const ta = document.getElementById('cx-extractor-output'); ta.select(); GM_setClipboard(ta.value); showNotification("已复制到剪贴板"); }; const closeBtn = document.createElement('button'); closeBtn.textContent = '关闭'; closeBtn.className = 'cx-btn cx-btn-secondary'; closeBtn.onclick = () => { uiContainer.style.display = 'none'; }; footer.appendChild(copyBtn); footer.appendChild(closeBtn); uiContainer.appendChild(header); uiContainer.appendChild(body); uiContainer.appendChild(footer); header.querySelector('.cx-close-icon').onclick = () => { uiContainer.style.display = 'none'; }; document.body.appendChild(uiContainer); } else { const ta = document.getElementById('cx-extractor-output'); if(ta) ta.value = content; } uiContainer.style.display = 'flex'; } function createFloatingButton() { if(document.getElementById('cx-float-btn')) return; const btn = document.createElement('button'); btn.id = 'cx-float-btn'; btn.innerHTML = `提取
题目`; btn.title = '点击提取题目'; btn.onclick = function() { extractAndDisplay(true); }; document.body.appendChild(btn); } function toggleAuto() { const val = !GM_getValue(AUTO_EXTRACT_KEY, false); GM_setValue(AUTO_EXTRACT_KEY, val); alert(`自动提取已${val ? '开启' : '关闭'},刷新页面生效。`); updateMenu(); } function updateMenu() { const isAuto = GM_getValue(AUTO_EXTRACT_KEY, false); try { GM_registerMenuCommand(`${isAuto ? '✅' : '❌'} 自动提取开关`, toggleAuto); GM_registerMenuCommand(`🖐 手动提取题目`, () => extractAndDisplay(true)); } catch (e) { console.warn("GM_registerMenuCommand 可能不可用:", e); } } updateMenu(); createFloatingButton(); // --- CSS 样式 --- GM_addStyle(` /* 浮窗按钮 - 改进为正方形文字版 */ #cx-float-btn { position: fixed; top: 180px; right: 15px; z-index: 99999; width: 50px; height: 50px; border-radius: 8px; /* 轻微圆角 */ background: #1890ff; color: #fff; border: none; box-shadow: 0 4px 12px rgba(24, 144, 255, 0.4); font-size: 13px; line-height: 1.2; cursor: pointer; transition: all 0.2s; display: flex; align-items: center; justify-content: center; text-align: center; font-family: -apple-system, sans-serif; font-weight: 500; } #cx-float-btn:hover { background: #40a9ff; transform: scale(1.05); } #cx-float-btn:active { background: #096dd9; transform: scale(0.95); } /* 通知条 */ #cx-notification-gm { position: fixed; top: 25px; left: 50%; transform: translateX(-50%); padding: 10px 20px; border-radius: 50px; display: flex; align-items: center; gap: 8px; box-shadow: 0 8px 20px rgba(0,0,0,0.12); z-index: 100000; font-family: sans-serif; font-size: 14px; font-weight: 500; pointer-events: none; backdrop-filter: blur(10px); } #cx-notification-gm.success { background: rgba(246, 255, 237, 0.95); color: #389e0d; border: 1px solid #b7eb8f; } #cx-notification-gm.error { background: rgba(255, 241, 240, 0.95); color: #cf1322; border: 1px solid #ffa39e; } @keyframes cx-slide-in { from { opacity: 0; transform: translate(-50%, -20px); } to { opacity: 1; transform: translate(-50%, 0); } } @keyframes cx-slide-out { from { opacity: 1; transform: translate(-50%, 0); } to { opacity: 0; transform: translate(-50%, -20px); } } /* 主面板容器 */ #cx-extractor-ui { position: fixed; right: 20px; bottom: 30px; width: 380px; max-height: 80vh; background: #fff; border-radius: 12px; box-shadow: 0 12px 48px rgba(0,0,0,0.12), 0 2px 8px rgba(0,0,0,0.06); z-index: 99998; display: flex; flex-direction: column; font-family: sans-serif; border: 1px solid rgba(0,0,0,0.05); animation: cx-pop-up 0.3s cubic-bezier(0.34, 1.56, 0.64, 1); } @keyframes cx-pop-up { from { opacity: 0; transform: scale(0.9) translateY(20px); } to { opacity: 1; transform: scale(1) translateY(0); } } .cx-header { padding: 14px 20px; border-bottom: 1px solid #f0f0f0; display: flex; justify-content: space-between; align-items: center; background: #fafafa; border-radius: 12px 12px 0 0; } .cx-title { font-weight: 600; font-size: 15px; color: #1f1f1f; } .cx-close-icon { font-size: 20px; color: #999; cursor: pointer; width: 24px; height: 24px; display: flex; align-items: center; justify-content: center; border-radius: 4px; } .cx-close-icon:hover { background: #e6e6e6; color: #333; } .cx-body { padding: 0; flex: 1; display: flex; flex-direction: column; } #cx-extractor-output { width: 100%; height: 260px; padding: 15px 20px; border: none; background: #fff; font-family: "Menlo", "Monaco", "Consolas", monospace; font-size: 12px; line-height: 1.6; color: #333; resize: none; outline: none; box-sizing: border-box; } #cx-extractor-output::-webkit-scrollbar { width: 6px; } #cx-extractor-output::-webkit-scrollbar-thumb { background: #d9d9d9; border-radius: 3px; } .cx-footer { padding: 12px 20px; border-top: 1px solid #f0f0f0; display: flex; gap: 12px; background: #fff; border-radius: 0 0 12px 12px; } .cx-btn { flex: 1; border: none; border-radius: 6px; padding: 9px 0; font-size: 14px; font-weight: 500; cursor: pointer; transition: all 0.2s; } .cx-btn-primary { background: #1890ff; color: #fff; } .cx-btn-primary:hover { background: #40a9ff; } .cx-btn-secondary { background: #f5f5f5; color: #595959; border: 1px solid #d9d9d9; } .cx-btn-secondary:hover { background: #fff; border-color: #40a9ff; color: #40a9ff; } `); window.addEventListener('load', () => { if (GM_getValue(AUTO_EXTRACT_KEY, false)) { setTimeout(() => extractAndDisplay(false), 2000); } }); })();