☰
问卷星自动填写脚本实战:加权随机与真人模拟
2026/10/17 2:35:09 网站建设 项目流程

简介:一份针对问卷星自动填写需求的浏览器端js插件,面向需要批量处理问卷、快速完成表单填写的用户。插件支持单选题、多选题、比重题等多种题型,可随机生成答案并自动提交,适合课堂测验、市场调研等场景。压缩包共5个文件,以.user.js脚本为核心,另附txt说明文档及若干url快捷链接,方便用户快速了解脚本用法和扩展工具,包体仅9KB,轻量易部署。该资源已有9962人学习下载,属于较为流行的实用工具。通过这份资源,用户可直接获得可运行的自动填写脚本,省去手动编写代码的麻烦;同时附带安装指引与相关站点入口,有助于快速在Chrome等浏览器中启用,并可根据实际题型进行简单调整,提高填写效率。整体上是一份免配置、开箱即用的问卷填写辅助方案。

1. 问卷星自动填写脚本:先搞明白它到底做了什么

开发问卷的时候,最耗时间的不是设计题目,而是把一份问卷反复填几十遍来验证数据能不能正常入库。单选、多选、矩阵、排序、提交前的必填校验,每一处都要有足够多样本的填答数据,光靠手点效率太低。问卷星自动随机填写脚本,就是解决这个重复劳动的网络软件:它以 JS 插件的方式注入问卷页面,自动识别题目类型,用加权随机策略生成答案,再按真人节奏一题题填完、校验、提交。适合两类人:一类是做问卷系统联调的开发者,另一类是需要为表单收集测试样本的运营或数据分析从业者。

2. 搭建运行环境:油猴脚本与三类触发方式

2.1 前置条件:浏览器与脚本管理器

这份资源以用户脚本的形式提供,本质是一个带元数据头的 JS 文件,要依赖浏览器扩展中的脚本管理器来运行。安装逻辑很简单:先在浏览器扩展市场里找到脚本管理器类插件,习惯上大家直接叫油猴;装好后新建一个用户脚本,把资源里的 js 内容整体粘贴进去,保存;再打开问卷页,脚本就会按头部规则自动注入。

下面是最小可用的脚本骨架,先确认环境通不通:

// ==UserScript== // @name 问卷星随机填写助手 // @namespace local.test // @version 0.9 // @description 问卷联调用的自动填答脚本 // @match https://your-survey-domain.com/* // @run-at document-idle // @grant none // ==/UserScript== (function () { 'use strict'; console.log('[问卷助手] 初始化完成,等待表单渲染'); })();

代码逻辑:脚本管理器在页面document-idle阶段执行这段 IIFE,console.log输出一行日志,用来确认注入是否成功。

参数说明:@match是脚本的生效范围,请改成你实际面对的问卷页域名,用*做通配时要谨慎,范围越窄越不容易污染其他网站;@run-at document-idle表示等页面主文档解析完再执行,避免脚本跑得太早拿不到 DOM;@grant none表示不申请额外的 GM_API,保持脚本在页面上下文内直接操作,权限模型最简单。如果后续要跨域读取题目配置,再考虑换@grant GM_xmlhttpRequest。

2.2 首次运行验证:从控制台确认脚本注入成功

问卷星的题目结构是异步渲染的,直接在执行阶段找表单元素经常会扑空。常见做法是轮询等待表单容器出现,或者用 MutationObserver 监听 DOM 插入。这里给一个轮询版本,逻辑直观,参数也好调:

(function () { 'use strict'; const CONFIG = { pollInterval: 500, maxWait: 15000, triggerMode: 'manual' }; function waitForForm(callback) { const start = Date.now(); const timer = setInterval(() => { const form = document.querySelector('#questionForm'); if (form) { clearInterval(timer); callback(form); } else if (Date.now() - start > CONFIG.maxWait) { clearInterval(timer); console.warn('[问卷助手] 等待表单超时,请检查是否在问卷详情页'); } }, CONFIG.pollInterval); } function createButton() { const btn = document.createElement('button'); btn.textContent = '随机填写'; btn.style.cssText = 'position:fixed;right:20px;bottom:20px;z-index:99999;'; btn.addEventListener('click', () => console.log('[问卷助手] 手动触发填答')); document.body.appendChild(btn); } waitForForm(() => { createButton(); console.log('[问卷助手] 表单已就绪,初始化完成'); }); })();

