// ==UserScript== // @name 超星学习通作业/考试一键导出为Word(docx) // @namespace http://tampermonkey.net/ // @version 2.6 // @description 自动抓取学习通作业/考试题目、选项、正确答案、解析,并生成Word文档。完美修复题号重复问题,支持图片,支持自定义标题。 // @author Restrl. // @match *://*.chaoxing.com/mooc2/work/view* // @match *://*.chaoxing.com/mooc-ans/mooc2/work/view* // @match *://*.chaoxing.com/exam/test/reVersionTestStartNew* // @match *://*.chaoxing.com/exam-ans/* // @require https://cdn.jsdelivr.net/npm/docx@7.1.0/build/index.js // @require https://cdn.jsdelivr.net/npm/file-saver@2.0.5/dist/FileSaver.min.js // @grant GM_xmlhttpRequest // ==/UserScript== (function() { 'use strict'; const { Document, Packer, Paragraph, TextRun, ImageRun, HeadingLevel, AlignmentType } = docx; function addExportButton() { if (document.querySelector('#exportBtn')) return; const btn = document.createElement('button'); btn.id = 'exportBtn'; btn.innerText = '📤 导出为 Word'; btn.style.cssText = ` position: fixed; top: 20px; right: 20px; z-index: 9999; padding: 10px 20px; background-color: #00a1d6; color: white; border: none; border-radius: 5px; cursor: pointer; box-shadow: 0 4px 6px rgba(0,0,0,0.1); font-weight: bold; font-size: 14px; `; btn.onclick = startExport; document.body.appendChild(btn); } async function urlToBlob(url) { return new Promise((resolve) => { if (url.startsWith('data:image')) { fetch(url).then(res => res.blob()).then(resolve).catch(() => resolve(null)); return; } GM_xmlhttpRequest({ method: "GET", url: url, responseType: "blob", onload: (res) => resolve(res.response), onerror: () => resolve(null) }); }); } async function parseRichText(element) { const runs = []; if (!element) return runs; const nodes = element.childNodes; for (const node of nodes) { if (node.nodeType === Node.TEXT_NODE) { const text = node.textContent.trim(); // 过滤掉可能存在的单独的题号文本(如 "1. ") // 但这里不能太激进,防止误杀题干里的数字,主要靠外部逻辑控制 if (text) runs.push(new TextRun({ text: text })); } else if (node.nodeType === Node.ELEMENT_NODE) { if (node.tagName === 'IMG') { const src = node.src; if (src) { const imgBlob = await urlToBlob(src); if (imgBlob) { const imgBuffer = await imgBlob.arrayBuffer(); runs.push(new TextRun({ text: "\n" })); runs.push(new ImageRun({ data: imgBuffer, transformation: { width: 400, height: 300 }, })); runs.push(new TextRun({ text: "\n" })); } } } else if (node.tagName === 'BR') { runs.push(new TextRun({ text: "\n" })); } else if (['P', 'DIV', 'SPAN', 'H3', 'STRONG', 'B', 'SUB', 'SUP'].includes(node.tagName)) { const childRuns = await parseRichText(node); runs.push(...childRuns); if (node.tagName === 'P') runs.push(new TextRun({ text: "\n" })); } } } return runs; } function getQuestionNodes() { let q = document.querySelectorAll('.aiArea'); if (q.length > 0) return q; q = document.querySelectorAll('.questionLi'); if (q.length > 0) return q; return document.querySelectorAll('.TiMu'); } function getAssignmentTitle() { const titleNode = document.querySelector('h2.mark_title'); if (titleNode) { return titleNode.innerText.trim(); } let fallback = document.title || "作业导出"; return fallback.split(/[|\-_]/)[0].trim(); } async function startExport() { const btn = document.querySelector('#exportBtn'); btn.innerText = '⏳ 正在处理...'; btn.disabled = true; let docTitle = getAssignmentTitle(); const safeFilename = docTitle.replace(/[\\/:*?"<>|]/g, '_'); const docChildren = []; docChildren.push(new Paragraph({ text: docTitle, heading: HeadingLevel.HEADING_1, alignment: AlignmentType.CENTER })); docChildren.push(new Paragraph({ text: "" })); const questions = getQuestionNodes(); console.log(`精准捕获到 ${questions.length} 道题目`); for (let i = 0; i < questions.length; i++) { const qBox = questions[i]; const titleNode = qBox.querySelector('h3.mark_name'); if (!titleNode) continue; const fullTitleText = titleNode.innerText; const isFillIn = fullTitleText.includes("填空"); // --- 核心修复区:完美去重题号 --- let richTextRuns = []; // 策略1:精准提取(针对你的页面结构) // 只抓取 "题目类型" 和 "题目内容",无视 h3 下那个游荡的文本节点 "1. " const typeNode = titleNode.querySelector('.colorShallow'); // (单选题) const contentNode = titleNode.querySelector('.qtContent'); // 题目正文 if (contentNode) { // 如果能找到正文容器,就只解析它和类型 if (typeNode) { richTextRuns.push(...await parseRichText(typeNode)); richTextRuns.push(new TextRun({ text: " " })); // 加个空格美观 } richTextRuns.push(...await parseRichText(contentNode)); } else { // 策略2:兜底方案(针对老旧页面) // 如果找不到 contentNode,说明结构比较老,只能暴力清洗 const cloneTitle = titleNode.cloneNode(true); // 移除可能的序号 span const indexSpan = cloneTitle.querySelector('span:first-child'); if(indexSpan && /^\d+/.test(indexSpan.innerText)) indexSpan.remove(); richTextRuns = await parseRichText(cloneTitle); // 暴力正则:移除开头的 "数字+点/顿号/空格" if(richTextRuns.length > 0 && richTextRuns[0].text) { richTextRuns[0].text = richTextRuns[0].text.replace(/^[\s\d.、.]+/, "").trim(); } } // 写入文档:使用我们生成的 i+1 作为序号 docChildren.push(new Paragraph({ children: [ new TextRun({ text: `${i + 1}. `, bold: true, size: 24 }), ...richTextRuns ] })); // 选项处理 if (!isFillIn) { const optionUl = qBox.querySelector('ul.mark_letter'); if (optionUl) { const options = optionUl.querySelectorAll('li'); for (const opt of options) { const optRuns = await parseRichText(opt); docChildren.push(new Paragraph({ children: optRuns, indent: { left: 720 } })); } } } // 答案处理 let ansText = ""; const ansDiv = qBox.querySelector('.mark_answer'); if (ansDiv) { const fillDiv = ansDiv.querySelector('.mark_fill'); if (fillDiv) { ansText = fillDiv.innerText.replace(/正确答案[::]?/g, "").trim(); } if (!ansText) { const greenSpan = ansDiv.querySelector('.mark_key .colorGreen'); if (greenSpan) { ansText = greenSpan.innerText.replace(/正确答案[::]?/g, "").trim(); } else { const keyDiv = ansDiv.querySelector('.mark_key'); if (keyDiv) { const raw = keyDiv.innerText.trim(); if (raw.includes("正确答案")) { ansText = raw.split("正确答案")[1].trim(); } else if (!raw.includes("我的答案")) { ansText = raw; } } } } } if (ansText) { ansText = ansText.replace(/^[::\s]+/, ""); docChildren.push(new Paragraph({ children: [ new TextRun({ text: "【正确答案】:", bold: true, color: "FF0000" }), new TextRun({ text: ansText, bold: true, color: "FF0000" }) ] })); } // 解析处理 const analysisDiv = qBox.querySelector('.analysisDiv, .mark_answer_analysis'); if (analysisDiv) { const analysisContent = analysisDiv.querySelector('.qtAnalysis, .text'); if (analysisContent) { const analysisRuns = await parseRichText(analysisContent); docChildren.push(new Paragraph({ children: [ new TextRun({ text: "【解析】:", bold: true, italics: true, color: "0000FF" }), ...analysisRuns ] })); } } docChildren.push(new Paragraph({ text: "--------------------------------------------------", color: "CCCCCC" })); docChildren.push(new Paragraph({ text: "" })); } try { const doc = new Document({ sections: [{ children: docChildren }] }); const blob = await Packer.toBlob(doc); saveAs(blob, `${safeFilename}.docx`); btn.innerText = '✅ 导出成功'; setTimeout(() => { btn.innerText = '📤 导出为 Word'; btn.disabled = false; }, 3000); } catch (e) { console.error(e); alert("导出出错,详细错误请看F12控制台"); btn.innerText = '❌ 出错'; } } window.addEventListener('load', addExportButton); })();