// ==UserScript== // @name 磨题帮爬题 // @namespace https://tampermonkey.net/ // @version 1.12 // @description 自动抓取磨题帮题库,支持单选、多选、判断、填空,提取题目导出TXT/Excel // @match https://www.motibang.com/* // @grant none // @run-at document-end // ==/UserScript== (async function () { 'use strict'; const MAX_FAIL = 5, PANEL_WIDTH = 280, DELAY_ANSWER = 800, DELAY_NEXT = 1000, DELAY_RETRY = 400; const NEXT_RETRY = 5, NEXT_INTERVAL = 300, ANIM_DURATION = 300, TIMEOUT = 12000, BTN_HEIGHT = '38px'; const sleep = ms => new Promise(r => setTimeout(r, ms)); const cleanFileName = str => (str || '题库').replace(/[\/\\:*?"<>|]/g, '_'); function isClickable(el) { if (!el) return false; if (el.disabled) return false; if (el.offsetParent === null) return false; const s = window.getComputedStyle(el); if (s.visibility === 'hidden' || s.opacity === '0' || s.display === 'none') return false; return true; } async function withTimeout(p, ms, fb = null) { let t; const tp = new Promise((_, r) => { t = setTimeout(() => r(new Error('Timeout')), ms); }); try { const r = await Promise.race([p, tp]); clearTimeout(t); return r; } catch (e) { clearTimeout(t); return fb; } } let isRunning = false, output = '', records = [], count = 0, totalQuestions = 0, failCount = 0; let examTitle = '', isDark = false, currentStatus = '', isMinimized = false, exportExpanded = false; const seenQuestions = new Set(); let panelBodyHeight = 0; function getTheme() { return { panelBg: isDark ? 'rgba(30,30,30,0.96)' : '#fff', panelShadow: isDark ? '0 4px 22px rgba(0,0,0,0.65)' : '0 4px 22px rgba(0,0,0,0.17)', headerBg: isDark ? 'rgba(50,50,55,0.98)' : '#f0f2f5', headerBorder: isDark ? '#555' : '#dee2e6', textColor: isDark ? '#e8e8e8' : '#1a1a2e', subTextColor: isDark ? '#aaa' : '#6c757d', btnColor: isDark ? '#ddd' : '#495057' }; } const panel = document.createElement('div'); panel.style.cssText = `position:fixed;top:80px;right:28px;z-index:2147483647;border-radius:14px;width:${PANEL_WIDTH}px;font-size:14px;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;cursor:default;user-select:none;overflow:visible;touch-action:none;transition:box-shadow ${ANIM_DURATION}ms ease,background ${ANIM_DURATION}ms ease;`; panel.innerHTML = `
📝磨题帮爬题
进度:0/0
就绪
`; document.body.appendChild(panel); const dragHandle = document.getElementById('dragHandle'), panelTitleWrap = document.getElementById('panelTitleWrap'), statusText = document.getElementById('statusText'); const minBtn = document.getElementById('minBtn'), darkBtn = document.getElementById('darkBtn'), panelBody = document.getElementById('panelBody'); const progressRow = document.getElementById('progressRow'), statusEl = document.getElementById('status'), startStopBtn = document.getElementById('startStopBtn'); const exportBtn = document.getElementById('exportBtn'), exportSub = document.getElementById('exportSub'), exportTxtBtn = document.getElementById('exportTxtBtn'), exportCsvBtn = document.getElementById('exportCsvBtn'); function applyTheme() { const t = getTheme(); panel.style.background = t.panelBg; panel.style.boxShadow = t.panelShadow; dragHandle.style.background = t.headerBg; dragHandle.style.borderBottomColor = t.headerBorder; panelTitleWrap.style.color = t.textColor; darkBtn.style.color = t.btnColor; minBtn.style.color = t.btnColor; progressRow.style.color = t.textColor; statusEl.style.color = t.subTextColor; } function cachePanelHeight() { if (!isMinimized) { const pmh = panelBody.style.maxHeight, po = panelBody.style.overflow; panelBody.style.maxHeight = 'none'; panelBody.style.overflow = 'visible'; panelBodyHeight = panelBody.scrollHeight; panelBody.style.maxHeight = pmh; panelBody.style.overflow = po; } } function expandExport() { if (exportExpanded) { collapseExport(); return; } exportExpanded = true; exportBtn.style.opacity = '0'; exportBtn.style.pointerEvents = 'none'; exportSub.style.transform = 'translateY(0) scale(1)'; exportSub.style.opacity = '1'; exportSub.style.pointerEvents = 'auto'; } function collapseExport() { exportExpanded = false; exportSub.style.transform = 'translateY(-10px) scale(0.88)'; exportSub.style.opacity = '0'; exportSub.style.pointerEvents = 'none'; exportBtn.style.opacity = '1'; exportBtn.style.pointerEvents = 'auto'; } exportBtn.onclick = (e) => { e.stopPropagation(); expandExport(); }; exportTxtBtn.onclick = (e) => { e.stopPropagation(); collapseExport(); saveTxt(); }; exportCsvBtn.onclick = (e) => { e.stopPropagation(); collapseExport(); saveExcel(); }; dragHandle.addEventListener('mousedown', collapseExport); dragHandle.addEventListener('touchstart', collapseExport, { passive: false }); let isDragging = false, startX, startY, startLeft, startTop; function getPos(e) { return e.touches?.length > 0 ? { x: e.touches[0].clientX, y: e.touches[0].clientY } : { x: e.clientX, y: e.clientY }; } function dragStart(e) { if (e.target.closest('button')) return; e.preventDefault(); isDragging = true; const p = getPos(e); startX = p.x; startY = p.y; const r = panel.getBoundingClientRect(); startLeft = r.left; startTop = r.top; panel.style.transition = 'none'; } function dragMove(e) { if (!isDragging) return; e.preventDefault(); const p = getPos(e); let l = startLeft + p.x - startX, t = startTop + p.y - startY; l = Math.max(0, Math.min(l, window.innerWidth - PANEL_WIDTH)); t = Math.max(0, Math.min(t, window.innerHeight - 60)); panel.style.left = l + 'px'; panel.style.top = t + 'px'; panel.style.right = 'auto'; } function dragEnd() { if (isDragging) { isDragging = false; requestAnimationFrame(() => { panel.style.transition = `box-shadow ${ANIM_DURATION}ms ease,background ${ANIM_DURATION}ms ease`; }); } } dragHandle.addEventListener('mousedown', dragStart); document.addEventListener('mousemove', dragMove); document.addEventListener('mouseup', dragEnd); dragHandle.addEventListener('touchstart', dragStart, { passive: false }); document.addEventListener('touchmove', dragMove, { passive: false }); document.addEventListener('touchend', dragEnd); document.addEventListener('touchcancel', dragEnd); darkBtn.onclick = (e) => { e.stopPropagation(); collapseExport(); isDark = !isDark; darkBtn.textContent = isDark ? '●' : '○'; applyTheme(); }; async function toggleMinimize() { collapseExport(); isMinimized = !isMinimized; if (isMinimized) { cachePanelHeight(); panelBody.style.maxHeight = panelBodyHeight + 'px'; panelBody.style.opacity = '1'; void panelBody.offsetHeight; panelBody.style.maxHeight = '0px'; panelBody.style.opacity = '0'; panelBody.style.padding = '0 14px'; panelBody.style.pointerEvents = 'none'; dragHandle.style.borderRadius = '14px'; dragHandle.style.borderBottom = 'none'; minBtn.textContent = '+'; } else { if (panelBodyHeight === 0) cachePanelHeight(); panelBody.style.maxHeight = '0px'; panelBody.style.opacity = '0'; panelBody.style.padding = '0 14px'; void panelBody.offsetHeight; panelBody.style.maxHeight = panelBodyHeight + 'px'; panelBody.style.opacity = '1'; panelBody.style.padding = '10px 14px 14px'; panelBody.style.pointerEvents = 'auto'; dragHandle.style.borderRadius = '14px 14px 0 0'; dragHandle.style.borderBottom = '1px solid'; minBtn.textContent = '−'; } updateTitle(); applyTheme(); } minBtn.onclick = (e) => { e.stopPropagation(); toggleMinimize(); }; function updateTitle() { if (isMinimized && currentStatus) { statusText.style.display = 'inline-block'; statusText.textContent = currentStatus; } else { statusText.style.display = 'none'; statusText.textContent = ''; } } function setProgress() { const el = document.getElementById('progress'); if (el) el.innerText = `${count}/${totalQuestions || '?'}`; updateTitle(); } function setStatus(s) { currentStatus = s; if (statusEl) statusEl.innerText = s; updateTitle(); } function saveTxt() { if (!output) { setStatus('⚠️ 暂无数据'); return; } const b = new Blob(['\uFEFF' + output], { type: "text/plain;charset=utf-8" }); const u = URL.createObjectURL(b); const a = document.createElement("a"); a.href = u; a.download = `${cleanFileName(examTitle)}_${count}题.txt`; a.click(); URL.revokeObjectURL(u); setStatus('✅ TXT导出完成'); } function typeToText(t) { const m = { single: '单选题', multiple: '多选题', judge: '判断题', fillblank: '填空题' }; return m[t] || '单选题'; } function escapeCSV(s) { if (!s) return ''; const x = s.replace(/[\r\n]+/g, ' ').trim(); if (x.includes(',') || x.includes('"')) return `"${x.replace(/"/g, '""')}"`; return x; } function saveExcel() { if (records.length === 0) { setStatus('⚠️ 暂无数据'); return; } const allKeys = new Set(); records.forEach(r => { if (r.type === 'single' || r.type === 'multiple') Object.keys(r.options).forEach(k => allKeys.add(k)); }); const sortedKeys = Array.from(allKeys).sort(); const headers = ['题型', '题干', ...sortedKeys, '答案']; let csv = '\uFEFF' + headers.join(',') + '\n'; records.forEach(r => { const row = [typeToText(r.type), escapeCSV(r.question)]; sortedKeys.forEach(k => { row.push(escapeCSV((r.type === 'single' || r.type === 'multiple') ? (r.options[k] || '') : '')); }); row.push(escapeCSV(r.answer)); csv += row.join(',') + '\n'; }); const b = new Blob([csv], { type: "text/csv;charset=utf-8" }); const u = URL.createObjectURL(b); const a = document.createElement("a"); a.href = u; a.download = `${cleanFileName(examTitle)}_${count}题.csv`; a.click(); URL.revokeObjectURL(u); setStatus('✅ Excel导出完成'); } async function resetAllRecords() { try { const moreBtn = Array.from(document.querySelectorAll('button')).find(b => b.textContent.trim() === '更多' && isClickable(b)); if (!moreBtn) { setStatus('⚠️ 未找到更多按钮'); return false; } moreBtn.click(); await sleep(600); const clearBtn = Array.from(document.querySelectorAll('.clear-btn')).find(b => b.textContent.trim() === '清空所有答题记录' && b.offsetParent !== null); if (!clearBtn) { moreBtn.click(); setStatus('⚠️ 未找到清空按钮'); return false; } clearBtn.click(); let confirmBtn = null; const st = Date.now(); while (Date.now() - st < 3000) { confirmBtn = Array.from(document.querySelectorAll('button')).find(b => b.textContent.trim() === '是' && b.classList.contains('el-button--primary') && b.offsetParent !== null); if (confirmBtn) break; await sleep(150); } if (!confirmBtn) { setStatus('⚠️ 未找到确认按钮'); return false; } confirmBtn.click(); setStatus('🔄 已清空记录'); await sleep(1500); const firstQ = Array.from(document.querySelectorAll('.question-col.pointer')).find(el => el.textContent.trim() === '1' && isClickable(el)); if (firstQ) { firstQ.click(); setStatus('✅ 已跳回第1题'); await sleep(1000); } return true; } catch (e) { console.error(e); return false; } } function clickShowAnswer() { const b = Array.from(document.querySelectorAll('button')).find(b => ['看答案','查看答案','显示答案'].includes(b.textContent.trim()) && isClickable(b)); if (b) { b.click(); return true; } return false; } async function waitAnswerReady(to = 3000) { const st = Date.now(); while (Date.now() - st < to) { if (document.querySelector('.button.correct.choice-abc')) return true; const fa = document.querySelector('.fillblank-answer'); if (fa && fa.textContent.trim()) return true; if (/正确答案[::是]\s*[对错正确错误是否√×TFYNtfyn]/.test(document.body.innerText)) return true; await sleep(200); } return false; } async function waitNextBtn() { for (let i = 0; i < NEXT_RETRY; i++) { const b = Array.from(document.querySelectorAll('button')).find(b => b.textContent.trim() === '下一页' && isClickable(b) && !b.classList.contains('is-disabled')); if (b) { b.click(); return true; } await sleep(NEXT_INTERVAL); } return false; } function detectType() { if (document.querySelector('.fillblank-answer') || document.querySelector('.fillblank-input')) return 'fillblank'; const t = document.body.innerText; if (/正确答案[::是]\s*[对错]/.test(t) || /[对错]\s*$/.test(t)) return 'judge'; const c = document.querySelectorAll('.button.correct.choice-abc'); if (c.length > 1) return 'multiple'; if (c.length === 1) return 'single'; if (/单选题/.test(t)) return 'single'; if (/多选题/.test(t)) return 'multiple'; if (/判断题/.test(t)) return 'judge'; return 'single'; } function getQuestion() { const q = document.querySelector('.question-content'); if (!q) return ''; const d = q.querySelector('.desc-container .content div') || q.querySelector('.desc-container .content'); if (d) return d.textContent.trim(); const t = q.textContent || ''; const m = t.match(/\d+\/\d+\s*(.+?)(?:A|$)/); if (m) return m[1].trim(); return t.split(/[A-F]\./)[0].replace(/^\d+\/\d+\s*/, '').trim(); } function getAnswer(type) { if (type === 'fillblank') { const fa = document.querySelector('.fillblank-answer'); return fa ? fa.textContent.trim() : ''; } const c = document.querySelectorAll('.button.correct.choice-abc'); if (c.length > 0) return Array.from(c).map(e => e.textContent.trim()).join(''); const t = document.body.innerText; if (type === 'judge') { const m = t.match(/正确答案[::是]\s*(对|错)/); if (m) return m[1]; } const m = t.match(/正确答案[::是]\s*([A-F]+)/i); return m ? m[1].toUpperCase() : ''; } function getOptions() { const o = {}; document.querySelectorAll('.choice-option').forEach(el => { const b = el.querySelector('.button.choice-abc'), d = el.querySelector('.desc-container .content div') || el.querySelector('.desc-container .content'); if (b && d) o[b.textContent.trim()] = d.textContent.trim(); }); return o; } function getExamTitle() { const w = document.querySelector('[class*="question"]'); if (!w) return ''; const t = w.textContent || ''; const m = t.match(/^(.+?)更多/); if (m) return m[1].trim(); return t.split('更多')[0].trim(); } function getTotal() { const m = document.body.innerText.match(/(\d+)\/(\d+)/); if (m) totalQuestions = parseInt(m[2]); } async function waitQuestionReady() { for (let i = 0; i < 20; i++) { const q = document.querySelector('.question-content'); if (q && q.textContent.trim()) return true; await sleep(300); } return false; } async function grabOne() { try { clickShowAnswer(); if (!await waitAnswerReady()) { setStatus('⚠️ 答案未加载'); return null; } const type = detectType(); const question = getQuestion(); const answer = getAnswer(type); const options = (type === 'fillblank' || type === 'judge') ? {} : getOptions(); if (!question) { setStatus('⚠️ 未识别题目'); return null; } if (!answer) { setStatus('⚠️ 无答案'); return null; } if (seenQuestions.has(question)) { setStatus('⏭️ 跳过重复题'); return null; } seenQuestions.add(question); return { type, question, answer, options }; } catch (e) { console.error(e); setStatus('⚠️ 抓取异常'); return null; } } function resetBtnOnly() { isRunning = false; startStopBtn.textContent = '开始抓取'; startStopBtn.style.background = '#2563eb'; startStopBtn.style.color = '#fff'; } startStopBtn.onclick = async function () { collapseExport(); if (isRunning) { isRunning = false; startStopBtn.textContent = '继续抓取'; startStopBtn.style.background = '#f59e0b'; startStopBtn.style.color = '#fff'; setStatus('已暂停'); return; } if (count === 0) { setStatus('🔄 清空答题记录...'); await resetAllRecords(); setStatus('⏳ 等待页面加载...'); if (!await waitQuestionReady()) { setStatus('❌ 页面未加载完成'); return; } output = ''; records = []; failCount = 0; seenQuestions.clear(); examTitle = getExamTitle(); if (examTitle) output += examTitle + '\n\n'; getTotal(); } else { failCount = 0; } isRunning = true; startStopBtn.textContent = '暂停'; startStopBtn.style.background = '#dc2626'; startStopBtn.style.color = '#fff'; setStatus('🚀 抓取中'); setProgress(); while (isRunning) { let data = null; try { data = await withTimeout(grabOne(), TIMEOUT); } catch (e) { data = null; } if (!data || !data.answer) { failCount++; setStatus(`⚠️ 抓取失败 ${failCount}/${MAX_FAIL}`); if (failCount >= MAX_FAIL) { setStatus('❌ 连续抓取失败,自动停止'); if (output) saveTxt(); resetBtnOnly(); return; } await sleep(DELAY_RETRY); if (!await waitNextBtn()) { setStatus('✅ 抓取完成'); resetBtnOnly(); return; } await sleep(DELAY_NEXT); continue; } failCount = 0; count++; records.push(data); if (data.type === 'fillblank') output += `${count}. ${data.question}\n答案:${data.answer}\n\n`; else if (data.type === 'judge') output += `${count}. ${data.question}\n答案:${data.answer}\n\n`; else { const keys = Object.keys(data.options).sort(); output += `${count}. ${data.question}\n${keys.map(k => `${k}. ${data.options[k]}`).join('\n')}\n答案:${data.answer}\n\n`; } setProgress(); await sleep(DELAY_RETRY); if (!await waitNextBtn()) { setStatus('✅ 抓取完成'); resetBtnOnly(); return; } await sleep(DELAY_NEXT); } }; setTimeout(() => cachePanelHeight(), 100); applyTheme(); setStatus('就绪'); })();