代码逻辑:waitForForm每 500 毫秒查一次#questionForm,查到就把控制权交给回调;查了 15 秒还没有,判定为不在问卷页,直接告警退出,避免无意义的死循环。

参数说明:pollInterval是轮询间隔,500 毫秒对绝大多数问卷页够用,网络慢的可以调到 800;maxWait是超时上限,15 秒是经验值,问卷星页面加载慢的时候可以放到 20 秒;triggerMode目前只是配置占位,真正触发方式在 2.3 节用。

验证方法:打开一份真实问卷页,按 F12 切到 Console,能看到[问卷助手] 表单已就绪,初始化完成,同时页面右下角出现一个悬浮按钮,说明环境已经跑通。如果 Console 里一行日志都没有,先回查@match规则是否覆盖了当前域名,再确认脚本管理器面板里的启用开关。

2.3 触发方式:手动按钮、自动载入与定时执行

脚本跑通之后,接下来要决定什么时候开始填答。三种触发方式对应三类使用场景:手动按钮适合调试,先看清楚页面结构再决定要不要填;自动载入适合批量打开多个标签页,统一开始填答;定时执行适合配合问卷的发布时间,到点自动跑第一份。

const CONFIG = { triggerMode: 'manual', // manual | auto | schedule scheduleTime: '2025-01-01T10:00:00', // triggerMode 为 schedule 时生效 minDelay: 1500, // 每题最短停留时间,单位毫秒 maxDelay: 4000, // 每题最长停留时间,单位毫秒 submitAfterDone: true // 全部填完后是否自动提交 };

代码逻辑:所有运行参数集中在一个配置对象里,改参数不需要动逻辑代码。scheduleTime只在triggerMode为schedule时参与计算,脚本内部用Date.parse(scheduleTime) - Date.now()算出等待时长。

参数说明:minDelay和maxDelay控制填答节奏,取值参考真人阅读速度,1.5 到 4 秒是常见区间;不要设置成固定值,否则多份问卷的填答耗时完全一致,反而显得机械。submitAfterDone建议先设为false跑几轮,确认填答结果正确后再开启自动提交。

参数可选值作用
triggerModemanual / auto / schedule决定填答启动方式
scheduleTimeISO 格式时间字符串定时触发时刻
minDelay / maxDelay毫秒每题停留随机区间
submitAfterDonetrue / false是否自动提交问卷

使用建议:手动模式永远是第一选择,新脚本或者新问卷模板第一次跑,必须用手动触发观察一轮;自动和定时模式适合确认无误后的批量场景,但也不要在正式环境高频使用,短时间内同 IP 大量提交很容易触发平台的异常检测,这个在第 4 章还会提到。

3. 随机填写逻辑拆解:加权策略、时间模拟与边界参数

3.1 加权随机:为什么全随机反而容易翻车

先解决最核心的问题:答案怎么随机。初版脚本最简单粗暴的做法是Math.random()均匀随机。跑下来会发现问题:均匀随机对单选和下拉框还能接受,但对量表题和矩阵题,答案分布会完全均匀地散在两端,这种模式恰恰是机填最明显的特征。真实用户在填量表时,答案会集中在中间偏积极或偏消极的区间,不会每个极端值都等概率出现。

所以这里引入加权随机。给每个选项配一个权重,权重越大被选中的概率越高:

function weightedPick(options, weights) { const total = weights.reduce((a, b) => a + b, 0); let rand = Math.random() * total; for (let i = 0; i < options.length; i++) { rand -= weights[i]; if (rand <= 0) return options[i]; } return options[options.length - 1]; } // 示例:量表题 5 个选项,中间权重最大 const options = ['非常不同意', '不同意', '一般', '同意', '非常同意']; const weights = [1, 2, 4, 2, 1]; const picked = weightedPick(options, weights);

代码逻辑:先求权重总和,生成一个0到总和的随机数,然后依次减掉每个选项的权重,减到小于等于0时返回当前选项。权重越大,占用的区间越长,命中概率越高。

参数说明:weights数组必须和options一一对应,数量不一致会直接返回最后一个选项,这点要小心;权重不需要归一化,写成[1, 2, 4, 2, 1]和[2, 4, 8, 4, 2]效果完全一样。实际配置时,建议把量表题的权重写成两边小中间大,单选和下拉框继续保持均匀随机。

还要补一个边界处理:当多个选项权重相同时,weightedPick退化为均匀抽样,这没问题;但如果某个题目要求填“是/否”且业务上希望 80% 填“否”,就把权重设成[1, 4],这种“有倾向的随机”比纯随机更接近真实分布。

3.2 时间模拟:把填答节奏伪装成真人的三个技巧

随机选答案只是第一步,填答节奏才是脚本像不像人的关键。早期测试阶段我踩过坑:固定延迟 2 秒填一道题,结果整份问卷的填答时间曲线完全水平,一看就是脚本。后来改成随机区间延迟,并且每道题填答前先滚动到题目位置,曲线才正常。

function sleep(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } async function fillWithHumanPace(questions) { for (const q of questions) { const delay = CONFIG.minDelay + Math.random() * (CONFIG.maxDelay - CONFIG.minDelay); q.scrollIntoView({ behavior: 'smooth', block: 'center' }); await sleep(delay); fillQuestion(q); } }

代码逻辑:fillWithHumanPace遍历题目容器,先平滑滚动到题目在页面中间位置,再等一个随机延迟,最后执行填答。async/await串起整个过程,后续想插入“填到一半暂停”或者“超过 N 秒手动接管”的逻辑都很方便。

参数说明:CONFIG.minDelay和CONFIG.maxDelay的差值决定了节奏变化的幅度,差值太小会显得手速过于稳定,差值太大又会让完成时间不可控。经验值是差值 2000 毫秒左右,整卷完成时间控制在 60 到 90 秒区间。用scrollIntoView的behavior: 'smooth'而不是直接跳转,也有助于减少页面检测特征。

这里有个实现细节经常被忽略:scrollIntoView是异步滚动,滚动动画没结束就点选,个别浏览器会点不到。所以代码里把await sleep(delay)放在滚动之后、填答之前,就是给滚动留出时间。

3.3 题型分发与兜底处理:单选、多选、矩阵、排序、填空

问卷星的题型基本是定型的:单选、多选、下拉、矩阵、排序、填空。脚本内部用一个分发器,按题目容器特征决定走哪个处理函数。识别题型这里,有一条硬经验:不要依赖 class 名,要用 DOM 结构和输入控件类型来判断,因为 class 名会随平台版本升级变化,而结构相对稳定。

function fillQuestion(questionEl) { const type = detectType(questionEl); switch (type) { case 'radio': pickRadio(questionEl); break; case 'checkbox': pickCheckbox(questionEl); break; case 'select': pickSelect(questionEl); break; case 'matrix': pickMatrix(questionEl); break; case 'sort': pickSort(questionEl); break; default: fillText(questionEl); } } function detectType(questionEl) { if (questionEl.querySelector('table')) return 'matrix'; if (questionEl.querySelector('select')) return 'select'; if (questionEl.querySelectorAll('input[type="radio"]').length > 0) return 'radio'; if (questionEl.querySelectorAll('input[type="checkbox"]').length > 0) return 'checkbox'; if (questionEl.querySelector('textarea, input[type="text"]')) return 'text'; return 'unknown'; }

代码逻辑:detectType按优先级判断,先找有没有表格结构,有就是矩阵题;再找下拉框和单选、复选控件。矩阵题优先是因为它内部也包含 radio,不提前拦截会被误判成普通单选。

参数说明:这里的分发逻辑是基础版。矩阵题要额外按行处理,每一行是一个独立的 radio 组,选择策略可以复用 3.1 节的weightedPick;排序题则是把备选项数组做一次洗牌,再按顺序依次点击。这些子函数在资源包里都是独立的,直接按需替换即可。

兜底逻辑是脚本稳定性的最后一道防线:第一轮填完后,还要有一个“补漏”环节,扫描所有带必填标记的题目容器,检查里面有没有已选中的控件,没有就重新填一次。实现上就是遍历容器、检查checked状态,把缺失的题目收集到一个数组里再重试。这个函数在 4.3 节会详细给出,先记住结论:无论分发逻辑多完善,提交前必须有一遍全量校验。

4. 避坑排查:脚本失效、点选失灵与验证码暂停的五个问题

这一章列出的五个问题都是实际跑问卷页时踩过的坑,每条按现象、原因、解决三步写,建议按顺序排查,前三个问题往往一次能杀掉 80% 的故障。

4.1 脚本完全没反应:Console 没有任何输出

现象:打开问卷页,Console 里连初始化日志都没有,页面右下角也没有悬浮按钮。

原因:最常见的是@match规则没覆盖当前问卷域名,脚本管理器认为当前页面不在生效范围;其次是脚本文件保存失败或者被管理器禁用,后者概率不高但容易忽略。

解决:先打开脚本管理器面板,确认脚本处于启用状态;再把@match临时改成https://*/*做一次验证,能出日志说明就是匹配范围问题,确认后把规则收窄回实际域名。注意通配符尽量精确到问卷服务子域,避免脚本在无关页面也被注入。

4.2 单选框点选无效:click 没触发联动

现象:脚本执行后,页面上某些单选题的选中态看起来是正常的,但提交后数据为空,或者后续跳题逻辑没有触发。

原因:问卷星部分题目的选项文本是包在label里的,事件绑定挂在label上。脚本直接对input调用.click()时,事件可能没有冒泡到外层,或者平台用了自定义事件,原生 click 无法触发联动。

解决:不要只操作input.checked,改为对选项容器派发完整的鼠标事件:

function clickOption(inputEl) { const label = inputEl.closest('label') || inputEl.parentElement; label.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true })); }

