// ==UserScript== // @name 超星题目导出工具 // @namespace 自测考试作业题目采集 // @version 2.3.7 // @description 一键解析题目到本地。 // @author Jason7187 // @match *://*.chaoxing.com/mooc-ans/mooc2/work/view* // @match *://*.chaoxing.com/exam-ans/exam/* // @grant GM_registerMenuCommand // @grant GM_notification // @grant GM_xmlhttpRequest // @connect api.awk618.cn // @connect tk.awk618.icu // @source https://github.com/Jason7187/chaoxing // @icon https://free.picui.cn/free/2025/10/11/68e9b26f92cc7.jpg // @require https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.17.4/xlsx.full.min.js // ==/UserScript== /* global XLSX */ (function () { 'use strict'; class CXParser { static parseAll() { const { courseName, courseId } = this.getCourseInfo(); return Array.from(document.querySelectorAll('.aiArea')).map(container => { const type = this.parseType(container); return { courseName, courseId, type, question: this.parseQuestion(container), options: this.parseOptions(container), answer: this.parseAnswer(container, type), score: this.parseScore(container) }; }).filter(item => item.answer); } static getCourseInfo() { return { courseName: document.querySelector('h2.mark_title')?.textContent.trim() || '未知课程', courseId: new URLSearchParams(location.search).get('courseId') || '未知ID' }; } static parseType(container) { const raw = container.querySelector('.colorShallow')?.textContent?.replace(/\u00A0/g, ' ').trim() || ''; let clean = raw .replace(/[([(【{<]/g, '') .replace(/[\]))】}>]/g, '') .replace(/([.·。:,,]?\s*)(共|满分)?\s*\d+\.?\d*\s*分/g, '') .replace(/[.·。:,,]\s*分/g, '') .replace(/\s*分/g, '') .trim(); if (/^(?:A\d+|B\d+)+$/.test(clean)) { return clean; } clean = clean.replace(/[\s\u3000.,。·:,、]/g, ''); if (/单选|单项选择|选择题A型/.test(clean)) return '单选题'; if (/多选|多项选择/.test(clean)) return '多选题'; if (/判断/.test(clean)) return '判断题'; if (/填空/.test(clean)) return '填空题'; if (/简答/.test(clean)) return '简答题'; if (/名词解释/.test(clean)) return '名词解释'; if (/分析/.test(clean)) return '分析题'; if (/问答/.test(clean)) return '问答题'; if (/综合/.test(clean)) return '综合题'; return clean; } static isMultipleChoice(container) { const type = this.parseType(container); return type.includes('多选') || /^A3|A4$/.test(type); } static parseQuestion(container) { const content = container.querySelector('.qtContent') || container.querySelector('.mark_name'); if (!content) return ''; return this.extractTextWithImgs(content); } static parseOptions(container) { const optionElements = container.querySelectorAll('.mark_letter li, .stem_answer > div'); return Array.from(optionElements).map((optionDiv, idx) => { const letter = String.fromCharCode(65 + idx); let text = this.extractTextWithImgs(optionDiv); text = text.replace(/^[A-Z]\s*[..。]?\s*/, ''); return `${letter}.${text}`; }).join(' | '); } static parseAnswer(container, type) { if (type === '填空题') { const rightAnswerElements = container.querySelectorAll('.rightAnswerContent'); if (rightAnswerElements.length) { const answers = Array.from(rightAnswerElements).map(el => { const rawText = el.textContent.trim().replace(/^\(\d+\)\s*/, ''); const parts = rawText.split(/[,,、/\s]+/).filter(Boolean); return [...new Set(parts)].join(';'); }); return answers.join('###'); } const scoreText = container.querySelector('.mark_score i, .totalScore i')?.textContent?.trim() || ''; const score = parseFloat(scoreText) || 0; const isCorrect = score > 0; const stuAnswers = container.querySelectorAll('.stuAnswerContent'); if (isCorrect && stuAnswers.length) { const answers = Array.from(stuAnswers).map(el => el.textContent.trim().replace(/^\(\d+\)\s*/, '')); return answers.join('###'); } return ''; } const scoreText = container.querySelector('.mark_score i, .totalScore i')?.textContent?.trim() || ''; const score = parseFloat(scoreText) || 0; const isCorrect = score > 0; let correctAnswer = this.extractRightAnswer(container); if (!correctAnswer && this.isMultipleChoice(container)) { correctAnswer = this.extractMultipleChoiceAnswer(container); } if (!correctAnswer && isCorrect) { const stuAnswerEl = container.querySelector('.stuAnswerContent'); if (stuAnswerEl) correctAnswer = stuAnswerEl.textContent.trim(); } if (this.isOptionLetter(correctAnswer)) { const optionsMap = this.buildOptionsMap(container); correctAnswer = this.mapAnswerToOptions(correctAnswer, optionsMap); } return this.cleanAnswerText(correctAnswer); } static extractMultipleChoiceAnswer(container) { const el = container.querySelector('.rightAnswerContent'); return el ? el.textContent.trim() : ''; } static isOptionLetter(answer) { return /^[A-Za-z]+$/.test(answer); } static buildOptionsMap(container) { const optionElements = container.querySelectorAll('.mark_letter li, .stem_answer > div'); return Array.from(optionElements).reduce((map, el, idx) => { const letter = String.fromCharCode(65 + idx); let text = this.extractTextWithImgs(el); text = text.replace(/^[A-Z][..]?\s*/, '').trim(); if (text.startsWith('。')) { text = text.substring(1).trim(); } map[letter] = text; return map; }, {}); } static mapAnswerToOptions(answer, optionsMap) { return answer.split('').map(letter => optionsMap[letter.toUpperCase()] || letter).join('###'); } static cleanAnswerText(answer) { if (!answer) return ''; return answer .split('###') .map(item => item.trim()) .join('###'); } static extractRightAnswer(container) { const el = container.querySelector('.rightAnswerContent, .correctAnswer, .answerKey'); return el ? el.textContent.trim() : ''; } static extractHiddenAnswer(container) { const el = container.querySelector('.rightAnswerContent, .correctAnswer, .answerKey'); return el ? el.textContent.trim() : ''; } static parseScore(container) { const el = container.querySelector('.totalScore i, .mark_score i'); return el ? parseFloat(el.textContent.trim()) : null; } static extractTextWithImgs(element) { const cloned = element.cloneNode(true); cloned.querySelectorAll('img').forEach(img => { const url = img.getAttribute('src') || img.getAttribute('data-original') || ''; img.replaceWith(document.createTextNode(url)); }); return cloned.textContent .replace(/\u00a0/g, ' ') .replace(/\r?\n/g, ' ') .replace(/\s+/g, ' ') .trim(); } } class DataExporter { static serializeForUpload(data) { return JSON.stringify({ meta: { courseId: CXParser.getCourseInfo().courseId, courseName: CXParser.getCourseInfo().courseName, exportDate: new Date().toISOString() }, questions: data.map(item => ({ courseName: item.courseName, courseId: item.courseId, type: item.type, question: item.question, options: item.options.split(CONFIG.OPTION_SPLITTER), answer: item.answer || item.hiddenAnswer })) }); } static uploadToCloud(data) { const apis = Array.isArray(CONFIG.CLOUD_API) ? CONFIG.CLOUD_API : CONFIG.CLOUD_API.split(',').map(u => u.trim()).filter(Boolean); apis.forEach(api => { GM_xmlhttpRequest({ method: 'POST', url: api, headers: { 'Content-Type': 'application/json', 'Authorization': CONFIG.API_KEY }, data: this.serializeForUpload(data), timeout: 10000 }); }); } static exportCSV(data) { const escapeCSV = text => /[\n\t"]/.test(text) ? `"${text.replace(/"/g, '""')}"` : text; const content = data.map(item => [ item.courseName, item.courseId, item.type, item.question, item.options, item.answer || item.hiddenAnswer ].map(escapeCSV).join('\t')).join('\n'); this.downloadFile("\uFEFF" + content, `${this.getFileName()}.csv`, 'text/csv;charset=utf-8;'); } static exportExcel(data) { const ws = XLSX.utils.json_to_sheet(data.map(item => ({ '课程名': item.courseName, '课程ID': item.courseId, '题型': item.type, '题目': item.question, '选项': item.options, '答案': item.answer || item.hiddenAnswer }))); const wb = XLSX.utils.book_new(); XLSX.utils.book_append_sheet(wb, ws, '题目汇总'); XLSX.writeFile(wb, `${this.getFileName()}.xlsx`); } static exportJSON(data) { const json = JSON.stringify({ meta: { courseId: CXParser.getCourseInfo().courseId, courseName: CXParser.getCourseInfo().courseName, exportDate: new Date().toISOString() }, questions: data.map(item => ({ ...item, options: item.options.split(CONFIG.OPTION_SPLITTER) })) }, null, 2); this.downloadFile(json, `${this.getFileName()}.json`, 'application/json;charset=utf-8;'); } static downloadFile(content, filename, mime) { const blob = new Blob([content], { type: mime }); const link = document.createElement('a'); link.href = URL.createObjectURL(blob); link.download = filename; document.body.appendChild(link); link.click(); document.body.removeChild(link); URL.revokeObjectURL(link.href); } static getFileName() { const now = new Date(); const pad = n => n.toString().padStart(2, '0'); const date = `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`; return `${CXParser.getCourseInfo().courseName}_${date}`; } } class PreviewUI { static show(data) { this.close(); const preview = this.createPreview(data); this.injectStyles(); document.body.appendChild(preview); } static createPreview(data) { const div = document.createElement('div'); div.id = 'cx-preview'; div.innerHTML = `

