您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
使用可配置的选择器来适配不同网站,支持复杂的输入格式。根据网站自动选择合适的选择器。
当前为
// ==UserScript== // @name 【自制】问卷星输入答案自动填写 // @namespace http://tampermonkey.net/ // @version 1.1.0 // @description 使用可配置的选择器来适配不同网站,支持复杂的输入格式。根据网站自动选择合适的选择器。 // @match https://lms.ouchn.cn/exam/* // @match https://ks.wjx.top/vm/mBcE5Ax.aspx // @match https://www.wjx.cn/vm/eU7tjdY.aspx // @match https://www.szxuexiao.com/zuoti/html/33.html // @grant GM_addStyle // @grant GM_setValue // @grant GM_getValue // ==/UserScript== (function () { "use strict"; // 全局变量和常量 let questions = []; let isQuestionDetected = false; const GLOBAL = { fillAnswerDelay: 300, debounceDelay: 300, }; const DEFAULT_SELECTORS = { "lms.ouchn.cn": { subjectContainer: ".exam-subjects > ol > li.subject", questionText: ".subject-description", options: '.subject-options input[type="radio"], .subject-options input[type="checkbox"]', answerElement: ".answer-options", }, }; let SELECTORS = JSON.parse( GM_getValue("domainSelectors", JSON.stringify(DEFAULT_SELECTORS)) ); // 工具函数 function debounce(func, wait) { let timeout; return function executedFunction(...args) { const later = () => { clearTimeout(timeout); func(...args); }; clearTimeout(timeout); timeout = setTimeout(later, wait); }; } function sleep(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } function getCurrentDomain() { return window.location.hostname; } function getSelectorsForCurrentDomain() { const currentDomain = getCurrentDomain(); return SELECTORS[currentDomain] || null; } // UI 相关函数 function createMainInterface() { const container = document.createElement("div"); container.id = "auto-fill-container"; container.className = "fixed top-5 right-5 bg-white p-6 rounded-lg shadow-xl w-96 max-w-[90%] transition-all duration-300 ease-in-out"; container.innerHTML = ` <h3 class="text-2xl font-bold mb-4 text-gray-800">自动填写答案</h3> <div id="status-message" class="mb-4 text-sm font-medium text-gray-600"></div> <div id="main-content"> <textarea id="bulk-input" class="w-full h-32 p-3 mb-4 border border-gray-300 rounded-md resize-none focus:ring-2 focus:ring-blue-500 focus:border-transparent" placeholder="输入答案,如: A,B,C 或 1-3(ABC,DEF,GHI)"></textarea> <div id="questions-preview" class="max-h-64 overflow-y-auto mb-4 bg-gray-50 rounded-md p-3"></div> <div class="grid grid-cols-2 gap-3"> <button id="fillButton" class="col-span-2 bg-blue-500 hover:bg-blue-600 text-white font-bold py-2 px-4 rounded transition duration-300">填写答案</button> <button id="clearButton" class="bg-gray-200 hover:bg-gray-300 text-gray-800 font-medium py-2 px-4 rounded transition duration-300">清空输入</button> <button id="pasteButton" class="bg-green-500 hover:bg-green-600 text-white font-medium py-2 px-4 rounded transition duration-300">粘贴识别</button> <button id="configButton" class="bg-purple-500 hover:bg-purple-600 text-white font-medium py-2 px-4 rounded transition duration-300">配置选择器</button> <button id="detectButton" class="bg-yellow-500 hover:bg-yellow-600 text-white font-medium py-2 px-4 rounded transition duration-300">智能识别</button> </div> </div> `; document.body.appendChild(container); // 添加事件监听器 document .getElementById("fillButton") .addEventListener("click", fillAnswers); document .getElementById("clearButton") .addEventListener("click", clearInputs); document .getElementById("pasteButton") .addEventListener("click", pasteAndRecognize); document .getElementById("configButton") .addEventListener("click", showSelectorWizard); document .getElementById("detectButton") .addEventListener("click", smartDetectAnswers); document .getElementById("bulk-input") .addEventListener( "input", debounce(updateQuestionsPreview, GLOBAL.debounceDelay) ); } function updateQuestionsPreview() { const bulkInput = document.getElementById("bulk-input"); const questionsPreview = document.getElementById("questions-preview"); const answers = parseAnswers(bulkInput.value); // 添加自定义样式 if (!document.getElementById("custom-question-preview-style")) { const style = document.createElement("style"); style.id = "custom-question-preview-style"; style.textContent = ` .question-row { transition: all 0.3s ease; } .question-row:hover { transform: translateY(-2px); box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); } `; document.head.appendChild(style); } questionsPreview.innerHTML = questions .map((q, i) => { const answer = answers[i] || "-"; const isValid = validateAnswer(answer, q); const isFilled = answer !== "-"; const backgroundClass = isFilled ? isValid ? "bg-green-100" : "bg-red-100" : "bg-gray-100"; const answerColorClass = isFilled ? isValid ? "text-green-600" : "text-red-600" : "text-gray-600"; return ` <div class="question-row flex items-center mb-2 p-2 rounded ${backgroundClass}"> <div class="flex-none w-8 text-right mr-2"> <span class="font-bold text-gray-700">${i + 1}.</span> </div> <div class="flex-grow flex items-center overflow-hidden"> <span class="text-xs px-2 py-1 rounded mr-2 ${getTypeColor( q.type )}">${q.type}</span> <span class="text-sm text-gray-600 truncate flex-grow" title="${ q.text }"> ${ q.text.length > 10 ? q.text.substring(0, 10) + "..." : q.text } </span> <span class="font-semibold ml-2 ${answerColorClass}">${answer}</span> </div> </div> `; }) .join(""); } function getTypeColor(type) { switch (type) { case "单选题": return "bg-blue-200 text-blue-800"; case "多选题": return "bg-green-200 text-green-800"; case "判断题": return "bg-yellow-200 text-yellow-800"; default: return "bg-gray-200 text-gray-800"; } } function showMessage(message, type = "info", duration = 0) { const statusElement = document.getElementById("status-message"); statusElement.textContent = message; statusElement.className = `mb-4 text-sm font-medium p-3 rounded ${ type === "error" ? "bg-red-100 text-red-700" : type === "success" ? "bg-green-100 text-green-700" : "bg-blue-100 text-blue-700" }`; if (duration > 0) { setTimeout(() => { statusElement.textContent = ""; statusElement.className = "mb-4 text-sm font-medium text-gray-600"; }, duration); } } // 核心功能函数 function determineQuestionType(subject, options) { // 检查选项的类型 const optionTypes = Array.from(options).map((option) => { const input = option.tagName.toLowerCase() === "input" ? option : option.querySelector("input"); return input ? input.type : null; }); // 根据选项类型确定题目类型 if (optionTypes.every((type) => type === "radio")) { return optionTypes.length === 2 ? "判断题" : "单选题"; } else if (optionTypes.every((type) => type === "checkbox")) { return "多选题"; } // 如果无法通过input类型确定,尝试通过其他特征判断 const optionTexts = Array.from(options).map((option) => option.textContent.trim().toLowerCase() ); if (optionTexts.includes("正确") && optionTexts.includes("错误")) { return "判断题"; } // 如果仍然无法确定,返回未知类型 return "未知类型"; } function detectQuestions() { const currentSelectors = getSelectorsForCurrentDomain(); if (!currentSelectors) { showSelectorWizard(); return; } const subjectElements = document.querySelectorAll( currentSelectors.subjectContainer ); if (subjectElements.length === 0) { const questionTexts = Array.from( document.querySelectorAll(currentSelectors.questionText) ); const optionGroups = groupOptions( document.querySelectorAll(currentSelectors.options) ); questions = questionTexts.map((text, index) => { const questionText = questionTexts[index].textContent.trim(); const options = optionGroups[index]; if (!text || !options) { return null; } const questionType = determineQuestionType(text, options); return { type: questionType, optionCount: options.length, text: questionText, index: index + 1, }; }); console.log(questions); } else { questions = Array.from(subjectElements) .map((subject, index) => { const questionText = subject .querySelector(currentSelectors.questionText) ?.textContent.trim(); const options = subject.querySelectorAll(currentSelectors.options); if (!questionText || options.length === 0) { return null; } let questionType = determineQuestionType(subject, options); return { type: questionType, optionCount: options.length, text: questionText, index: index + 1, }; }) .filter((q) => q !== null); } isQuestionDetected = questions.length > 0; updateQuestionsPreview(); if (isQuestionDetected) { showMessage(`检测到 ${questions.length} 道题目`, "success"); } else { showMessage("未检测到题目,请配置选择器或重新检测", "error"); } } function groupOptions(options) { const groups = {}; options.forEach((option) => { if (!groups[option.name]) { groups[option.name] = []; } groups[option.name].push(option); }); return Object.values(groups); } function parseAnswers(input) { if (!input.trim()) { return []; } input = input.replace(/\s/g, "").toUpperCase(); const patterns = [ { regex: /(\d+)-(\d+)([A-Z]+)/, process: (match, answers) => { const [_, start, end, choices] = match; for (let i = parseInt(start); i <= parseInt(end); i++) { answers[i - 1] = choices[i - parseInt(start)] || ""; } }, }, { regex: /(\d+)([A-Z]+)/, process: (match, answers) => { const [_, number, choices] = match; answers[parseInt(number) - 1] = choices; }, }, { regex: /([A-Z]+)/, process: (match, answers) => { answers.push(match[1]); }, }, ]; let answers = []; const segments = input.split(","); segments.forEach((segment) => { let matched = false; for (const pattern of patterns) { const match = segment.match(pattern.regex); if (match) { pattern.process(match, answers); matched = true; break; } } if (!matched) { showMessage(`无法解析的输入段: ${segment}`, "error"); } }); return answers; } function validateAnswer(answer, question) { if (!answer || answer === "") return true; const options = answer.split(""); if (question.type === "单选题" || question.type === "判断题") { return ( options.length === 1 && options[0].charCodeAt(0) - 64 <= question.optionCount ); } else if (question.type === "多选题") { return options.every( (option) => option.charCodeAt(0) - 64 <= question.optionCount ); } return true; } async function fillAnswers() { const currentSelectors = getSelectorsForCurrentDomain(); if (!currentSelectors) { showMessage("未找到当前网站的选择器配置,请先配置选择器", "error"); return; } const bulkInput = document.getElementById("bulk-input"); const answers = parseAnswers(bulkInput.value); let filledCount = 0; const subjectContainers = document.querySelectorAll(currentSelectors.subjectContainer); const useSubjectLogic = subjectContainers.length > 0; for (let i = 0; i < questions.length; i++) { if (i >= answers.length) break; const question = questions[i]; const answer = answers[i]; if (answer && validateAnswer(answer, question)) { if (useSubjectLogic) { const subject = subjectContainers[question.index - 1]; if (subject) { filledCount += await fillAnswerForSubject(subject, answer, currentSelectors); } } else { filledCount += await fillAnswerForOptionGroup(i, answer, currentSelectors); } } } showMessage(`已填写 ${filledCount} 个答案`, "success"); } async function fillAnswerForSubject(subject, answer, currentSelectors) { let filledCount = 0; const options = subject.querySelectorAll(currentSelectors.options); for (let optionIndex = 0; optionIndex < options.length; optionIndex++) { const option = options[optionIndex]; const optionLetter = String.fromCharCode(65 + optionIndex); const shouldBeChecked = answer.includes(optionLetter); const input = option.tagName.toLowerCase() === "input" ? option : option.querySelector("input"); if (input && shouldBeChecked !== input.checked) { const label = option.closest("span") || option; label.click(); await sleep(GLOBAL.fillAnswerDelay); filledCount++; } } return filledCount; } async function fillAnswerForOptionGroup(questionIndex, answer, currentSelectors) { let filledCount = 0; const options = groupOptions(document.querySelectorAll(currentSelectors.options))[questionIndex]; if (options) { for (let optionIndex = 0; optionIndex < options.length; optionIndex++) { const option = options[optionIndex]; const optionLetter = String.fromCharCode(65 + optionIndex); const shouldBeChecked = answer.includes(optionLetter); if (shouldBeChecked !== option.checked) { option.click(); await sleep(GLOBAL.fillAnswerDelay); filledCount++; } } } return filledCount; } function clearInputs() { document.getElementById("bulk-input").value = ""; updateQuestionsPreview(); showMessage("输入已清空", "info"); } async function pasteAndRecognize() { try { const text = await navigator.clipboard.readText(); const bulkInput = document.getElementById("bulk-input"); bulkInput.value = text; updateQuestionsPreview(); showMessage("已从剪贴板粘贴并识别答案", "success"); } catch (err) { showMessage("无法访问剪贴板,请手动粘贴", "error"); } } function smartDetectAnswers() { const currentSelectors = getSelectorsForCurrentDomain(); if (!currentSelectors) { showMessage("未找到当前网站的选择器配置,请先配置选择器", "error"); return; } const subjectContainers = document.querySelectorAll( currentSelectors.subjectContainer ); const detectedAnswers = questions.map((question, index) => { const subject = document.querySelector( `${currentSelectors.subjectContainer}:nth-child(${question.index})` ); if (!subject) return ""; const answerElement = subject.querySelector( currentSelectors.answerElement ); if (!answerElement) return ""; const answerText = answerElement.textContent.trim(); if (!answerText) return ""; return processAnswer(answerText, question.type); }); const bulkInput = document.getElementById("bulk-input"); bulkInput.value = detectedAnswers.join(","); updateQuestionsPreview(); showMessage("已智能识别当前答案", "success"); } function processAnswer(answerText, questionType) { answerText = answerText.toUpperCase(); switch (questionType) { case "单选题": return answerText.match(/[A-Z]/)?.[0] || ""; case "多选题": return answerText.match(/[A-Z]/g)?.join("") || ""; case "判断题": if ( answerText.includes("对") || answerText.includes("A") || answerText === "T" ) { return "A"; } else if ( answerText.includes("错") || answerText.includes("B") || answerText === "F" ) { return "B"; } return ""; default: return answerText; } } // 新的选择器配置工具 function showSelectorWizard() { const currentDomain = getCurrentDomain(); const currentSelectors = SELECTORS[currentDomain] || {}; const wizard = document.createElement("div"); wizard.id = "selector-wizard"; wizard.className = "fixed top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 bg-white p-6 rounded-lg shadow-xl z-50 w-96 max-w-[90%]"; wizard.innerHTML = ` <div class="wizard-header flex justify-between items-center mb-4"> <h3 class="text-2xl font-bold text-gray-800">DOM 选择器配置</h3> <button id="close-wizard" class="text-gray-500 hover:text-gray-700">×</button> </div> <div class="wizard-body"> <div class="mb-4"> <label class="block text-sm font-medium text-gray-700 mb-1">当前网站</label> <input type="text" id="current-domain" class="w-full px-3 py-2 border border-gray-300 rounded-md" value="${currentDomain}" readonly> </div> ${createSelectorInput( "subjectContainer", "题目容器选择器", currentSelectors.subjectContainer )} ${createSelectorInput( "questionText", "问题文本选择器", currentSelectors.questionText )} ${createSelectorInput( "options", "选项选择器", currentSelectors.options )} ${createSelectorInput( "answerElement", "答案元素选择器", currentSelectors.answerElement )} <div class="wizard-controls mt-4 flex justify-between gap-2"> <button id="test-selectors" class="flex-grow bg-yellow-500 hover:bg-yellow-600 text-white font-bold py-2 px-4 rounded transition duration-300">测试选择器</button> <button id="save-selector" class="flex-grow bg-purple-500 hover:bg-purple-600 text-white font-bold py-2 px-4 rounded transition duration-300">保存</button> </div> </div> `; document.body.appendChild(wizard); document .getElementById("test-selectors") .addEventListener("click", testSelectors); document .getElementById("save-selector") .addEventListener("click", saveSelectors); document .getElementById("close-wizard") .addEventListener("click", () => wizard.remove()); } function testSelectors() { const testResults = {}; ["subjectContainer", "questionText", "options", "answerElement"].forEach( (selectorType) => { const selector = document.getElementById(selectorType).value; const elements = document.querySelectorAll(selector); testResults[selectorType] = elements.length; } ); let message = "测试结果:\n"; for (const [type, count] of Object.entries(testResults)) { message += `${type}: 找到 ${count} 个元素\n`; } alert(message); } function saveSelectors() { const currentDomain = getCurrentDomain(); SELECTORS[currentDomain] = { subjectContainer: document.getElementById("subjectContainer").value, questionText: document.getElementById("questionText").value, options: document.getElementById("options").value, answerElement: document.getElementById("answerElement").value, }; GM_setValue("domainSelectors", JSON.stringify(SELECTORS)); document.getElementById("selector-wizard").remove(); showMessage("选择器配置已保存,正在重新检测题目", "success"); detectQuestions(); } function createSelectorInput(id, label, value = "") { return ` <div class="mb-4"> <label for="${id}" class="block text-sm font-medium text-gray-700 mb-1">${label}</label> <input id="${id}" type="text" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-transparent" value="${value}"> </div> `; } // 初始化函数 function init() { // 加载 Tailwind CSS const tailwindCSS = `https://cdn.jsdelivr.net/npm/[email protected]/dist/tailwind.min.css`; const link = document.createElement("link"); link.href = tailwindCSS; link.rel = "stylesheet"; document.head.appendChild(link); // 创建主界面 createMainInterface(); // 延迟执行检测题目,确保页面完全加载 setTimeout(detectQuestions, 2000); } // 当 DOM 加载完成时初始化脚本 if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", init); } else { init(); } })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址