// ==UserScript== // @name 超星学习通作业题目提取工具(支持图片) // @namespace http://tampermonkey.net/ // @version 1.3 // @description 提取超星学习通作业页面的题目、选项和答案,支持图片提取和Word下载 // @author Assistant // @match *://*.chaoxing.com/mooc-ans/mooc2/work/view* // @grant none // @require https://unpkg.com/docx@7.8.2/build/index.js // @require https://unpkg.com/file-saver@2.0.5/dist/FileSaver.min.js // ==/UserScript== (function() { 'use strict'; // 等待页面加载完成 function waitForElement(selector, timeout = 10000) { return new Promise((resolve, reject) => { const startTime = Date.now(); function check() { const element = document.querySelector(selector); if (element) { resolve(element); } else if (Date.now() - startTime > timeout) { reject(new Error(`元素 ${selector} 未找到`)); } else { setTimeout(check, 100); } } check(); }); } // 图片信息结构 class ImageInfo { constructor(src, alt = '', width = '', height = '') { this.src = src; this.alt = alt; this.width = width; this.height = height; } } // 题目数据结构 class Question { constructor() { this.type = ''; // 题目类型 this.number = ''; // 题目编号 this.content = ''; // 题目内容 this.contentImages = []; // 题目中的图片 this.options = []; // 选项列表 this.optionImages = []; // 选项中的图片 {optionIndex: number, images: ImageInfo[]} this.myAnswer = ''; // 我的答案 this.correctAnswer = ''; // 正确答案 this.score = ''; // 得分 this.isCorrect = false; // 是否正确 this.analysis = ''; // 答案解析 this.analysisImages = []; // 解析中的图片 } } // 题目解析器 class QuestionParser { constructor() { this.questions = []; } // 解析所有题目 parseAllQuestions() { this.questions = []; // 获取所有题目容器 const questionContainers = document.querySelectorAll('.questionLi'); questionContainers.forEach((container, index) => { try { const question = this.parseQuestion(container, index + 1); if (question) { this.questions.push(question); } } catch (error) { console.error(`解析第${index + 1}题时出错:`, error); } }); return this.questions; } // 提取元素中的图片 extractImages(element) { const images = []; if (!element) return images; const imgElements = element.querySelectorAll('img'); imgElements.forEach(img => { const src = img.getAttribute('src') || img.getAttribute('data-original'); const alt = img.getAttribute('alt') || ''; const width = img.getAttribute('width') || ''; const height = img.getAttribute('height') || ''; if (src) { images.push(new ImageInfo(src, alt, width, height)); } }); return images; } // 获取纯文本内容(去除图片标签) getTextContent(element) { if (!element) return ''; // 克隆元素以避免修改原DOM const cloned = element.cloneNode(true); // 将图片替换为占位符 const images = cloned.querySelectorAll('img'); images.forEach((img, index) => { const placeholder = document.createElement('span'); placeholder.textContent = `[图片${index + 1}]`; img.parentNode.replaceChild(placeholder, img); }); return this.cleanText(cloned.textContent); } // 解析单个题目 parseQuestion(container, index) { const question = new Question(); // 解析题目编号和类型 const titleElement = container.querySelector('.mark_name'); if (titleElement) { const titleText = titleElement.textContent.trim(); const typeMatch = titleText.match(/\((.*?题)\)/); const numberMatch = titleText.match(/^(\d+)\./); question.type = typeMatch ? typeMatch[1] : '未知题型'; question.number = numberMatch ? numberMatch[1] : index.toString(); } // 解析题目内容和图片 const contentElement = container.querySelector('.qtContent'); if (contentElement) { question.content = this.getTextContent(contentElement); question.contentImages = this.extractImages(contentElement); } // 解析选项和选项图片 const optionElements = container.querySelectorAll('.mark_letter li, .qtDetail li'); optionElements.forEach((option, optionIndex) => { const optionText = this.getTextContent(option); if (optionText) { question.options.push(optionText); // 提取选项中的图片 const optionImages = this.extractImages(option); if (optionImages.length > 0) { question.optionImages.push({ optionIndex: optionIndex, images: optionImages }); } } }); // 解析我的答案 const myAnswerElements = container.querySelectorAll('.stuAnswerContent'); const myAnswers = []; myAnswerElements.forEach(elem => { const answerText = this.cleanText(elem.textContent); if (answerText) { myAnswers.push(answerText); } }); question.myAnswer = myAnswers.join('; '); // 解析正确答案 const correctAnswerElements = container.querySelectorAll('.rightAnswerContent'); const correctAnswers = []; correctAnswerElements.forEach(elem => { const answerText = this.cleanText(elem.textContent); if (answerText) { correctAnswers.push(answerText); } }); question.correctAnswer = correctAnswers.join('; '); // 解析答案解析 const analysisElement = container.querySelector('.qtAnalysis'); if (analysisElement) { question.analysis = this.getTextContent(analysisElement); question.analysisImages = this.extractImages(analysisElement); } // 解析得分 const scoreElement = container.querySelector('.totalScore i'); if (scoreElement) { question.score = scoreElement.textContent.trim(); } // 判断是否正确 const correctIcon = container.querySelector('.marking_dui'); question.isCorrect = !!correctIcon; return question; } // 清理文本内容 cleanText(text) { if (!text) return ''; return text.replace(/\s+/g, ' ').trim(); } // 获取作业标题 getWorkTitle() { const titleElement = document.querySelector('.mark_title'); return titleElement ? this.cleanText(titleElement.textContent) : '作业题目'; } // 获取统计信息 getStatistics() { const stats = { totalQuestions: this.questions.length, correctCount: this.questions.filter(q => q.isCorrect).length, totalScore: '0', maxScore: '0', totalImages: 0 }; // 统计图片数量 this.questions.forEach(q => { stats.totalImages += q.contentImages.length; q.optionImages.forEach(optImg => { stats.totalImages += optImg.images.length; }); stats.totalImages += q.analysisImages.length; }); // 获取总分 const scoreElement = document.querySelector('.resultNum i'); if (scoreElement) { stats.totalScore = scoreElement.textContent.trim(); } // 获取满分 const maxScoreElement = document.querySelector('.infoHead span:nth-child(2)'); if (maxScoreElement) { const match = maxScoreElement.textContent.match(/满分:\s*(\d+)/); if (match) { stats.maxScore = match[1]; } } stats.accuracy = stats.totalQuestions > 0 ? ((stats.correctCount / stats.totalQuestions) * 100).toFixed(1) + '%' : '0%'; return stats; } } // 检查必要的库是否加载 function checkLibraries() { const errors = []; if (typeof window.docx === 'undefined') { errors.push('docx库未加载'); } if (typeof window.saveAs === 'undefined' && typeof saveAs === 'undefined') { errors.push('FileSaver库未加载'); } return errors; } // Word文档生成器 class WordGenerator { constructor() { this.docx = window.docx; this.saveAs = window.saveAs || saveAs; this.imageCache = new Map(); // 图片缓存 } // 下载图片并转换为blob async downloadImage(url) { if (this.imageCache.has(url)) { return this.imageCache.get(url); } try { const response = await fetch(url); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const blob = await response.blob(); this.imageCache.set(url, blob); return blob; } catch (error) { console.warn(`无法下载图片 ${url}:`, error); return null; } } // 获取图片尺寸 getImageDimensions(originalWidth, originalHeight, maxWidth = 400) { let width = parseInt(originalWidth) || 300; let height = parseInt(originalHeight) || 200; // 限制最大宽度 if (width > maxWidth) { height = (height * maxWidth) / width; width = maxWidth; } return { width, height }; } // 生成Word文档 async generateWord(questions, title, stats, options = {}) { const { includeMyAnswer = true, includeCorrectAnswer = true, includeScore = true, includeStatistics = true, includeAnalysis = true, includeSeparator = true, embedImages = false } = options; const doc = new this.docx.Document({ sections: [{ properties: {}, children: [ // 标题 new this.docx.Paragraph({ children: [ new this.docx.TextRun({ text: title, bold: true, size: 32 }) ], alignment: this.docx.AlignmentType.CENTER, spacing: { after: 400 } }), // 统计信息 ...(includeStatistics ? this.createStatisticsSection(stats) : []), // 题目列表 ...await this.createQuestionsSection(questions, includeMyAnswer, includeCorrectAnswer, includeScore, includeAnalysis, includeSeparator, embedImages) ] }] }); return doc; } // 创建统计信息部分 createStatisticsSection(stats) { return [ new this.docx.Paragraph({ children: [ new this.docx.TextRun({ text: "答题统计", bold: true, size: 24 }) ], spacing: { before: 200, after: 200 } }), new this.docx.Paragraph({ children: [ new this.docx.TextRun({ text: `总题数: ${stats.totalQuestions}题 | 正确: ${stats.correctCount}题 | 准确率: ${stats.accuracy} | 得分: ${stats.totalScore}/${stats.maxScore}分 | 图片数: ${stats.totalImages}张` }) ], spacing: { after: 400 } }) ]; } // 创建图片信息段落 async createImagesParagraphs(images, prefix = "图片", embedImages = false) { const paragraphs = []; if (images.length > 0) { for (let index = 0; index < images.length; index++) { const img = images[index]; if (embedImages) { try { const imageBlob = await this.downloadImage(img.src); if (imageBlob) { const dimensions = this.getImageDimensions(img.width, img.height); paragraphs.push( new this.docx.Paragraph({ children: [ new this.docx.TextRun({ text: `${prefix}${index + 1}:`, bold: true, color: "666666" }) ], spacing: { before: 200, after: 100 } }), new this.docx.Paragraph({ children: [ new this.docx.ImageRun({ data: imageBlob, transformation: { width: dimensions.width, height: dimensions.height, }, }) ], alignment: this.docx.AlignmentType.CENTER, spacing: { after: 200 } }) ); } else { // 如果图片下载失败,显示链接 paragraphs.push( new this.docx.Paragraph({ children: [ new this.docx.TextRun({ text: `${prefix}${index + 1}: `, bold: true, color: "666666" }), new this.docx.TextRun({ text: `${img.src} (下载失败)`, color: "CC0000" }) ], indent: { left: 400 }, spacing: { after: 100 } }) ); } } catch (error) { console.error(`处理图片时出错:`, error); // 失败时显示链接 paragraphs.push( new this.docx.Paragraph({ children: [ new this.docx.TextRun({ text: `${prefix}${index + 1}: `, bold: true, color: "666666" }), new this.docx.TextRun({ text: `${img.src} (处理失败)`, color: "CC0000" }) ], indent: { left: 400 }, spacing: { after: 100 } }) ); } } else { // 显示链接 paragraphs.push( new this.docx.Paragraph({ children: [ new this.docx.TextRun({ text: `${prefix}${index + 1}: `, bold: true, color: "666666" }), new this.docx.TextRun({ text: img.src, color: "0066CC" }), ...(img.width || img.height ? [ new this.docx.TextRun({ text: ` (${img.width}x${img.height})`, color: "999999" }) ] : []) ], indent: { left: 400 }, spacing: { after: 100 } }) ); } } } return paragraphs; } // 创建题目部分 async createQuestionsSection(questions, includeMyAnswer, includeCorrectAnswer, includeScore, includeAnalysis, includeSeparator, embedImages = false) { const elements = []; for (let index = 0; index < questions.length; index++) { const question = questions[index]; // 题目标题 elements.push( new this.docx.Paragraph({ children: [ new this.docx.TextRun({ text: `${question.number}. [${question.type}] `, bold: true }), new this.docx.TextRun({ text: question.content }) ], spacing: { before: 300, after: 200 } }) ); // 题目中的图片 if (question.contentImages.length > 0) { const imageParagraphs = await this.createImagesParagraphs(question.contentImages, "题目图片", embedImages); elements.push(...imageParagraphs); } // 选项 if (question.options.length > 0) { for (let optionIndex = 0; optionIndex < question.options.length; optionIndex++) { const option = question.options[optionIndex]; elements.push( new this.docx.Paragraph({ children: [ new this.docx.TextRun({ text: option }) ], indent: { left: 400 }, spacing: { after: 100 } }) ); // 选项中的图片 const optionImageGroup = question.optionImages.find(oi => oi.optionIndex === optionIndex); if (optionImageGroup && optionImageGroup.images.length > 0) { const optionImageParagraphs = await this.createImagesParagraphs( optionImageGroup.images, `选项${String.fromCharCode(65 + optionIndex)}图片`, embedImages ); elements.push(...optionImageParagraphs); } } } // 我的答案 if (includeMyAnswer && question.myAnswer) { elements.push( new this.docx.Paragraph({ children: [ new this.docx.TextRun({ text: "我的答案: ", bold: true, color: "0066CC" }), new this.docx.TextRun({ text: question.myAnswer, color: "0066CC" }) ], spacing: { after: 100 } }) ); } // 正确答案 if (includeCorrectAnswer && question.correctAnswer) { elements.push( new this.docx.Paragraph({ children: [ new this.docx.TextRun({ text: "正确答案: ", bold: true, color: "009900" }), new this.docx.TextRun({ text: question.correctAnswer, color: "009900" }) ], spacing: { after: 100 } }) ); } // 答案解析 if (includeAnalysis && question.analysis) { elements.push( new this.docx.Paragraph({ children: [ new this.docx.TextRun({ text: "答案解析: ", bold: true, color: "FF6600" }), new this.docx.TextRun({ text: question.analysis, color: "FF6600" }) ], spacing: { after: 100 } }) ); // 解析中的图片 if (question.analysisImages.length > 0) { const analysisImageParagraphs = await this.createImagesParagraphs(question.analysisImages, "解析图片", embedImages); elements.push(...analysisImageParagraphs); } } // 得分和正确性 if (includeScore && question.score) { elements.push( new this.docx.Paragraph({ children: [ new this.docx.TextRun({ text: `得分: ${question.score}分 `, bold: true }), new this.docx.TextRun({ text: question.isCorrect ? "✓ 正确" : "✗ 错误", color: question.isCorrect ? "009900" : "CC0000", bold: true }) ], spacing: { after: 200 } }) ); } // 分隔线 if (includeSeparator && index < questions.length - 1) { elements.push( new this.docx.Paragraph({ children: [ new this.docx.TextRun({ text: "─".repeat(50), color: "CCCCCC" }) ], alignment: this.docx.AlignmentType.CENTER, spacing: { before: 200, after: 200 } }) ); } } return elements; } async downloadWord(doc, filename) { try { const blob = await this.docx.Packer.toBlob(doc); // 尝试多种下载方式 if (this.saveAs) { this.saveAs(blob, `${filename}.docx`); } else if (typeof saveAs !== 'undefined') { saveAs(blob, `${filename}.docx`); } else { // 备用下载方法 const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `${filename}.docx`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); } return true; } catch (error) { console.error('下载Word文档时出错:', error); return false; } } } // UI界面 class ExtractorUI { constructor() { this.parser = new QuestionParser(); this.wordGenerator = new WordGenerator(); this.isVisible = false; this.questions = []; } // 创建UI界面 createUI() { const container = document.createElement('div'); container.id = 'chaoxing-extractor'; container.innerHTML = `