代码逻辑:先找到输入框最近的label容器,再派发一个可冒泡的点击事件。bubbles: true让事件沿 DOM 树向上传播,平台侧的联动监听才能收到。

参数说明:如果页面里没有label容器,就退回到inputEl.parentElement。个别版本需要mousedown和mouseup一起派发,可以在这段逻辑里加一个for循环连发三个事件,保证覆盖。

4.3 必填项漏填:提交被拦截但脚本没报错

现象:脚本日志显示所有题目都填完了,提交时平台仍然弹出“第 5 题未作答”,且每次都不同题。

原因:问卷页面存在分段渲染,某些题目容器是在滚动到可视区域后才加载选项的。脚本第一轮遍历时这些题目还是空壳,遍历结束后才渲染出实际选项,结果就是漏填。

解决:填答结束后追加一遍全量校验,把缺失题目捞出来重填:

function verifyRequired(container) { const missing = []; container.querySelectorAll('[data-required="true"]').forEach((item) => { const hasValue = item.querySelector('input:checked') || item.querySelector('textarea:valid'); if (!hasValue) { missing.push(item); } }); return missing; } const missed = verifyRequired(formContainer); if (missed.length > 0) { for (const item of missed) { item.scrollIntoView({ block: 'center' }); fillQuestion(item); } const retry = verifyRequired(formContainer); if (retry.length > 0) { console.warn('[问卷助手] 仍有必填题未填,重试后自动终止'); } }

