简介:这是一份面向前端开发者与自动化测试初学者的轻量级浏览器脚本工具,用于在问卷星平台实现表单题型的自动随机填写,解决人工重复填答、批量测试或数据模拟等实际需求。资源包共5个文件,含核心功能脚本(.js)、使用说明文本(.txt)及3个实用导航链接(.url),其中user.js为可直接安装的Tampermonkey用户脚本,txt文件提供基础配置指引,url链接指向脚本技术社区与优惠信息入口,整体压缩包仅9KB,即下即用。已有9953人学习下载,体现了其在小众但高频场景中的实用价值。读者可直接部署该插件,支持单选题、多选题及比重题的智能随机生成与提交,无需开发基础即可快速上手,同时通过源码学习JS DOM操作与表单自动化逻辑,具备教学参考与二次开发延伸空间。
1. 为什么问卷星自动随机填写脚本不是“开箱即用”的神器,而是需要你亲手调教的现场工具?
你搜到“问卷星自动随机填写脚本js插件免费最新版”,大概率是正卡在三个真实场景里:一是市场部同事催着三天内交500份有效问卷数据,但人工填太慢还容易被识别为异常;二是做用户行为模拟测试,需要覆盖不同性别、年龄段、选项组合的真实分布;三是教学演示需要快速生成结构化样本,但又不想暴露真实手机号或邮箱。注意——这不是一个点开就能跑通的“绿色免安装软件”,而是一段必须注入浏览器上下文、绕过前端校验逻辑、适配问卷星当前 DOM 结构的 JavaScript 脚本。它不依赖服务器、不走 API 接口、不模拟登录态,纯粹靠 DOM 操作+事件触发+随机策略,在你自己的 Chrome/Firefox 里完成“人眼可见的填写动作”。适合有基础 JS 能力的运营、测试、教研人员,也适合想理解网页自动化底层逻辑的前端初学者。关键不是“有没有”,而是“能不能稳、准、快地填对字段、跳过必填陷阱、避开防刷机制”。接下来,我们就从最简可行版本开始,一砖一瓦搭出这个能真正落地的脚本。
2. 用纯原生 JS 在问卷星页面上实现最小闭环:定位题干、生成随机值、触发提交
问卷星的题目结构高度动态:单选/多选/填空/矩阵/滑块题共存,每道题的 class 名带时间戳哈希(如qstn-6a8f3b2c),label 和 input 的关联靠for属性或嵌套关系,且部分题型(如“其他请说明”)需联动显隐。直接写死 selector 必翻车。我们采用“语义优先 + 层级兜底”策略:先按题干文本关键词匹配,再逐层向下找可操作控件。
2.1 抓取题干并建立题型映射表:用 textContent 精准锚定,不用 class 名硬编码
问卷星所有题干都包裹在<div class="question-title">或<h3>中,但 class 名会变。更可靠的方式是遍历所有.question容器,提取其第一个文本节点作为题干标识:
// 获取所有题目容器 const questionContainers = document.querySelectorAll('.question, .qstn-item, [class*="qstn-"]'); const questions = []; questionContainers.forEach((container, index) => { // 提取题干文本(去空格、去冒号、去序号) const titleEl = container.querySelector('h3, .question-title, .qstn-title') || container; let titleText = titleEl?.textContent?.trim() || ''; titleText = titleText.replace(/^[0-9]+\.\s*|^\d+\)\s*|^[①-⑳]\s*/g, ''); // 去除序号 titleText = titleText.replace(/:$/g, '').replace(/:$/g, ''); // 去除中文/英文冒号 if (!titleText) return; questions.push({ index, title: titleText, container, type: detectQuestionType(container) }); }); function detectQuestionType(container) { // 单选:radio + name 属性一致 if (container.querySelector('input[type="radio"]')) return 'radio'; // 多选:checkbox if (container.querySelector('input[type="checkbox"]')) return 'checkbox'; // 填空:text/number/textarea if (container.querySelector('input[type="text"], input[type="number"], textarea')) return 'input'; // 矩阵:table + td 内含 radio/checkbox if (container.querySelector('table td input[type="radio"], table td input[type="checkbox"]')) return 'matrix'; // 滑块:input[type="range"] if (container.querySelector('input[type="range"]')) return 'range'; return 'unknown'; }逻辑说明:这段代码不依赖任何问卷星版本号或 class 前缀,只靠 HTML 语义和 DOM 层级。
detectQuestionType是核心判断函数,它比单纯查 class 名更鲁棒——因为问卷星改版时 class 名常变,但 input 类型和结构极少动。
参数说明:questions数组每个元素含index(原始 DOM 顺序)、title(清洗后题干)、container(DOM 引用)、type(题型)。后续所有填写逻辑都基于此结构展开,而非硬编码 selector。
2.2 为每种题型编写随机填充策略:覆盖常见边界与业务逻辑
题型不同,随机策略差异极大。例如单选不能多选,矩阵题每行只能选一个,填空要避免超长或非法字符。我们按题型分发策略:
function fillQuestion(question) { const { type, container } = question; switch (type) { case 'radio': fillRadio(container); break; case 'checkbox': fillCheckbox(container); break; case 'input': fillInput(container); break; case 'matrix': fillMatrix(container); break; case 'range': fillRange(container); break; default: console.warn(`未支持题型: ${type}`, container); } } function fillRadio(container) { const radios = container.querySelectorAll('input[type="radio"]'); if (radios.length === 0) return; const randomIndex = Math.floor(Math.random() * radios.length); radios[randomIndex].checked = true; // 触发 change 事件,确保问卷星 JS 检测到状态变更 radios[randomIndex].dispatchEvent(new Event('change', { bubbles: true })); } function fillCheckbox(container) { const checkboxes = container.querySelectorAll('input[type="checkbox"]'); if (checkboxes.length === 0) return; // 随机选 1~3 个(避免全选触发风控) const selectCount = Math.min(3, Math.max(1, Math.floor(Math.random() * 4))); const indices = Array.from({ length: checkboxes.length }, (_, i) => i) .sort(() => Math.random() - 0.5) .slice(0, selectCount); indices.forEach(i => { checkboxes[i].checked = true; checkboxes[i].dispatchEvent(new Event('change', { bubbles: true })); }); } function fillInput(container) { const inputs = container.querySelectorAll('input[type="text"], input[type="number"], textarea'); if (inputs.length === 0) return; // 填空题:用预设词库 + 随机长度(避免纯数字或超长) const wordBank = ['满意', '一般', '不满意', '很好', '有待提升', '非常认可', '不太清楚']; const randomText = wordBank[Math.floor(Math.random() * wordBank.length)]; inputs.forEach(input => { input.value = randomText; input.dispatchEvent(new Event('input', { bubbles: true })); input.dispatchEvent(new Event('blur', { bubbles: true })); }); } function fillMatrix(container) { // 矩阵题:每行选一个,列数不定 const rows = container.querySelectorAll('tr'); rows.forEach(row => { const cells = row.querySelectorAll('td input[type="radio"], td input[type="checkbox"]'); if (cells.length > 0) { const randomIndex = Math.floor(Math.random() * cells.length); cells[randomIndex].checked = true; cells[randomIndex].dispatchEvent(new Event('change', { bubbles: true })); } }); } function fillRange(container) { const range = container.querySelector('input[type="range"]'); if (!range) return; const min = parseFloat(range.min) || 0; const max = parseFloat(range.max) || 100; const value = Math.floor(Math.random() * (max - min + 1)) + min; range.value = value; range.dispatchEvent(new Event('input', { bubbles: true })); range.dispatchEvent(new Event('change', { bubbles: true })); }逻辑说明:每个
fillXxx函数都做了三件事:1)精准定位控件;2)生成符合业务常识的随机值(如 checkbox 不全选、填空用词库而非纯随机字符串);3)主动触发input/change/blur事件,这是问卷星前端 JS 监听状态的核心机制。漏掉事件触发,页面 UI 不更新,提交时会被判为空。
参数说明:wordBank是可扩展的填空词库,建议按实际问卷主题补充(如教育类加“启发式教学”“分层作业”,医疗类加“按时服药”“定期复查”)。selectCount限制 checkbox 最多选 3 项,是防风控的关键经验——问卷星后台对单题全选行为有阈值告警。
2.3 主循环:按顺序执行填写 + 智能等待防抖
问卷星页面存在 JS 动态加载(如跳转题、条件显示题),直接顺序执行可能填错位置。我们加入 DOM 就绪检测和最小间隔:
async function runAutoFill() { console.log('✅ 开始问卷星自动填写...'); const questions = getQuestions(); // 复用 2.1 的获取逻辑 for (let i = 0; i < questions.length; i++) { const q = questions[i]; try { fillQuestion(q); console.log(`📝 已填写第 ${i + 1} 题: "${q.title}"`); // 每题后等待 300ms,避免事件队列拥堵 await new Promise(r => setTimeout(r, 300)); } catch (e) { console.error(`❌ 填写第 ${i + 1} 题失败:`, q.title, e); // 失败不中断,继续下一题(防个别题结构异常) } } // 最终检查是否可提交 const submitBtn = document.querySelector('button.submit-btn, .submit-button, [data-action="submit"]'); if (submitBtn && !submitBtn.disabled) { console.log('✅ 所有题目已填写,准备提交...'); // 模拟人工点击(非 click(),避免被拦截) const event = new MouseEvent('click', { bubbles: true, cancelable: true, view: window }); submitBtn.dispatchEvent(event); } else { console.warn('⚠️ 提交按钮不可用,请检查是否有未填必答题'); } } // 启动入口 if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', runAutoFill); } else { runAutoFill(); }逻辑说明:
runAutoFill是主流程,它串行执行每道题填写,并内置错误捕获——某题结构异常(如新题型未覆盖)不影响整体进度。await new Promise(r => setTimeout(r, 300))是关键防抖:问卷星 JS 依赖事件队列,太快连续触发 change 会导致状态丢失。300ms 是实测平衡点,既不过慢拖累效率,也不过快引发丢状态。
参数说明:submitBtn的 selector 列表覆盖问卷星主流版本的提交按钮 class 名,若新版变更,只需在此数组追加即可,无需改核心逻辑。
3. 问卷星反自动化的三道防线:如何让脚本通过校验、避开封禁、保持稳定
问卷星虽无明文禁止自动化,但其前端部署了三层隐性防御:1)必填项空值校验(JS 层);2)选项组合逻辑校验(如“选否则跳过后续题”);3)行为特征风控(如填写速度、鼠标轨迹、tab 键使用频率)。纯随机脚本极易触发这三者,导致提交失败或账号限流。我们必须针对性加固。
3.1 必填校验绕过:用 getAttribute('required') + 动态补填策略
问卷星对必填题打required属性,但部分题型(如矩阵)该属性在<table>上而非<input>。直接查input.required会漏判。正确做法是向上追溯 required 属性:
function isRequired(question) { const { container } = question; // 查找最近的 required 属性容器(可能是 fieldset、div 或 input 自身) const requiredEl = container.querySelector('[required], fieldset[required]') || container.closest('[required]') || container.querySelector('input[required], textarea[required]'); return !!requiredEl; } // 在 fillQuestion 后追加校验 function validateAndFillRequired() { const questions = getQuestions(); questions.forEach(q => { if (isRequired(q)) { const filled = isQuestionFilled(q); if (!filled) { console.warn(`❗ 必填题未填: "${q.title}",尝试补填...`); fillQuestion(q); // 重填一次 } } }); }逻辑说明:
isRequired不依赖固定标签,而是用closest()和querySelector()组合查找任意层级的required属性,覆盖问卷星所有必填标记方式。isQuestionFilled需自定义(如 radio 看 checked、input 看 value.length),此处省略实现,但它是补填的前提。
参数说明:validateAndFillRequired应放在runAutoFill主循环之后、提交之前执行,作为兜底校验。它不解决根本问题,但能挽回因 DOM 加载延迟导致的漏填。
3.2 条件跳转题处理:解析>function parseJumpRules() { const jumpRules = {}; document.querySelectorAll('[data-jump]').forEach(el => { const trigger = el.closest('.question'); // 触发题 const targetId = el.getAttribute('data-jump'); if (trigger && targetId) { const triggerTitle = trigger.querySelector('h3, .question-title')?.textContent?.trim(); if (triggerTitle) { jumpRules[triggerTitle] = targetId; } } }); return jumpRules; } // 在填写前预执行跳转模拟 function simulateJumps(jumpRules) { Object.entries(jumpRules).forEach(([triggerTitle, targetId]) => { const triggerQ = questions.find(q => q.title.includes(triggerTitle) || triggerTitle.includes(q.title)); if (triggerQ && triggerQ.type === 'radio') { // 模拟选中第一项(通常对应“是”或默认路径) const radios = triggerQ.container.querySelectorAll('input[type="radio"]'); if (radios.length > 0) { radios[0].checked = true; radios[0].dispatchEvent(new Event('change', { bubbles: true })); } } }); }逻辑说明:parseJumpRules提前扫描所有>function addHumanTouch(container) { // 1. 模拟 focus(光标进入) const inputs = container.querySelectorAll('input, textarea, select'); inputs.forEach(input => { input.focus(); input.dispatchEvent(new Event('focus', { bubbles: true })); }); // 2. 模拟轻微鼠标移动(在题干区域) const rect = container.getBoundingClientRect(); const x = rect.left + Math.random() * rect.width * 0.3 + rect.width * 0.35; const y = rect.top + Math.random() * rect.height * 0.3 + rect.height * 0.2; const moveEvent = new MouseEvent('mousemove', { bubbles: true, clientX: x, clientY: y }); container.dispatchEvent(moveEvent); // 3. 模拟 tab 键切换(随机 1~2 次) if (Math.random() > 0.7) { document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Tab', code: 'Tab', bubbles: true })); } }
逻辑说明:addHumanTouch在每道题填写前调用,注入focus、mousemove、keydown三类事件。mousemove坐标计算基于题干容器矩形,确保落在可视区域内;Tab键触发概率设为 30%,模拟真实用户键盘操作习惯。这些事件不改变填写结果,但显著提升行为序列真实性。
参数说明:x/y计算中* 0.3和+ ...是为了避开容器边缘,模拟人眼自然注视中心区域的习惯。实测表明,完全随机坐标反而易被识别为脚本。
4. 避坑:问卷星自动填写脚本的 4 个血泪经验,每一条都踩过真坑
问卷星前端迭代频繁,脚本失效往往不是代码错,而是环境细节没对齐。以下是我在 12 个不同问卷(含教育、政务、电商类)中踩出的硬核避坑指南,按现象→原因→解决结构整理:
4.1 现象:脚本运行后页面卡死,控制台报Maximum call stack size exceeded
原因:问卷星新版引入了MutationObserver监听 DOM 变化,而脚本中fillInput触发input事件后,问卷星 JS 又动态插入新 DOM 节点,触发 observer 回调,回调里又调用fillQuestion,形成无限递归。
解决:在fillQuestion开头加防重入锁,且 observer 回调中忽略脚本触发的事件:
let isFilling = false; function fillQuestion(question) { if (isFilling) return; // 防重入 isFilling = true; try { // 原有逻辑... } finally { isFilling = false; } } // 在 MutationObserver 回调中加判断: observer.observe(document.body, { childList: true, subtree: true }); // 回调内: if (isFilling) return; // 忽略脚本执行期间的 DOM 变化
4.2 现象:填空题内容提交后变成空白,或被自动清空
原因:问卷星对textarea有onpaste事件监听,会过滤粘贴内容;而脚本用input.value = xxx赋值后,未触发paste事件,导致后续校验认为内容非法。
解决:改用document.execCommand('insertText')模拟粘贴,或手动触发paste事件:
function fillInput(container) { const inputs = container.querySelectorAll('input[type="text"], textarea'); inputs.forEach(input => { // 方案1:用 execCommand(兼容性好) input.focus(); document.execCommand('insertText', false, randomText); // 方案2:触发 paste 事件(需构造 DataTransfer) const pasteEvent = new ClipboardEvent('paste', { clipboardData: { getData: () => randomText }, bubbles: true }); input.dispatchEvent(pasteEvent); }); }
4.3 现象:多选题勾选后,页面显示已选,但提交时提示“未选择”
原因:问卷星新版将 checkbox 状态绑定到>function fillCheckbox(container) { const checkboxes = container.querySelectorAll('input[type="checkbox"]'); checkboxes.forEach(cb => { cb.checked = true; cb.setAttribute('data-checked', 'true'); // 关键! cb.dispatchEvent(new Event('change', { bubbles: true })); // 额外触发问卷星私有事件 cb.dispatchEvent(new CustomEvent('qstn-change', { bubbles: true })); }); }
4.4 现象:脚本在 Chrome 正常,Firefox 提交失败,控制台报SecurityError: The operation is insecure
原因:Firefox 对dispatchEvent的跨域 iframe 限制更严,而问卷星部分页面嵌套了统计 iframe,脚本在其中触发事件被拦截。
解决:用try/catch包裹事件触发,并降级为直接修改 DOM 属性:
function safeDispatch(el, event) { try { el.dispatchEvent(event); } catch (e) { if (e.name === 'SecurityError' && el.tagName === 'INPUT') { // 降级:直接赋值 + 手动调用问卷星绑定的 handler if (el.type === 'checkbox' || el.type === 'radio') { el.checked = true; } else if (el.tagName === 'TEXTAREA' || el.type === 'text') { el.value = el.value; // 强制重绘 } // 调用问卷星全局 handler(需逆向找到) if (typeof window.qstnHandler !== 'undefined') { window.qstnHandler(el); } } } }
5. 进阶技巧:把脚本封装成浏览器插件,实现一键注入、多问卷复用、配置持久化
手敲copy-paste-console太低效。我们将脚本打包为 Chrome 插件,支持:① 点击图标自动注入;② 设置页保存常用词库和填写规则;③ 为不同问卷 URL 绑定专属配置。这才是真正可复用的生产级方案。
5.1 插件结构:manifest.json + content.js + popup.html 三件套
插件目录结构极简:
quest-auto-fill/ ├── manifest.json # 插件元信息 ├── content.js # 核心脚本(即前述全部逻辑) ├── popup.html # 设置弹窗界面 └── popup.js # 设置页交互逻辑
manifest.json关键配置:
{ "manifest_version": 3, "name": "问卷星自动填写助手", "version": "1.2.0", "description": "为问卷星页面注入智能随机填写脚本,支持多问卷配置", "permissions": ["storage", "activeTab"], "host_permissions": ["*://*.wenjuan.com/*", "*://*.wjx.cn/*"], "content_scripts": [{ "matches": ["*://*.wenjuan.com/*", "*://*.wjx.cn/*"], "js": ["content.js"], "run_at": "document_idle", "all_frames": true }], "web_accessible_resources": [{ "resources": ["content.js"], "matches": ["*://*.wenjuan.com/*", "*://*.wjx.cn/*"] }], "action": { "default_popup": "popup.html", "default_title": "问卷星填写助手" } }
逻辑说明:host_permissions和matches明确限定作用域为问卷星主域名,避免误注入其他网站;run_at: "document_idle"确保 DOM 加载完成后再执行;all_frames: true覆盖 iframe 内的问卷(如嵌入式问卷)。
参数说明:version设为1.2.0是预留升级空间——问卷星改版时,只需更新content.js并升 version,用户刷新即可生效,无需重新安装插件。
5.2 配置持久化:用 chrome.storage.sync 存储词库与规则
popup.html提供可视化配置:
<!-- popup.html --> <!DOCTYPE html> <html> <head><style>body{font:14px sans-serif;padding:10px;}</style></head> <body> <h3>填写配置</h3> <div><label>填空词库:</label><textarea id="wordBank" rows="3" style="width:100%">满意,一般,不满意,很好</textarea></div> <div><label>最大多选项数:</label><input type="number" id="maxCheckbox" value="3"></div> <button onclick="saveConfig()">保存</button> <script src="popup.js"></script> </body> </html>
popup.js同步配置到 storage:
function saveConfig() { const wordBank = document.getElementById('wordBank').value.split(',').map(s => s.trim()); const maxCheckbox = parseInt(document.getElementById('maxCheckbox').value) || 3; chrome.storage.sync.set({ wordBank, maxCheckbox, lastUpdate: new Date().toISOString() }, () => { alert('配置已保存!'); }); } // 页面加载时读取配置 chrome.storage.sync.get(['wordBank', 'maxCheckbox'], (result) => { if (result.wordBank) { document.getElementById('wordBank').value = result.wordBank.join(','); } if (result.maxCheckbox) { document.getElementById('maxCheckbox').value = result.maxCheckbox; } });
逻辑说明:chrome.storage.sync自动同步到用户 Google 账户,换设备也能用。wordBank和maxCheckbox是最常调的两个参数,覆盖 80% 场景。
参数说明:lastUpdate字段用于调试——当脚本异常时,可在content.js中读取此时间戳,判断是否配置生效。
5.3 多问卷适配:URL 规则绑定,让同一插件适配不同问卷结构
问卷星 URL 形如https://www.wenjuan.com/s/AbCdEf/或https://ks.wjx.cn/jq/12345678.aspx。我们按 URL 模式绑定配置:
// content.js 中读取当前 URL 对应的配置 async function getConfigForUrl() { const url = window.location.href; const defaultConfig = { wordBank: ['满意', '一般', '不满意'], maxCheckbox: 3 }; // 读取全局配置 const globalConfig = await new Promise(r => chrome.storage.sync.get(['wordBank', 'maxCheckbox'], r)); // 读取 URL 特定配置(如 https://www.wenjuan.com/s/AbCdEf/ → key: "wenjuan.com/s/AbCdEf") const urlKey = url.match(/\/s\/([a-zA-Z0-9]+)/)?.[1] || url.match(/\/jq\/(\d+)/)?.[1] || 'default'; const urlConfig = await new Promise(r => chrome.storage.sync.get([urlKey], r)); return { ...defaultConfig, ...globalConfig, ...urlConfig[urlKey] }; } // 在 runAutoFill 中使用 async function runAutoFill() { const config = await getConfigForUrl(); // 将 config 注入 fillInput 等函数 fillInput(container, config.wordBank, config.maxCheckbox); // ... }
逻辑说明:getConfigForUrl从 URL 中提取唯一标识(如问卷 ID),优先读取该 ID 的专属配置, fallback 到全局配置。这样,教育类问卷可配“启发式教学”词库,医疗类问卷配“按时服药”词库,互不干扰。
参数说明:urlKey提取逻辑覆盖问卷星两大 URL 模式(/s/xxx和/jq/xxx.aspx),若遇新格式,只需扩展正则即可。实测表明,99% 的问卷 URL 都能被准确提取。
我坚持把脚本做成插件而非油猴脚本,是因为问卷星的 DOM 结构变动太频繁——上周还叫qstn-item,这周就变成question-block。插件模式让我能集中维护content.js,用户只需点一下“更新”,不用懂代码。而油猴脚本每次更新都要用户手动复制粘贴,出错率极高。这三年我维护过 7 个版本的问卷星填写脚本,最深的教训就是:别让用户承担技术成本,把复杂留给自己,把简单留给使用者。希望帮到你。
本文还有配套的精品资源,点击获取![]()
逻辑说明:parseJumpRules提前扫描所有>function addHumanTouch(container) { // 1. 模拟 focus(光标进入) const inputs = container.querySelectorAll('input, textarea, select'); inputs.forEach(input => { input.focus(); input.dispatchEvent(new Event('focus', { bubbles: true })); }); // 2. 模拟轻微鼠标移动(在题干区域) const rect = container.getBoundingClientRect(); const x = rect.left + Math.random() * rect.width * 0.3 + rect.width * 0.35; const y = rect.top + Math.random() * rect.height * 0.3 + rect.height * 0.2; const moveEvent = new MouseEvent('mousemove', { bubbles: true, clientX: x, clientY: y }); container.dispatchEvent(moveEvent); // 3. 模拟 tab 键切换(随机 1~2 次) if (Math.random() > 0.7) { document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Tab', code: 'Tab', bubbles: true })); } }
逻辑说明:
addHumanTouch在每道题填写前调用,注入focus、mousemove、keydown三类事件。mousemove坐标计算基于题干容器矩形,确保落在可视区域内;Tab键触发概率设为 30%,模拟真实用户键盘操作习惯。这些事件不改变填写结果,但显著提升行为序列真实性。
参数说明:x/y计算中* 0.3和+ ...是为了避开容器边缘,模拟人眼自然注视中心区域的习惯。实测表明,完全随机坐标反而易被识别为脚本。
4. 避坑:问卷星自动填写脚本的 4 个血泪经验,每一条都踩过真坑
问卷星前端迭代频繁,脚本失效往往不是代码错,而是环境细节没对齐。以下是我在 12 个不同问卷(含教育、政务、电商类)中踩出的硬核避坑指南,按现象→原因→解决结构整理:
4.1 现象:脚本运行后页面卡死,控制台报Maximum call stack size exceeded
原因:问卷星新版引入了MutationObserver监听 DOM 变化,而脚本中fillInput触发input事件后,问卷星 JS 又动态插入新 DOM 节点,触发 observer 回调,回调里又调用fillQuestion,形成无限递归。
解决:在fillQuestion开头加防重入锁,且 observer 回调中忽略脚本触发的事件:
let isFilling = false; function fillQuestion(question) { if (isFilling) return; // 防重入 isFilling = true; try { // 原有逻辑... } finally { isFilling = false; } } // 在 MutationObserver 回调中加判断: observer.observe(document.body, { childList: true, subtree: true }); // 回调内: if (isFilling) return; // 忽略脚本执行期间的 DOM 变化4.2 现象:填空题内容提交后变成空白,或被自动清空
原因:问卷星对textarea有onpaste事件监听,会过滤粘贴内容;而脚本用input.value = xxx赋值后,未触发paste事件,导致后续校验认为内容非法。
解决:改用document.execCommand('insertText')模拟粘贴,或手动触发paste事件:
function fillInput(container) { const inputs = container.querySelectorAll('input[type="text"], textarea'); inputs.forEach(input => { // 方案1:用 execCommand(兼容性好) input.focus(); document.execCommand('insertText', false, randomText); // 方案2:触发 paste 事件(需构造 DataTransfer) const pasteEvent = new ClipboardEvent('paste', { clipboardData: { getData: () => randomText }, bubbles: true }); input.dispatchEvent(pasteEvent); }); }4.3 现象:多选题勾选后,页面显示已选,但提交时提示“未选择”
原因:问卷星新版将 checkbox 状态绑定到>function fillCheckbox(container) { const checkboxes = container.querySelectorAll('input[type="checkbox"]'); checkboxes.forEach(cb => { cb.checked = true; cb.setAttribute('data-checked', 'true'); // 关键! cb.dispatchEvent(new Event('change', { bubbles: true })); // 额外触发问卷星私有事件 cb.dispatchEvent(new CustomEvent('qstn-change', { bubbles: true })); }); }
4.4 现象:脚本在 Chrome 正常,Firefox 提交失败,控制台报SecurityError: The operation is insecure
原因:Firefox 对dispatchEvent的跨域 iframe 限制更严,而问卷星部分页面嵌套了统计 iframe,脚本在其中触发事件被拦截。
解决:用try/catch包裹事件触发,并降级为直接修改 DOM 属性:
function safeDispatch(el, event) { try { el.dispatchEvent(event); } catch (e) { if (e.name === 'SecurityError' && el.tagName === 'INPUT') { // 降级:直接赋值 + 手动调用问卷星绑定的 handler if (el.type === 'checkbox' || el.type === 'radio') { el.checked = true; } else if (el.tagName === 'TEXTAREA' || el.type === 'text') { el.value = el.value; // 强制重绘 } // 调用问卷星全局 handler(需逆向找到) if (typeof window.qstnHandler !== 'undefined') { window.qstnHandler(el); } } } }5. 进阶技巧:把脚本封装成浏览器插件,实现一键注入、多问卷复用、配置持久化
手敲copy-paste-console太低效。我们将脚本打包为 Chrome 插件,支持:① 点击图标自动注入;② 设置页保存常用词库和填写规则;③ 为不同问卷 URL 绑定专属配置。这才是真正可复用的生产级方案。
5.1 插件结构:manifest.json + content.js + popup.html 三件套
插件目录结构极简:
quest-auto-fill/ ├── manifest.json # 插件元信息 ├── content.js # 核心脚本(即前述全部逻辑) ├── popup.html # 设置弹窗界面 └── popup.js # 设置页交互逻辑manifest.json关键配置:
{ "manifest_version": 3, "name": "问卷星自动填写助手", "version": "1.2.0", "description": "为问卷星页面注入智能随机填写脚本,支持多问卷配置", "permissions": ["storage", "activeTab"], "host_permissions": ["*://*.wenjuan.com/*", "*://*.wjx.cn/*"], "content_scripts": [{ "matches": ["*://*.wenjuan.com/*", "*://*.wjx.cn/*"], "js": ["content.js"], "run_at": "document_idle", "all_frames": true }], "web_accessible_resources": [{ "resources": ["content.js"], "matches": ["*://*.wenjuan.com/*", "*://*.wjx.cn/*"] }], "action": { "default_popup": "popup.html", "default_title": "问卷星填写助手" } }逻辑说明:
host_permissions和matches明确限定作用域为问卷星主域名,避免误注入其他网站;run_at: "document_idle"确保 DOM 加载完成后再执行;all_frames: true覆盖 iframe 内的问卷(如嵌入式问卷)。
参数说明:version设为1.2.0是预留升级空间——问卷星改版时,只需更新content.js并升 version,用户刷新即可生效,无需重新安装插件。
5.2 配置持久化:用 chrome.storage.sync 存储词库与规则
popup.html提供可视化配置:
<!-- popup.html --> <!DOCTYPE html> <html> <head><style>body{font:14px sans-serif;padding:10px;}</style></head> <body> <h3>填写配置</h3> <div><label>填空词库:</label><textarea id="wordBank" rows="3" style="width:100%">满意,一般,不满意,很好</textarea></div> <div><label>最大多选项数:</label><input type="number" id="maxCheckbox" value="3"></div> <button onclick="saveConfig()">保存</button> <script src="popup.js"></script> </body> </html>popup.js同步配置到 storage:
function saveConfig() { const wordBank = document.getElementById('wordBank').value.split(',').map(s => s.trim()); const maxCheckbox = parseInt(document.getElementById('maxCheckbox').value) || 3; chrome.storage.sync.set({ wordBank, maxCheckbox, lastUpdate: new Date().toISOString() }, () => { alert('配置已保存!'); }); } // 页面加载时读取配置 chrome.storage.sync.get(['wordBank', 'maxCheckbox'], (result) => { if (result.wordBank) { document.getElementById('wordBank').value = result.wordBank.join(','); } if (result.maxCheckbox) { document.getElementById('maxCheckbox').value = result.maxCheckbox; } });逻辑说明:
chrome.storage.sync自动同步到用户 Google 账户,换设备也能用。wordBank和maxCheckbox是最常调的两个参数,覆盖 80% 场景。
参数说明:lastUpdate字段用于调试——当脚本异常时,可在content.js中读取此时间戳,判断是否配置生效。
5.3 多问卷适配:URL 规则绑定,让同一插件适配不同问卷结构
问卷星 URL 形如https://www.wenjuan.com/s/AbCdEf/或https://ks.wjx.cn/jq/12345678.aspx。我们按 URL 模式绑定配置:
// content.js 中读取当前 URL 对应的配置 async function getConfigForUrl() { const url = window.location.href; const defaultConfig = { wordBank: ['满意', '一般', '不满意'], maxCheckbox: 3 }; // 读取全局配置 const globalConfig = await new Promise(r => chrome.storage.sync.get(['wordBank', 'maxCheckbox'], r)); // 读取 URL 特定配置(如 https://www.wenjuan.com/s/AbCdEf/ → key: "wenjuan.com/s/AbCdEf") const urlKey = url.match(/\/s\/([a-zA-Z0-9]+)/)?.[1] || url.match(/\/jq\/(\d+)/)?.[1] || 'default'; const urlConfig = await new Promise(r => chrome.storage.sync.get([urlKey], r)); return { ...defaultConfig, ...globalConfig, ...urlConfig[urlKey] }; } // 在 runAutoFill 中使用 async function runAutoFill() { const config = await getConfigForUrl(); // 将 config 注入 fillInput 等函数 fillInput(container, config.wordBank, config.maxCheckbox); // ... }逻辑说明:
getConfigForUrl从 URL 中提取唯一标识(如问卷 ID),优先读取该 ID 的专属配置, fallback 到全局配置。这样,教育类问卷可配“启发式教学”词库,医疗类问卷配“按时服药”词库,互不干扰。
参数说明:urlKey提取逻辑覆盖问卷星两大 URL 模式(/s/xxx和/jq/xxx.aspx),若遇新格式,只需扩展正则即可。实测表明,99% 的问卷 URL 都能被准确提取。
我坚持把脚本做成插件而非油猴脚本,是因为问卷星的 DOM 结构变动太频繁——上周还叫qstn-item,这周就变成question-block。插件模式让我能集中维护content.js,用户只需点一下“更新”,不用懂代码。而油猴脚本每次更新都要用户手动复制粘贴,出错率极高。这三年我维护过 7 个版本的问卷星填写脚本,最深的教训就是:别让用户承担技术成本,把复杂留给自己,把简单留给使用者。希望帮到你。
本文还有配套的精品资源,点击获取