您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Greasy Fork is available in English.
从组卷网提取试卷内容并导出为Word,支持图片处理
// ==UserScript== // @name 组卷网试卷提取 // @namespace http://tampermonkey.net/ // @version 0.1 // @description 从组卷网提取试卷内容并导出为Word,支持图片处理 // @icon https://toolb.cn/favicon/zujuan.xkw.com // @author pansoul // @license MIT // @match https://zujuan.xkw.com/zujuan/* // @grant GM_addStyle // @grant GM_setClipboard // @grant GM_download // @grant GM_xmlhttpRequest // @connect staticzujuan.xkw.com // @connect cdn*.xkw.com // ==/UserScript== (function() { 'use strict'; GM_addStyle(` .xkw-exporter { position: fixed; top: 100px; right: 20px; z-index: 9999; background: #4285f4; color: #fff; padding: 10px 18px 10px 42px; /* 预留左侧 icon 空间 */ border-radius: 6px; box-shadow: 0 4px 10px rgba(0,0,0,0.25); font-family: Arial, sans-serif; font-size: 14px; cursor: pointer; transition: background 0.25s; } .xkw-exporter::before { content: ''; position: absolute; left: 12px; top: 50%; width: 18px; height: 18px; transform: translateY(-50%); background: url('https://toolb.cn/favicon/zujuan.xkw.com') no-repeat center/contain; } .xkw-exporter:hover { background: #3367d6; } .xkw-exporter-panel { position: fixed; top: 150px; right: 20px; z-index: 9998; background: #ffffff; border-radius: 8px; box-shadow: 0 8px 20px rgba(0,0,0,0.15); padding: 20px 22px 18px; width: 330px; display: none; font-family: 'Microsoft YaHei', Arial, sans-serif; } .xkw-exporter-panel h3 { margin: 0 0 12px 0; font-size: 18px; border-bottom: 1px solid #e0e0e0; padding-bottom: 10px; } .xkw-exporter-panel button { display: block; width: 100%; margin: 10px 0; padding: 10px 0; background: #4285f4; color: #fff; border: none; border-radius: 4px; font-size: 15px; cursor: pointer; transition: background 0.25s; } .xkw-exporter-panel button:hover { background: #3367d6; } .xkw-exporter-panel .close { position: absolute; top: 10px; right: 10px; cursor: pointer; font-size: 18px; } .xkw-exporter-panel .options { margin: 10px 0; } .xkw-exporter-panel .options label { display: block; margin: 5px 0; } .xkw-exporter-progress { position: fixed; top: 200px; right: 20px; z-index: 9997; background: rgba(0,0,0,0.7); color: white; padding: 10px; border-radius: 4px; display: none; } `); window.addEventListener('load', function() { setTimeout(initExporter, 1000); }); function initExporter() { const exporterBtn = document.createElement('div'); exporterBtn.className = 'xkw-exporter'; exporterBtn.textContent = '导出试卷'; exporterBtn.addEventListener('click', toggleExporterPanel); document.body.appendChild(exporterBtn); const exporterPanel = document.createElement('div'); exporterPanel.className = 'xkw-exporter-panel'; exporterPanel.innerHTML = ` <span class="close">×</span> <h3>试卷导出工具</h3> <div class="options"> <label><input type="checkbox" id="download-images" checked> 下载图片(Word格式)</label> <label><input type="checkbox" id="format-equations" checked> 格式化公式</label> </div> <button id="export-word">导出为Word</button> `; document.body.appendChild(exporterPanel); const progressDiv = document.createElement('div'); progressDiv.className = 'xkw-exporter-progress'; progressDiv.innerHTML = '处理中...'; document.body.appendChild(progressDiv); exporterPanel.querySelector('.close').addEventListener('click', function() { exporterPanel.style.display = 'none'; }); document.getElementById('export-word').addEventListener('click', function() { exportPaper('word'); }); } function toggleExporterPanel() { const panel = document.querySelector('.xkw-exporter-panel'); panel.style.display = panel.style.display === 'none' || panel.style.display === '' ? 'block' : 'none'; } function showProgress(message) { const progress = document.querySelector('.xkw-exporter-progress'); progress.textContent = message; progress.style.display = 'block'; } function hideProgress() { const progress = document.querySelector('.xkw-exporter-progress'); progress.style.display = 'none'; } function extractPaperContent() { showProgress('正在提取试卷内容...'); const downloadImages = document.getElementById('download-images').checked; const formatEquations = document.getElementById('format-equations').checked; const paperTitle = document.querySelector('.paper-title .main-title')?.textContent.trim() || '未命名试卷'; const paperContent = { title: paperTitle, sections: [], options: { includeAnswers: false, includeExplanations: false, downloadImages, formatEquations } }; let questionGlobalIndex = 1; const questionTypes = document.querySelectorAll('.ques-type'); questionTypes.forEach((typeSection, typeIndex) => { const currentSection = { type: '', index: '', questions: [] }; const questions = typeSection.querySelectorAll('.ques-item'); questions.forEach((question, qIndex) => { let qContent = question.querySelector('.exam-item__cnt')?.innerHTML.trim() || ''; if (!qContent) { return; } const qNumber = `${questionGlobalIndex}.`; questionGlobalIndex++; { const temp = document.createElement('div'); temp.innerHTML = qContent; const idx = temp.querySelector('.quesindex'); if (idx) idx.parentNode.removeChild(idx); qContent = temp.innerHTML; } qContent = qContent.replace(/^\s*(?:<[^>]+>\s*)*\d+\s*[..、]?\s*/, ''); const options = []; const optionsTable = question.querySelector('table[name="optionsTable"]'); if (optionsTable) { const optionRows = optionsTable.querySelectorAll('tr'); optionRows.forEach(row => { const cells = row.querySelectorAll('td'); cells.forEach(cell => { options.push(cell.innerHTML); }); }); const tempDiv = document.createElement('div'); tempDiv.innerHTML = qContent; const contentOptionsTable = tempDiv.querySelector('table[name="optionsTable"]'); if (contentOptionsTable) { contentOptionsTable.parentNode.removeChild(contentOptionsTable); } qContent = tempDiv.innerHTML; } const images = []; if (downloadImages) { const imgElements = question.querySelectorAll('img'); imgElements.forEach(img => { if (img.src && !img.src.startsWith('data:')) { images.push({ src: img.src, alt: img.alt || '', width: img.width || 0, height: img.height || 0 }); } }); } // 不再获取答案和解析 const answer = ''; const explanation = ''; currentSection.questions.push({ number: qNumber, content: qContent, options: options, answer: answer, explanation: explanation, images: images }); }); if (currentSection.questions.length > 0) { const typeName = typeSection.querySelector('.questypename')?.textContent.trim() || `题型${typeIndex + 1}`; const rawTypeIndex = typeSection.querySelector('.questypeindex b')?.textContent.trim() || `${typeIndex + 1}、`; const typeIndex2 = rawTypeIndex.replace(/^\d+[、\.]\s*\d+[、\.]/, match => { const firstNum = match.match(/^\d+/)[0]; return `${firstNum}、`; }); currentSection.type = typeName; currentSection.index = typeIndex2; paperContent.sections.push(currentSection); } }); hideProgress(); return paperContent; } function paperToHTML(paperContent) { let html = ` <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>${paperContent.title}</title> <style> @page { size: A4; margin: 2cm; } body { font-family: "SimSun", "Microsoft YaHei", Arial, sans-serif; line-height: 1.6; font-size: 12pt; width: 21cm; margin: 0 auto; } .paper-title { text-align: center; font-size: 18pt; font-weight: bold; margin: 20px 0 30px 0; } .section-title { font-weight: bold; margin: 20px 0 15px 0; font-size: 14pt; } .question { margin-bottom: 20px; page-break-inside: avoid; } .question-content { margin-bottom: 8px; } .options { margin-left: 2em; } .option { margin-bottom: 5px; } .answer { color: #d81e06; margin-top: 8px; font-weight: bold; } .explanation { color: #1e88e5; margin-top: 8px; border-left: 3px solid #1e88e5; padding-left: 10px; } img { max-width: 90%; display: block; margin: 10px auto; } table { border-collapse: collapse; width: 100%; } table, th, td { border: 1px solid #ddd; } th, td { padding: 8px; text-align: left; } @media print { .page-break { page-break-before: always; } body { font-size: 12pt; } .no-print { display: none; } } </style> </head> <body> <div class="paper-title">${paperContent.title}</div> `; const nonEmptySections = paperContent.sections.filter(section => section.questions && section.questions.length > 0); nonEmptySections.forEach((section, sectionIndex) => { html += `<div class="section-title">${section.index} ${section.type}</div>`; section.questions.forEach((question, qIndex) => { html += `<div class="question">`; html += `<div class="question-content">${question.number} ${cleanHTML(question.content)}</div>`; if (question.options.length > 0) { html += `<div class="options">`; const optionRows = []; for (let i = 0; i < question.options.length; i += 2) { const row = [question.options[i]]; if (i + 1 < question.options.length) { row.push(question.options[i + 1]); } optionRows.push(row); } html += `<table border="0" cellpadding="5" cellspacing="0" style="border:none;">`; optionRows.forEach(row => { html += `<tr>`; row.forEach(option => { html += `<td style="border:none;">${cleanHTML(option)}</td>`; }); if (row.length === 1) { html += `<td style="border:none;"></td>`; } html += `</tr>`; }); html += `</table>`; html += `</div>`; } // 不再显示答案和解析 html += `</div>`; if ((qIndex + 1) % 10 === 0 && qIndex < section.questions.length - 1) { html += `<div class="page-break"></div>`; } }); if (sectionIndex < nonEmptySections.length - 1) { html += `<div class="page-break"></div>`; } }); html += ` <script> function fixMathFormulas() { // 替换常见的数学符号 const mathSymbols = { '\\\\frac{': '(', // 分数开始 '}': ')', // 括号结束 '\\\\sqrt{': '√(', // 平方根 '\\\\le': '≤', // 小于等于 '\\\\ge': '≥', // 大于等于 '\\\\neq': '≠', // 不等于 '\\\\alpha': 'α', // 希腊字母 '\\\\beta': 'β', '\\\\gamma': 'γ', '\\\\delta': 'δ', '\\\\pi': 'π', '\\\\infty': '∞', // 无穷 '\\\\times': '×', // 乘号 '\\\\div': '÷', // 除号 '\\\\pm': '±', // 正负号 '\\\\cdot': '·', // 点乘 '\\\\ldots': '...', // 省略号 '\\\\equiv': '≡', // 恒等于 '\\\\cong': '≅', // 全等于 '\\\\approx': '≈', // 约等于 '\\\\triangle': '△', // 三角形 '\\\\angle': '∠', // 角 '\\\\perp': '⊥', // 垂直 '\\\\parallel': '∥', // 平行 '\\\\sim': '∼', // 相似 '\\\\partial': '∂', // 偏导数 '\\\\int': '∫', // 积分 '\\\\sum': '∑', // 求和 '\\\\prod': '∏', // 求积 '\\\\lim': 'lim', // 极限 '\\\\rightarrow': '→', // 箭头 '\\\\leftarrow': '←', '\\\\Rightarrow': '⇒', '\\\\Leftarrow': '⇐', '\\\\Leftrightarrow': '⇔', '\\\\leftrightarrow': '↔', '\\\\subset': '⊂', // 集合 '\\\\supset': '⊃', '\\\\subseteq': '⊆', '\\\\supseteq': '⊇', '\\\\cup': '∪', '\\\\cap': '∩', '\\\\in': '∈', '\\\\notin': '∉', '\\\\emptyset': '∅', '\\\\mathbb{R}': 'ℝ', // 特殊集合 '\\\\mathbb{Z}': 'ℤ', '\\\\mathbb{N}': 'ℕ', '\\\\mathbb{Q}': 'ℚ', '\\\\mathbb{C}': 'ℂ' }; // 替换数学符号 const elements = document.querySelectorAll('.question-content, .option, .answer, .explanation'); elements.forEach(el => { let html = el.innerHTML; for (const [symbol, replacement] of Object.entries(mathSymbols)) { const regex = new RegExp(symbol.replace(/\\/g, '\\\\'), 'g'); html = html.replace(regex, replacement); } el.innerHTML = html; }); // 处理上下标 const superscriptRegex = /<sup>(.*?)<\/sup>/g; const subscriptRegex = /<sub>(.*?)<\/sub>/g; elements.forEach(el => { let html = el.innerHTML; html = html.replace(superscriptRegex, '^($1)'); html = html.replace(subscriptRegex, '_($1)'); el.innerHTML = html; }); // 移除知识点链接和提示 const knowledgeLinks = document.querySelectorAll('.knowledge-point a, a[class*="knowledge"], [class*="知识点"]'); knowledgeLinks.forEach(link => { if (link.parentNode) { link.parentNode.removeChild(link); } }); // 移除知识点提示 const knowledgePoints = document.querySelectorAll('.knowledge-point, [class*="knowledge"], [class*="知识点"]'); knowledgePoints.forEach(point => { if (point.parentNode) { point.parentNode.removeChild(point); } }); // 移除题型编号中的重复 const sectionTitles = document.querySelectorAll('.section-title'); sectionTitles.forEach(title => { title.textContent = title.textContent.replace(/^(\d+)[、\.]\s*\d+[、\.]/, '$1、'); }); // 移除题目编号中的重复 const questionContents = document.querySelectorAll('.question-content'); questionContents.forEach(content => { const firstPart = content.innerHTML.split(' ')[0]; if (firstPart && firstPart.match(/^\d+\./)) { content.innerHTML = content.innerHTML.replace(/^(\d+)\.\s*\d+\./, '$1.'); } }); } // 页面加载完成后执行 window.addEventListener('load', fixMathFormulas); </script> </body> </html> `; return html; } function paperToText(paperContent) { let text = `${paperContent.title}\n\n`; const nonEmptySections = paperContent.sections.filter(section => section.questions && section.questions.length > 0); nonEmptySections.forEach(section => { let sectionTitle = section.index + ' ' + section.type; sectionTitle = sectionTitle.replace(/^(\d+)[、\.]\s*\d+[、\.]/, '$1、'); text += `${sectionTitle}\n\n`; section.questions.forEach(question => { let questionNumber = question.number; questionNumber = questionNumber.replace(/^(\d+)[..、]\s*\d+[..、]/, '$1.'); text += `${questionNumber} ${stripHTML(question.content)}\n`; if (question.options.length > 0) { question.options.forEach((option, index) => { text += ` ${stripHTML(option)}\n`; }); } // 不再显示答案和解析 text += `\n`; }); text += `\n`; }); return text; } async function processImages(paperContent) { if (!paperContent.options.downloadImages) return paperContent; showProgress('正在处理图片...'); const imagePromises = []; const imageMap = new Map(); paperContent.sections.forEach(section => { section.questions.forEach(question => { question.images.forEach(img => { if (!imageMap.has(img.src)) { imageMap.set(img.src, null); const promise = fetchImageAsBase64(img.src) .then(base64 => { imageMap.set(img.src, base64); }) .catch(err => { console.error(`Failed to fetch image: ${img.src}`, err); }); imagePromises.push(promise); } }); }); }); await Promise.all(imagePromises); paperContent.sections.forEach(section => { section.questions.forEach(question => { const imgRegex = /<img[^>]+src="([^"]+)"[^>]*>/g; question.content = question.content.replace(imgRegex, (match, src) => { const base64 = imageMap.get(src); if (base64) { return match.replace(src, base64); } return match; }); question.options = question.options.map(option => { return option.replace(imgRegex, (match, src) => { const base64 = imageMap.get(src); if (base64) { return match.replace(src, base64); } return match; }); }); // 不再处理答案和解析中的图片 }); }); hideProgress(); return paperContent; } function fetchImageAsBase64(url) { return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: 'GET', url: url, responseType: 'arraybuffer', onload: function(response) { try { let binary = ''; const bytes = new Uint8Array(response.response); const len = bytes.byteLength; for (let i = 0; i < len; i++) { binary += String.fromCharCode(bytes[i]); } let mimeType = 'image/jpeg'; if (url.endsWith('.png')) { mimeType = 'image/png'; } else if (url.endsWith('.gif')) { mimeType = 'image/gif'; } else if (url.endsWith('.svg')) { mimeType = 'image/svg+xml'; } const base64 = 'data:' + mimeType + ';base64,' + btoa(binary); resolve(base64); } catch (e) { reject(e); } }, onerror: function(error) { reject(error); } }); }); } async function exportPaper(format) { let paperContent = extractPaperContent(); if (format === 'word' && paperContent.options.downloadImages) { paperContent = await processImages(paperContent); } const fileName = `${paperContent.title}_${new Date().toISOString().slice(0, 10)}`; showProgress(`正在导出为${format.toUpperCase()}格式...`); switch (format) { case 'word': const html = paperToHTML(paperContent); const blob = new Blob([html], {type: 'text/html'}); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `${fileName}.doc`; document.body.appendChild(a); a.click(); document.body.removeChild(a); setTimeout(() => URL.revokeObjectURL(url), 100); break; case 'txt': const text = paperToText(paperContent); const txtBlob = new Blob([text], {type: 'text/plain'}); const txtUrl = URL.createObjectURL(txtBlob); const txtLink = document.createElement('a'); txtLink.href = txtUrl; txtLink.download = `${fileName}.txt`; document.body.appendChild(txtLink); txtLink.click(); document.body.removeChild(txtLink); setTimeout(() => URL.revokeObjectURL(txtUrl), 100); break; case 'json': const json = JSON.stringify(paperContent, null, 2); const jsonBlob = new Blob([json], {type: 'application/json'}); const jsonUrl = URL.createObjectURL(jsonBlob); const jsonLink = document.createElement('a'); jsonLink.href = jsonUrl; jsonLink.download = `${fileName}.json`; document.body.appendChild(jsonLink); jsonLink.click(); document.body.removeChild(jsonLink); setTimeout(() => URL.revokeObjectURL(jsonUrl), 100); break; } hideProgress(); alert(`已成功导出为${format.toUpperCase()}格式!`); } function cleanHTML(html) { if (!html) return ''; return html.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, ''); } function stripHTML(html) { if (!html) return ''; const temp = document.createElement('div'); temp.innerHTML = html; return temp.textContent || temp.innerText || ''; } function formatEquations(html) { if (!html) return ''; return html; } })();