代码逻辑:verifyRequired遍历所有带必填标记的题目容器,有选中的 radio、checkbox 或有内容的 textarea 都算通过;失败则滚动到该题重新填,再验一遍。两次校验都失败就输出警告并终止。

参数说明:这里的[data-required="true"]是示例写法,实际问卷页面的必填标记可能叫class="required"或别的属性,务必先用 DevTools 检查一遍真实结构再填。这类细节属于页面结构耦合,脚本发布后如果平台改版,这里是最先需要更新的部分。

4.4 验证码把脚本卡死:填到一半停住不动

现象:脚本一路填到第 3 题左右,突然没有后续动作了,页面中间出现拖动验证码。

原因:问卷开启了智能验证,填答行为异常时触发。验证码是 iframe 嵌入的,与问卷页不同源,@grant none模式下脚本无法直接操作 iframe 内部元素,逻辑进入死等状态。

解决:不要尝试自动过验证码,成本高且容易被风控。脚本侧应该检测验证码容器出现,然后主动暂停填答,等待人工处理:

function detectCaptcha() { return document.querySelector('.captcha-container, #captcha, iframe[src*="captcha"]'); } // 在每道题填答前检查一次 function checkCaptchaPause() { if (detectCaptcha()) { console.warn('[问卷助手] 检测到验证码,暂停填答等待人工'); return new Promise((resolve) => { const timer = setInterval(() => { if (!detectCaptcha()) { clearInterval(timer); resolve(); } }, 1000); }); } }

