简介:一份针对问卷星平台的自动随机填写JavaScript插件,旨在帮助运营人员、测试人员或调研用户在问卷填写环节减少重复手工操作。插件覆盖单选题、多选题与比重题三类常见题型,能够按设定逻辑自动生成随机答案。资源包共5个文件,压缩包仅9KB,主要包含user.js核心脚本、txt说明文档以及若干参考链接文件,方便用户快速理解脚本用法与浏览器扩展安装方式。目前已有9962人学习或下载。除即用即装外,脚本内逻辑清晰,可作为学习浏览器用户脚本开发、掌握自动化表单填充原理的入门范例;同时附带的浏览器兼容性说明与参考来源能帮助用户在Chrome等主流浏览器中顺利部署,整体体量虽小,但功能直接,适合需要临时批量填写问卷或希望二次开发定制答案策略的场景。
1. 问卷星自动随机填写脚本:一个解决“批量问卷录入”痛点的JS插件
还在一道题一道题地手动点问卷星?如果在做批量问卷投放、从事网络软件相关的数据采集工作,或者经常被“问卷互填群”里几十份链接搞得心烦,这个脚本就是为这类重复劳动准备的。它本质是一段塞进浏览器就能跑的JavaScript插件,加载后会自动识别问卷里的单选题、多选题、矩阵题和填空题,按预设规则生成一套随机但合理的答案,模拟人工节奏逐题填写,最后自动点击提交。它不调用任何后台接口,纯粹在前端模拟真人行为,所以问卷方看到的就是一份真实填答记录。新手拿来装上油猴即可跑通,开发者可以在此基础上改成自己的数据生成引擎。
2. 脚本结构设计:从读题到提交的一条完整随机链路
2.1 整体层次:调度层、随机策略层、DOM操作层
拆开这个脚本,能明显看到三层结构。调度层负责整个执行节奏,它维护一个任务队列,每个问卷链接对应一组待执行动作;随机策略层是核心,它把每种题型的随机规则封装成独立函数,单选题从选项数组里取随机索引,多选题按权值比例抽取若干项;DOM操作层则专注和页面交互,找到选项节点、模拟点击、触发事件、检查是否漏填。
三层之间通过一个简单的“状态机”串起来:初始态→等待题目渲染→逐题填写→校验完成度→提交。每进入一个状态,调度层会先调用随机策略层生成答案,再把答案交给DOM操作层去执行。这样做的好处是,如果问卷星的页面结构变了,只需要改DOM操作层,随机策略完全不用动。
// 调度器核心代码——每个任务的生命周期管理 const taskQueue = []; let isRunning = false; async function runTask(task) { isRunning = true; try { await waitForQuestions(task.paper); // 等待问卷题目渲染完成 const answers = generateAnswers(task); // 随机策略层生成答案 await fillAnswers(answers); // DOM操作层填入页面 await verifyCompletion(); // 校验是否有漏填 await submitPaper(task); // 提交并记录结果 console.log(`[完成任务] ${task.name} 用时 ${(Date.now() - task.startTime) / 1000}s`); } catch (err) { console.error(`[任务失败] ${task.name}: ${err.message}`); } finally { isRunning = false; next(); // 取队列里的下一个任务 } }上面的代码是整个脚本的节拍器。waitForQuestions里用了轮询加超时机制,因为问卷星有些题是异步加载的,直接执行查找会扑空;generateAnswers返回的是一个数组,每个元素对应一道题的选中结果;fillAnswers是纯DOM操作,逐个把答案“放”到页面上。verifyCompletion这步很关键,它遍历所有必填题,如果有漏网之鱼会在控制台标红。参数task.paper是问卷的元信息,包括题目列表和各题的随机规则。
2.2 关键技术选型:为什么用MutationObserver而不是setInterval
最初版本里,等待题目渲染用的是setInterval每500ms查一次DOM,但这有两个问题:一是空转浪费性能,二是问卷星的部分题型(矩阵题、下拉题)的DOM结构是动态生成的,用轮询很难精准捕捉到渲染完成的时机。后来改成MutationObserver监听题目容器子节点的变化,一旦检测到特定数量的选项节点插入就立刻接管。
// 监听问卷内容容器,节点变化时触发后续流程 function waitForQuestions(paper, timeout = 15000) { return new Promise((resolve, reject) => { const container = document.querySelector(paper.containerSelector); if (!container) return reject(new Error('未找到问卷容器,可能iframe结构不同')); // 已存在的题目直接放行 if (container.querySelectorAll(paper.questionSelector).length >= paper.expectedCount) { return resolve(); } const timer = setTimeout(() => reject(new Error('等待超时,题目未完全渲染')), timeout); const observer = new MutationObserver(() => { const actual = container.querySelectorAll(paper.questionSelector).length; if (actual >= paper.expectedCount) { clearTimeout(timer); observer.disconnect(); resolve(); } }); observer.observe(container, { childList: true, subtree: true }); }); }这里paper.expectedCount需要预先配置,一般取问卷题目总数,填入时按questionSelector能匹配到的实际节点数判断。MutationObserver的回调里做了幂等处理——即使多次触发也只会resolve一次,避免了重复执行后续流程。超时机制是血泪经验换来的:有次在某个网速较差的环境下,题目加载慢,脚本直接报错退出,打那以后就养成了所有等待必带超时的习惯。
3. 核心随机逻辑拆解:单选题、多选题与矩阵题的实现代码
3.1 单选题:均匀随机与加权随机的切换
单选题是最常见的题型,随机策略看似简单——从选项里随机选一个,但实际使用时会有两种诉求:一是模拟真实填答,各选项出现概率大致均匀;二是调查问卷的投放方可能希望某个选项(比如“同意”)出现的概率更高,方便做对照。所以脚本把单选逻辑拆成uniformChoice和weightedChoice两个函数,配置里指定用哪个。
// 单选题随机策略:均匀随机与加权随机 function singleChoice(options, strategy = 'uniform', weights = null) { if (strategy === 'uniform') { const idx = Math.floor(Math.random() * options.length); return options[idx]; } if (strategy === 'weighted') { // weights 与 options 一一对应,例如 [0.2, 0.3, 0.5],总和应为 1 let cumulative = 0; const rand = Math.random(); for (let i = 0; i < options.length; i++) { cumulative += (weights && weights[i]) ? weights[i] : 1 / options.length; if (rand <= cumulative) return options[i]; } return options[options.length - 1]; // 兜底返回最后一项 } throw new Error('未知的单选策略类型'); }加权的实现用的是累进概率法,rand落在哪个区间就选哪项。比如四个选项权重分别是0.1、0.2、0.3、0.4,那么随机数0.76会落到0.6到1.0区间,选第四项。参数weights可以不传,此时加权退化成均匀随机。这里有个翻车点:权重数组的总和并不强制校验为1,因为概率累加到最后一项自动兜底,但如果某一项权重写成了负值,整个累进逻辑就乱了,所以调用方要自己保证合法性。
3.2 多选题:按比例抽取数量,再按权重选具体项
多选题比单选多一层随机性——不仅要随机选哪些项,还要随机选几项。脚本用一个minCount和maxCount的区间来约束数量,再从区间内随机取一个整数作为本轮的选中数,最后从选项池里不重复抽取。
// 多选题随机策略:先随机决定选几项,再随机决定选哪几项 function multiChoice(options, minCount = 2, maxCount = null) { const upper = maxCount || options.length; const pickCount = Math.floor(Math.random() * (upper - minCount + 1)) + minCount; const shuffled = [...options].sort(() => Math.random() - 0.5); // 洗牌 return shuffled.slice(0, pickCount); }洗牌排序用的是简化版的Fisher-Yates,对选项数量通常不超过10个的问卷场景完全够用。需要注意边界:如果minCount大于选项总数,脚本不会报错,但返回的数组长度会小于理论值,校验完成度时会判定为漏填。我一般在调用处加一道防御——Math.min(minCount, options.length),宁可少选也不要触发问卷自带的校验弹窗。
3.3 矩阵题:逐行独立随机,行间可配置“同答案”模式
矩阵题是问卷星里结构最丑的题型,它在DOM上是一张二维表格,每行是一个子题目,每列是一个评分等级。随机策略需要遍历每一行,对每行单独做一次单选。但实际投放时有一个常见需求:模拟“认真填答”的答卷,不应该每行都选同一个等级,否则数据一眼假;反过来,在某些特定场景下(比如录入一份真实存在的纸质问卷)又需要所有行都填同一列。脚本用sameAcrossRows开关控制这两种模式。
// 矩阵题随机策略:遍历每一行进行独立随机或统一随机 function matrixChoice(rows, columns, sameAcrossRows = false) { const results = []; let fixedColumn = null; if (sameAcrossRows) { // 所有行选同一列,模拟一份“复制粘贴”的答案 fixedColumn = Math.floor(Math.random() * columns.length); } for (const row of rows) { const colIdx = sameAcrossRows ? fixedColumn : Math.floor(Math.random() * columns.length); results.push({ rowId: row.id, columnId: columns[colIdx].id, columnLabel: columns[colIdx].label, }); } return results; }参数rows和columns是从DOM里提取的节点集合,每项都带id和label两个属性。逐行随机模式下,每一行都有独立的Math.random()调用,所以行间不会出现规律性;sameAcrossRows模式把随机数固定在循环外,所有行共用同一个索引。开发时要注意columns的取值顺序必须和DOM渲染顺序一致,否则会出现“选A但页面高亮的是B”的错位问题。
3.4 文本填空题:从话术池取随机内容
文本题的随机策略没有数学上的随机可言,它的核心是一个话术池——脚本内置了几十条经过脱敏的通用回答,按题目类型分组:职业、城市、使用习惯、建议意见等。每次填写时从对应分组里按均匀概率取一条填进去。
// 文本填空题:从话术池按分组随机取一条 const textPool = { career: ['互联网产品经理', '数据分析师', '软件测试工程师', '前端开发工程师'], city: ['上海', '杭州', '成都', '武汉'], habit: ['每天使用2小时左右', '周末集中使用', '工作日常用'], suggestion: ['整体体验不错,希望增加导出功能', '暂无其他建议', '希望优化交互细节'], }; function fillText(fieldType, customPool = null) { const pool = customPool || textPool[fieldType]; if (!pool || pool.length === 0) return '无'; return pool[Math.floor(Math.random() * pool.length)]; }这里的fieldType需要在配置阶段标注,比如根据题目文本里的“您的职业”自动映射到career,脚本提供正则匹配来做这个映射。因为话术池是常量数组,出现重复答案的概率很低。自定义池用customPool传入,适合有特殊业务需求的场景——比如某次调研只需要采集城市信息,就把默认池替换成目标城市列表,减少无关文本的干扰。
4. 运行时避坑指南:常见报错、漏填与识别问题的排查记录
4.1 页面找不到选项节点,控制台报“未找到问卷容器”
现象是脚本执行后毫无反应,打开控制台看到未找到问卷容器或等待超时。主要原因有两个:一是问卷页面外层套了iframe,document.querySelector只查了顶层文档,没穿透进去;二是问卷星的题型默认折叠,部分题目要滚动到可视区域才会渲染DOM。解决方式是在配置里增加frameSelector参数,脚本先拿到iframe的contentDocument再继续查询;同时把滚动策略从“一次性滚到底”改成“分段滚动,每滚一段暂停500ms”,保证懒加载的题目有足够时间渲染。
4.2 填完后提交,问卷星提示“存在未回答题目”
这是脚本和问卷星自校验打架的经典问题。原因有两层:外层原因是脚本判断“已填”的方式是用classList.contains('checked')之类的状态位,但矩阵题里部分行的状态位加在子节点上,遍历时漏了;内层原因是问卷星有些题目设了“条件显示”——比如选了“其他”才会出现一个补充文本框,脚本没处理这种动态出现的题。解决方式是校验前先把所有题目的状态统一汇总,用offsetParent !== null判断元素是否在页面中可见,只对可见题目做填答检查;补充文本框这类动态题则用一个二次扫描兜底。
4.3 提交频率过快,触发问卷星的风险验证
现象是脚本跑完一轮后没有跳转到提交成功页,而是出现滑块或点选验证。根本原因是填答节奏不像真人——每题的间隔固定为0毫秒,提交按钮也是瞬间点击。解决方式是在调度层加“拟人延迟”,每题填完后随机等待800到2500毫秒,提交前额外等待2到5秒;同时把点击动作改成“按下-延迟-抬起”两段式。从效果看,随机延迟比任何UA伪装都管用。
4.4 答案填了又被清空,选项高亮闪烁后回到未选状态
这个现象通常出现在多选题上:脚本触发了click事件,页面暂时选中了选项,但脚本继续执行后续代码时,某个全局事件把前一项的状态重置了。排查下来是因为脚本在填入每一题时触发了问卷星的“选项联动”逻辑——某些题之间有关联关系,选择A题某选项后,B题的所有已选状态被后端清空重算。解决方式是填答完成后做一遍全卷重扫,发现空题就重新填,最多重试三次。做这版迭代时也算是“翻车”翻出来的经验,后来凡是带联动逻辑的问卷都直接建议客户关闭联动校验再投放。
4.5 脚本在浏览器控制台手动执行正常,但油猴跑不起来
现象是粘贴到开发者工具的Console里一切正常,装上油猴后不生效。原因几乎都是执行时机不对——油猴默认在document-start时注入,此时整个DOM还没建好,脚本查找容器必然失败。解决方式是在脚本头的元数据里显式声明@run-at document-idle,让油猴等页面全部加载完再执行;同时在代码里保留防御性的DOMContentLoaded判断,双保险。
5. 本地运行与外部服务集成:油猴加载、参数配置与Webhook对接
5.1 油猴环境与脚本加载步骤
脚本以用户脚本的形式分发,运行环境是Tampermonkey或Violentmonkey。安装步骤是先给浏览器装上油猴扩展,然后新建脚本,把分发文件内容粘贴进去保存。关键点在脚本头部的元数据块,它决定了脚本的生效范围和注入时机。
// ==UserScript== // @name 问卷星自动随机填写脚本 // @namespace https://local.lab/autofill // @version 1.0.3 // @description 自动识别题型并随机填写,支持单选多选矩阵文本 // @match https://*.wjx.cn/* // @match https://*.wjx.top/* // @match https://*.问卷星.com/* // @run-at document-idle // @grant none // ==/UserScript==@match规则只对问卷星主域名及其子域名生效,避免脚本在其他站点误触发。@grant none表示脚本不需要访问油猴的API,纯靠原生JavaScript运行,这样兼容性最好。保存后刷新问卷页面,控制台会输出“脚本已注入”的日志,如果没看到这行日志,优先检查@match是否被浏览器安全策略拦截。
5.2 核心参数配置:一份能直接抄的配置表
脚本的默认配置放在代码顶部的CONFIG对象里,改动它不需要动其他逻辑。下表是各参数的用途和取值范围。
| 参数名 | 默认值 | 用途说明 |
|---|---|---|
minDelay | 800 | 每题填答后的最短随机延迟(毫秒) |
maxDelay | 2500 | 每题填答后的最长随机延迟(毫秒) |
submitAfterFill | true | 全部填完后是否自动点击提交 |
maxRetryCount | 3 | 遇到漏填或答案被清空时的最大重试次数 |
sameAcrossMatrixRows | false | 矩阵题是否所有行选同一选项 |
multiMinCount | 2 | 多选题最小选中数量 |
multiMaxCount | 4 | 多选题最大选中数量(超过选项总数时自动截断) |
enableWebhook | false | 是否把填答结果POST到本地服务 |
改动建议是一处只动一个值,比如想控制提交频率,优先调minDelay和maxDelay,不要动maxRetryCount。multiMinCount调太高会触发问卷星的最小选项数校验,所以配置完第一件事就是打开控制台看有没有红色警告。
5.3 对接本地服务:把填答结果变成结构化数据
脚本不止能填问卷,它还带一个enableWebhook开关。打开后,每完成一份问卷,脚本会把当前问卷的标题、每道题的题号和选中项内容组装成一个JSON对象,通过navigator.sendBeacon发送到本地或内网服务。这个设计是为了批量投放场景——填一百份问卷后,服务端能实时收到一百条结构化记录,直接写入数据库或导出Excel。
// 结果上报:组装当前填写结果并发送到本地服务 function reportResult(paperInfo, fillResult) { if (!CONFIG.enableWebhook) return; const payload = { paperTitle: paperInfo.title, filledAt: new Date().toISOString(), answers: fillResult.map((item, index) => ({ questionNo: index + 1, questionType: item.type, selected: item.selected, })), }; fetch(CONFIG.webhookUrl, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload), keepalive: true, // 页面跳转前也能送达 }).catch((err) => console.warn('[上报失败]', err.message)); }keepalive: true很关键。提交问卷后页面会立刻跳转到“已完成”页,如果用普通fetch,请求可能来不及发出去就被浏览器终止了。sendBeacon也有同样的保活特性,但它的payload大小限制在64KB以内,对问卷填答结果来说绰绰有余。本地服务端只需要一个简单的POST /api/answers接口,接收JSON后落库即可。
6. 二次开发思路:把脚本改造成一套可复用的问卷数据生成服务
如果只是日常使用,懂配置就够了。但要把它变成自己的工具,有几个值得投入的改造方向。第一是把随机策略层抽成独立模块,现在所有函数都挂在一个全局对象上,可以改造成export的形式,方便单元测试——单选函数跑一万次,统计各个选项的分布频率,验证是否符合期望概率;矩阵的行列映射关系也可以抽成配置文件,不写死在代码里。第二是加一个可视化配置面板,用油猴的GM_setValue把参数存到本地,每次弹窗调整延迟和权重,不需要打开代码改JSON。第三是扩展题型覆盖,除了标准题,问卷星还有“排序题”和“量表题”,量表题本质是矩阵题的变体,复用matrixChoice即可,排序题则需要额外实现Fisher-Yates全排列后按序点击。
验证方法上,最有效的不是看脚本报不报错,而是收集50份填答数据做统计分析——各选项的分布是否接近期望,答案之间的关联是否过于规律。如果分布均匀,说明随机逻辑合格;如果某份问卷所有选项清一色选A,十有八九是种子随机数被复用了,检查一下是否在循环外缓存了随机对象。
做这版迭代时吃过一次亏:当时为了提升效率把全部选项的延迟改成固定800ms,投放了二十份问卷后发现提交时间几乎一模一样,数据整体看起来像机器人刷的。从那以后我每次发布前都强制跑一遍“延迟分布检查”,确保每次填答的耗时方差足够大,样本数据才经得起推敲。希望这份拆解能帮到你,跑通之后,按自己的业务场景改参数就好。
本文还有配套的精品资源,点击获取