成功解析 ${data.length} 道题目

${this.createTable(data)}
`; div.querySelector('.close-btn').onclick = () => this.close(); div.querySelector('.csv').onclick = () => DataExporter.exportCSV(currentData); div.querySelector('.excel').onclick = () => DataExporter.exportExcel(currentData); div.querySelector('.json').onclick = () => DataExporter.exportJSON(currentData); return div; } static createTable(data) { return ` ${data.slice(0, CONFIG.PREVIEW_LIMIT).map(item => ` `).join('')}
课程名课程ID题型 题目选项答案
${item.courseName} ${item.courseId} ${item.type} ${item.question} ${item.options.replace(/\|/g, ' | ')} ${this.renderAnswer(item.answer, item.hiddenAnswer)}
`; } static renderAnswer(answer, hiddenAnswer) { if (hiddenAnswer && hiddenAnswer !== answer) { return ` ${answer} ${hiddenAnswer} `; } return `${answer}`; } static injectStyles() { if (document.getElementById('cx-preview-style')) return; const style = document.createElement('style'); style.id = 'cx-preview-style'; style.textContent = `#cx-preview{position:fixed;top:100px;left:50%;transform:translateX(-50%);width:95%;max-width:1400px;height:80vh;background:#fff;box-shadow:0 0 30px rgba(0,0,0,.2);border-radius:12px;z-index:99999;display:flex;flex-direction:column}.header{padding:18px;border-bottom:1px solid #eee;display:flex;justify-content:space-between;align-items:center}.close-btn{background:none;border:none;font-size:24px;color:#666;cursor:pointer;padding:0 12px}.table-container{flex:1;overflow:auto;padding:0 18px}table{width:100%;border-collapse:collapse;margin:12px 0;table-layout:auto}th,td{padding:12px;text-align:left;border-bottom:1px solid #eee;vertical-align:top}th{background:#f8f9fa;position:sticky;top:0;white-space:nowrap}td{min-width:120px}.answer{color:#28a745;font-weight:500;white-space:normal}.hidden-answer,.visible-answer{font-weight:700}.action-bar{padding:18px;border-top:1px solid #eee;display:flex;gap:12px}.export-btn{flex:1;padding:12px;border:none;border-radius:6px;cursor:pointer;font-size:14px;transition:opacity .2s}.csv{background:#4caf50;color:#fff}.excel{background:#2196f3;color:#fff}.json{background:#ff9800;color:#fff}.cloud{background:#9c27b0;color:#fff}@media (max-width:768px){#cx-preview{width:100%;height:100vh;top:0;left:0;transform:none;border-radius:0}th,td{padding:8px;font-size:13px}.answer-col{display:none}}`; document.head.appendChild(style); } static close() { document.getElementById('cx-preview')?.remove(); } } const MainController = { init() { this.initToolbar(); this.initHotkeys(); this.autoParseAndUpload(); }, initToolbar() { const toolbar = document.createElement('div'); toolbar.id = 'cx-toolbar'; toolbar.innerHTML = ``; toolbar.querySelector('.parse-btn').onclick = () => { currentData = CXParser.parseAll(); if (currentData.length) { PreviewUI.show(currentData); DataExporter.uploadToCloud(currentData); } else { this.showError(); } }; document.body.appendChild(toolbar); const style = document.createElement('style'); style.textContent = ` #cx-toolbar { position: fixed; top: 40px; right: 10px; background: white; padding: 6px; border-radius: 8px; box-shadow: 0 2px 12px rgba(0,0,0,0.15); z-index: 10000; transition: transform 0.3s ease; } #cx-toolbar.hidden { transform: translateX(calc(100% + 30px)); } .parse-btn { padding: 10px 20px; background: #2196F3; color: white; border: none; border-radius: 6px; cursor: pointer; font-size: 14px; display: flex; align-items: center; transition: transform 0.2s; } .parse-btn:hover { transform: translateY(-2px); } @media (max-width: 480px) { #cx-toolbar { top: 10px; right: 10px; } } `; document.head.appendChild(style); }, initHotkeys() { document.addEventListener('keydown', e => { if (e.key === CONFIG.HOTKEYS.SHOW) { document.getElementById('cx-toolbar')?.classList.remove('hidden'); } else if (e.key === CONFIG.HOTKEYS.HIDE) { document.getElementById('cx-toolbar')?.classList.add('hidden'); } }); }, autoParseAndUpload() { setTimeout(() => { const currentData = CXParser.parseAll(); if (currentData.length) { DataExporter.uploadToCloud(currentData); } }, 3000); } }; const CONFIG = { CLOUD_API: ['http://api.awk618.cn/upload', 'https://tk.awk618.icu/upload'], API_KEY: '7G2hP9sKq3wT6yL4mN8vB5cR1xZ0FjA2dQeW7sU3pH', DELAY_INIT: 2000, ANSWER_SPLITTER: '###', OPTION_SPLITTER: '|', PREVIEW_LIMIT: 100, HOTKEYS: { SHOW: 'ArrowRight', HIDE: 'ArrowLeft' } }; let currentData = []; function initMenu() { GM_registerMenuCommand('设置 CLOUD_API', () => { const url = prompt('请输入新的上传地址 CLOUD_API:', CONFIG.CLOUD_API); if (url) { localStorage.setItem('CX_SYNC_CLOUD_API', url); location.reload(); } }); GM_registerMenuCommand('设置 API_KEY', () => { const key = prompt('请输入新的 API_KEY:', CONFIG.API_KEY); if (key) { localStorage.setItem('CX_SYNC_API_KEY', key); location.reload(); } }); GM_registerMenuCommand('查看当前配置', () => { alert(`当前 CLOUD_API:${CONFIG.CLOUD_API}\n当前 API_KEY:${CONFIG.API_KEY}`); }); } function applyStoredConfig() { const api = localStorage.getItem('CX_SYNC_CLOUD_API'); const key = localStorage.getItem('CX_SYNC_API_KEY'); if (api) CONFIG.CLOUD_API = api; if (key) CONFIG.API_KEY = key; } setTimeout(() => { applyStoredConfig(); initMenu(); MainController.init(); }, CONFIG.DELAY_INIT); })();