代码逻辑:checkCaptchaPause返回一个 Promise,验证码在页面上时进入 1 秒一次的轮询,直到人工处理完验证码消失再继续填答。

参数说明:detectCaptcha的选择器同样要根据实际页面调整,重点是把这个检测逻辑插在每道题填答之前,而不是整份问卷开头,因为验证码多半是中途才弹出来的。

4.5 悬浮按钮能点,但提交后答案对不上

现象:按钮正常出现,日志也显示每道题都填了,但后台回收的数据里部分答案串行,比如第 3 题的答案跑到了第 5 题。

原因:脚本按输入控件类型做了全局遍历,而页面渲染时会给部分题目生成隐藏的辅助 input,全局遍历把这些隐藏字段也当成有效控件,取到了错误的值。

解决:遍历单位必须从“控件”上升到“题目容器”。每个题目容器内部再做局部查找,并且只看可视控件:

function collectAnswers() { const answers = {}; document.querySelectorAll('.question-container').forEach((q) => { const qid = q.dataset.questionId || q.getAttribute('data-id'); const checked = q.querySelector('input:checked'); const textVal = q.querySelector('textarea:valid'); answers[qid] = checked ? checked.value : (textVal ? textVal.value : '未填'); }); console.table(answers); return answers; }

代码逻辑:通过题目容器拿到题目编号,在容器范围内查选中状态,保证答案归属正确。console.table输出一行一题,方便对照。

参数说明:.question-container和>const SURVEY_CONFIGS = [ { name: '登录体验问卷', urlKeyword: 'login-survey', minDelay: 1800, maxDelay: 3500, preferenceMap: { '您的职业': '3' } }, { name: '新版首页反馈', urlKeyword: 'homepage-feedback', minDelay: 2500, maxDelay: 5000, preferenceMap: {} } ]; const current = SURVEY_CONFIGS.find((item) => window.location.href.includes(item.urlKeyword) );

代码逻辑:按 URL 关键字匹配当前问卷,命中后取对应配置。preferenceMap是“偏好覆盖表”,用来指定某些题目固定选某个选项,其余题目保持随机策略。

参数说明:urlKeyword取 URL 里最稳定的片段,别用带参数的部分;preferenceMap的键是题目文本或题目编号,值是该题要固定的选项值。比如“您的职业”固定选“3”,脚本检测到该题时跳过随机,直接按映射填入,其他题目不受影响。

字段类型含义
namestring问卷别名,用于日志输出
urlKeywordstringURL 匹配关键词
minDelay / maxDelaynumber该问卷的填答节奏区间
preferenceMapobject题目级固定答案映射

5.2 提交前的自校验:console.table 输出与必填核对

脚本跑得再稳,提交前也必须有一道人眼确认的关卡。我的习惯是让脚本在提交前把每道题的最终状态输出成表格,手动扫一眼再决定是否放行。

function validateBeforeSubmit() { const answers = collectAnswers(); const emptyKeys = Object.keys(answers).filter((k) => answers[k] === '未填'); if (emptyKeys.length > 0) { console.warn('[问卷助手] 以下题目未填充:', emptyKeys); return false; } const captchaVisible = detectCaptcha(); if (captchaVisible) { console.warn('[问卷助手] 验证码仍可见,取消自动提交'); return false; } console.table(answers); return true; }

代码逻辑:提交按钮被点击前,先调用validateBeforeSubmit,检查是否存在“未填”项、验证码是否仍然可见,全部通过才执行提交。空题列表和最终答案表都会打到 Console 上。

参数说明:这个函数复用了 4.5 节的collectAnswers和 4.4 节的detectCaptcha,三个函数配合使用,覆盖了“答案归属、必填完整性、验证码阻断”三个最常见的失败场景。如果问卷里有手机号、邮箱这类格式要求,再在validateBeforeSubmit里追加对应的正则校验即可。

这套脚本我用下来最大的教训就是:拿到任何新问卷模板,先不要挂自动批处理,而是打开 Console 手动触发一次填答,对照console.table的输出检查题目是否正确对应。从那以后,我每次拿到新的问卷资源都强制走一遍这个流程,脚本翻车的概率降了大半。希望这份脚本和配置模板能帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询