☰
问卷星自动随机填写脚本实战:从DOM定位到事件触发全解析
2026/10/9 13:34:37 网站建设 项目流程

简介:这是一款由Ahaochan分享的问卷星自动随机填写浏览器脚本,适合需要快速完成问卷测试、批量收集样例数据或进行功能验证的普通用户与轻度测试者。脚本目前覆盖单选题、多选题与比重题等常见题型,可在浏览器中注入运行,帮助减少手动逐题点击的重复操作。压缩包内共5个文件,核心为1个.user.js脚本文件,另附3个快捷访问网址与1个说明txt,整体仅9KB,下载后按对应浏览器安装方式配置即可使用。资源已有9957人学习下载,配套说明中包含Chrome等主流浏览器的启用指引,对不同安装环境做了简要区分。对于希望高效处理问卷填写、又不想自己编写代码的用户,这份小工具能直接提供可运行的脚本方案,并保留后续按需扩展题型的空间。

1. 问卷星自动随机填写脚本:先弄明白它到底解决什么问题

如果你在帮导师批量回收课程问卷、帮公司做市场调研样本的初步清洗,或者只是需要给一份冗长的满意度调查快速填入合理答案,你大概率想过:能不能有个脚本,帮我自动把问卷星平台上的题目随机填完?答案是能,而且实现方式远比想象中成熟。所谓问卷星自动随机填写脚本,本质是一段运行在浏览器里的 JavaScript 代码,它通过识别问卷星页面的 DOM 结构,定位每一道题的可选项(单选、多选、矩阵、量表、填空),再按预设规则注入随机值,最后触发合法的事件序列提交问卷。

这个方向的关键价值在于:它把“填写一份问卷”从人手反复点击的机械劳动,压缩成一次自动执行。对需要每天生成几十份测试样本的质检人员,对做课程设计模拟实验的学生,对需要验证自己问卷逻辑是否有漏洞的产品经理,这脚本都值得备一份。但也要清醒看到边界:它解决的是“自动填答案”这个动作,不负责绕开验证码、不负责躲避平台风控、更不负责让每一份问卷都通过人工审核。把期望放在工具上,把判断留给人自己——这是玩转这类脚本的第一条原则。

2. 问卷星页面结构拆解:写脚本前必须先懂的选择器原理

2.1 问卷星的题型 DOM 特征与定位思路

问卷星平台的题目渲染结构很固定:每一道题外层是一个div,内部由题干容器、选项容器、以及可能存在的校验层组成。以单选为例,常见结构是多个label标签包裹着input[type="radio"],每个input的name属性按题号递增(例如q1、q2、q3),选项value是数字编号。多选题通常是input[type="checkbox"],量表题则是每个评分点对应一个td里的 radio 组,矩阵题就是一组表格化的 radio。

定位策略的核心逻辑是:不依赖具体题目文本,只依赖这些稳定的结构特征。常见选择器写法是input[name^="q"](取所有名字以 q 开头的输入框),或者input[type="radio"][name="q1"]精准定位某一题。这比用文本匹配更稳,因为问卷星的题干文本可以被任意修改,但表单控件的命名规律长期稳定。我一般还会限定父级div的 class 或 id,防止选到分页组件里隐藏的控件。

2.2 XPath 与 CSS 选择器:哪个更适合随机填充场景

CSS 选择器的优势是简洁、执行快、兼容 Tampermonkey 的document.querySelectorAll。但当你需要按层级关系精确定位“第 2 页第 3 题的所有选项”时,CSS 写起来会越来越长,可读性下降。XPath 的优势正好补足这一点:它能直接写//div[contains(@class,'div_question')][7]//input[@type='radio'],按题号索引取选项,配合轴表达式还能拿到兄弟节点做联动判断。

对于随机填充这种场景,我的习惯是优先用 CSS 做“全量抓取”,再用数组索引做二次过滤。例如先拿到document.querySelectorAll('input[type="radio"]'),接着根据name属性分组,每个组内随机选一个元素。这样绕开了 XPath 的容量上限问题(有些浏览器对 65535 个节点的 XPath 结果有截断),代码也更贴近前端思维。只有在处理矩阵题时才改用 XPath,因为矩阵题的 radio 分布在多个tr里,CSS 选择器很难表达“同一行内取一个”这种语义。

2.3 事件触发:为什么不能直接给 value 赋值就完事

多数新手写的脚本会这样写:radio.checked = true; writtenInput.value = '张三';,然后发现提交时平台提示“请完成第 3 题”。原因很简单:表单控件需要派发事件,才能让问卷星的 JS 框架感知到值变了。只改 DOM 属性不会触发框架的监听器,这个坑几乎是所有自动填写脚本翻车的第一大原因。

正确做法是分两步走:先赋值,再派发事件。赋 sa 的代码片段如下。

function setRadioChecked(el) { el.click(); el.dispatchEvent(new Event('change', { bubbles: true })); }

这段代码里click()的作用是让浏览器原生行为生效,例如触发 label 的关联 effect、让 CSS 伪类匹配。dispatchEvent(new Event('change'))则是通知问卷星的 Vue 或 jQuery 监听器,值已经变了。参数bubbles: true很重要,表示事件会向上冒泡,父级容器的事件委托才能捕获到。缺少冒泡,框架监听常会失效。

对于文本框和文本域,类似地需要派发input事件。问卷星的输入框大多绑定在input事件上做实时校验,有些还监听blur事件做失焦校验。稳妥的方案是依次派发input、change、blur三个事件,顺序不能乱:先input模拟输入过程,再change模拟失焦前的值变更,最后blur触发校验钩子。

3. 用 Tampermonkey 在浏览器里部署脚本:最小可运行版本

3.1 为什么选 Tampermonkey 而不选浏览器控制台直接粘贴

浏览器控制台粘贴代码能跑,但刷新页面后脚本就丢了,页面加载前执行的需求也满足不了。Tampermonkey 这类用户脚本管理器解决的问题是:在问卷星页面 URL 匹配时自动注入脚本,并且提供跨域请求、GM 存储、脚本开关等能力。它本质是一个可持久化、可管理、可分享的 JS 运行环境。

另一个选择用 Puppeteer 跑全套浏览器自动化,适合大批量、需要并发和头部浏览器的场景。但它的部署成本高,需要 Node 环境,且占用资源大。对大多数只需要“打开问卷页面,自动填完提交”的轻量需求,Tampermonkey 是最短路径。我实际开发中常用两套并行:本地快速验证用 Tampermonkey,正式批量跑数据用 Puppeteer 挂无头浏览器。

3.2 脚本骨架与核心填写逻辑

下面的代码是问卷星自动随机填写脚本的 Tampermonkey 最小实现。它覆盖了单选、多选、填空和量表题,每道题随机选择一个合法答案。

// ==UserScript== // @name 问卷星随机填写助手 // @namespace local.playground // @version 0.1.0 // @match https://www.wjx.cn/vm/* // @grant none // ==/UserScript== (function () { 'use strict'; // 1. 等待页面渲染完成 function waitForForm(callback, timeout = 5000) { const start = Date.now(); const timer = setInterval(() => { const form = document.querySelector('#question-content'); if (form || Date.now() - start > timeout) { clearInterval(timer); callback(form); } }, 200); } // 2. 处理单选题组:每组随机选一个 function randomPickRadio(name) { const radios = document.querySelectorAll(`input[type="radio"][name="${name}"]`); if (radios.length === 0) return; const idx = Math.floor(Math.random() * radios.length); const el = radios[idx]; el.click(); el.dispatchEvent(new Event('change', { bubbles: true })); } // 3. 处理多选题组:按概率决定是否全选/选多少 function randomPickCheckbox(name, maxCount) { const boxes = document.querySelectorAll(`input[type="checkbox"][name="${name}"]`); if (boxes.length === 0) return; const count = 1 + Math.floor(Math.random() * Math.min(maxCount || 2, boxes.length)); const shuffled = Array.from(boxes).sort(() => Math.random() - 0.5); shuffled.slice(0, count).forEach((el) => { el.click(); el.dispatchEvent(new Event('change', { bubbles: true })); }); } // 4. 处理填空:按题干关键词填充不同内容 function fillTextInput(inputEl) { const questionDiv = inputEl.closest('.div_question'); const text = questionDiv ? questionDiv.innerText : ''; let val = '测试数据'; if (text.includes('姓名')) val = '张三'; else if (text.includes('邮箱')) val = 'dev@example.com'; else if (text.includes('手机')) val = '13800138000'; else if (text.includes('公司')) val = '某科技有限公司'; inputEl.value = val; inputEl.dispatchEvent(new Event('input', { bubbles: true })); inputEl.dispatchEvent(new Event('change', { bubbles: true })); } // 5. 主流程:先定位所有题,再逐个填 function run() { document.querySelectorAll('.div_question').forEach((qDiv) => { const radioInputs = qDiv.querySelectorAll('input[type="radio"]'); const checkboxInputs = qDiv.querySelectorAll('input[type="checkbox"]'); const textInputs = qDiv.querySelectorAll('input[type="text"], textarea'); if (radioInputs.length > 1) { randomPickRadio(radioInputs[0].name); } else if (checkboxInputs.length > 0) { randomPickCheckbox(checkboxInputs[0].name, 2); } else if (textInputs.length > 0) { fillTextInput(textInputs[0]); } }); // 6. 点击提交按钮 const submitBtn = document.querySelector('#submit_button'); if (submitBtn) { submitBtn.click(); } } waitForForm(() => { run(); }); })();

代码注释里的关键点:waitForForm用轮询而不是DOMContentLoaded,因为问卷星的部分题型是异步加载的,直接监听 DOM 就绪可能拿到空节点。randomPickRadio每次都重新查 DOM,而不是缓存集合,这是因为点击某些选项后框架会重建局部 DOM,导致旧引用失效。fillTextInput里的关键词命中规则是折中方案,没有关键词时填默认值,避免触发必填校验。

参数调优方面,timeout设为 5000 毫秒适用于多数宽带环境;如果你的问卷页面有大量矩阵题,建议提高到 8000。多选题的maxCount参数控制最多选几个,我通常根据题目是“最多选 2 项”还是“最多选 3 项”来改。运行前记得在 Tampermonkey 面板里确认 @match 的 URL 规则与你的问卷链接匹配,否则脚本完全不会执行。

3.3 验证脚本是否生效的三步排查法

部署完脚本后,先不要急着跑问卷,按下面三步验证。第一步,打开问卷页面的开发者工具,在 Console 面板执行localStorage.getItem('_qjx_token'),如果返回 null 属于正常,重点是第二步:查看 Tampermonkey 的脚本运行状态图标,确认它变成了红色实心状态,表示已注入当前页面。第三步是手动在 Console 里调用randomPickRadio('q1'),看第 1 题的选项是否被选中。

如果点击后选项没有变化,先看 Console 有没有红色的 JS 报错。最常见的错误是el.closest is not a function,这是因为某些旧版浏览器不支持closest方法,需要换成el.parentElement.closest或者自己写循环。另一个常见错误是submit_button的 id 实际是btnSubmit,不同模板的按钮命名不同,需要打开 Elements 面板确认真实 id。

4. 随机策略与反检测:别让你的数据一眼假

4.1 均匀随机与加权随机:不同场景的答案分布设计

最基本的随机是Math.floor(Math.random() * n),它让每个选项命中概率完全相等。这种策略在“只要一份看起来填完了”的场景够用,但当你需要模拟真实用户分布时,均匀随机反而显得假。比如一款产品满意度调查,如果 5 档评分每档都 20% 的人选,这份数据拿去分析会让评审直接怀疑是机器人填的。真实市场的满意度通常呈偏态分布(高分居多,中间次之,低分最少)。

加权随机可以解决这个问题。常见做法是先定义权重数组,比如const weights = [5, 15, 30, 35, 15],然后计算累计权重,生成一个 0~100 的随机数,映射到具体选项。实现代码很简短:

function weightedRandomIndex(weights) { const total = weights.reduce((a, b) => a + b, 0); let rand = Math.random() * total; for (let i = 0; i < weights.length; i++) { rand -= weights[i]; if (rand < 0) return i; } return weights.length - 1; }

这段代码的思路是把权重当成线段长度,随机数落在哪一段就选哪个选项。权重数组的取值依据应该是你要模拟的人群特征:如果是普通消费者,高分偏多;如果是售后反馈,低分偏多;如果没把握,保持均匀即可。注意权重总和不必是 100,脚本会自动归一化,这让调整更省心。

4.2 时间延迟与输入节奏:模拟人类而非机械连点

问卷星服务端做异常判定时,一个很简单的指标是单份问卷的提交耗时。真人填一份 30 题的量表通常需要 60~120 秒,脚本 1 秒提交完就异常显眼。所以随机延迟是刚需,不能省。

我常用的策略有两层。第一层是每道题间的思考间隔,用setTimeout随机延迟 800~2500 毫秒;第二层是提交前的犹豫时间,随机延迟 2~5 秒。代码实现时可以用一个async队列,让每道题的处理都await一段随机时长:

function sleep(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } async function fillAllQuestions() { const questions = document.querySelectorAll('.div_question'); for (const q of questions) { handleSingleQuestion(q); await sleep(800 + Math.random() * 1700); } await sleep(2000 + Math.random() * 3000); document.querySelector('#submit_button')?.click(); }

这里的handleSingleQuestion是上一节代码的抽取函数。延迟时间也可以做成可配置项:const delayRange = { min: 800, max: 2500 },方便快速调整。还有一个细节:某些矩阵题内部有多行,行与行之间也应该有 300~600 毫秒的间隔,否则整页瞬间填满会触发浏览器的自动填充检测。

4.3 模拟滚动与真实交互:降低被识别的概率

脚本直接执行click()看起来行为正常,但有经验的平台风控可以检查鼠标轨迹:真人操作时鼠标会从页面顶部逐步往下滚,点击位置集中在选项附近,而脚本的点击通常是瞬发的、无轨迹的。解决方法是在每次点击前,先滚动到该元素位置,再触发mousemove事件。

function humanClick(el) { el.scrollIntoView({ behavior: 'smooth', block: 'center' }); const rect = el.getBoundingClientRect(); const x = rect.left + Math.random() * rect.width; const y = rect.top + Math.random() * rect.height; const evt = new MouseEvent('mousemove', { clientX: x, clientY: y, bubbles: true }); document.dispatchEvent(evt); el.click(); }

scrollIntoView会触发浏览器的滚动监听,让页面滚动条产生真实位移。getBoundingClientRect拿到元素位置后,在元素矩形内部随机取一个点派发mousemove,模拟鼠标挪到该位置。注意坐标需要加上页面的滚动偏移,但这里用 clientX/clientY 配合mousemove到 document 就足够了,不必追求像素级准确。

5. 常见问题与避坑指南:问卷星脚本的五个高频翻车点

5.1 提交时提示“还有未完成题目”或“请完成第 X 题”

现象:脚本执行完,点击提交后问卷星提示有未完成的必答题。原因多数是题目居然是动态渲染的:页面加载完成后再通过 Ajax 请求注入选项,而脚本在第一次 DOM 轮询时只拿到了题干框架,真实选项还没渲染出来。解决方法是增加二次等待,在waitForForm回调里再轮询一次,确认所有input元素数量稳定后再开始填写。

function waitForInputsStable(timeout = 3000) { return new Promise((resolve) => { let lastCount = -1; let stableDuration = 0; const start = Date.now(); const timer = setInterval(() => { const count = document.querySelectorAll('input, textarea').length; if (count === lastCount) { stableDuration += 200; } else { lastCount = count; stableDuration = 0; } if (stableDuration >= 600 || Date.now() - start > timeout) { clearInterval(timer); resolve(); } }, 200); }); }

这段代码的核心是连续 600 毫秒内 DOM 数量不再变化,才认为渲染结束。对包含矩阵题或分页的问卷,这个策略远比固定setTimeout可靠,因为它天然免疫慢网速导致的延迟渲染。

5.2 脚本运行后选项被自动清空

现象:脚本勾选的选项在 1~2 秒后全部恢复为未选中状态。这是问卷星自作聪明的重置逻辑在作怪:当某个题目的选项配置被后端动态更新,或者题目属于“配额控制”类型(填满后自动改变可选状态),框架会重建整个题目的 DOM,之前脚本设置的选中值全被丢弃。解决方向是不要只填一次,而是填完所有题后重新遍历一次,把空选项补上。

补填逻辑要注意两点:必须先判断该题是否还允许填写(有的题目会因配额超限自动禁用),且补填时的选择不能与第一次随机相同,否则前后矛盾容易被看出是脚本。我实际开发中的做法是记录每道题选中的值,补填时从剩余选项里再随机一次。

5.3 提交按钮点击无效,但手动点击正常

现象:脚本调用的submit_button.click()没有触发提交,而手动点同一个按钮没问题。原因是按钮绑定了mousedown事件,click()不会触发 mousedown,导致问卷星认为没有按下按钮。解决方法是同时派发mousedown和mouseup事件,或者改用dispatchEvent(new MouseEvent('click', { bubbles: true }))。我一般两种都做:

function clickSubmit(btn) { btn.dispatchEvent(new MouseEvent('mousedown', { bubbles: true })); btn.dispatchEvent(new MouseEvent('mouseup', { bubbles: true })); btn.dispatchEvent(new MouseEvent('click', { bubbles: true })); }

注意MouseEvent构造器里的button参数默认为 0,代表主键。有些平台会检查event.isTrusted,这个属性无法通过脚本伪造,所以这招只适合没有校验isTrusted的页面。遇到校验严格的情况,可以退一步用document.elementFromPoint配合dispatchEvent派发坐标事件,但这属于极端情况,大多数问卷星模板不会卡这一步。

5.4 验证码弹出阻塞提交

现象:填写和点击提交都成功,但页面弹出验证码,需要手动拖动。这是平台风控的常见动作,触发原因通常有三类:IP 在短时间内提交多份问卷、浏览器指纹异常(例如无头浏览器特征)、单份问卷提交耗时异常短。对验证码本身没有绝对可靠的脚本解法,我的策略是“预防为主”:把提交间隔拉长到 10 分钟以上、关闭无头模式、给 User-Agent 设成正常浏览器的值、修改navigator.webdriver属性为 undefined。

修改navigator.webdriver的代码要在脚本最开始执行,且必须用Object.defineProperty方式覆写:

Object.defineProperty(navigator, 'webdriver', { get: () => undefined, });

这个改动只对当前页面生效,刷新后需要重新设置。如果验证码还是出现,唯一可行的手动进补是人工过一下验证码后继续,脚本自身不应强求或尝试调用滑块打码服务,那样可能触犯平台更重的风控。

5.5 IP 被限制:换网络或增加分发的必要性

现象:连续提交 5 份以上问卷后,第六份被秒拒或直接提示“请求过于频繁”。这是 IP 维度的限流。解决方式从轻到重有三个等级:第一,拉长间隔至单人填写耗时;第二,使用多家网络通道轮流提交;第三,把脚本改造成可分布式执行,配合代理池。我实际做批量课时,用的是第一级加第二级的组合:每份问卷间隔随机 8~15 分钟,且轮换手机热点和办公网络。代理池的搭建成本高、可用性受限于资源渠道,只推荐给确实需要每天产出几百份数据的人。

6. 进阶技巧:让脚本支持批量问卷与自定义逻辑

当脚本能在单份问卷上稳定跑通后,下一步是让它接收参数化配置,摆脱每次改代码的尴尬。我一般会引入一个配置对象,放在脚本最前面:

const config = { delayRange: { min: 800, max: 2500 }, submitDelay: { min: 3000, max: 6000 }, weights: null, // 如果不为 null,则使用加权随机 fillText: { default: '测试数据', keywordMap: { '姓名': '张三', '邮箱': 'dev@example.com', }, }, pageUrlPattern: window.location.href, };

调用处全部改用配置里的值,这样换成另一份问卷时需要调的只是配置对象,而不是逻辑代码。比如新问卷的“工作年限”题可能期望偏年轻化,就把weights设置成[10, 20, 40, 20, 5, 5],不用改任何随机算法。

批量场景还必须处理查验环节。我的习惯是给每份提交过的问卷记录一个日志,用GM_setValue存储在脚本本地。日志字段包含时间戳、当前页面 URL、填写的题目数量、提交是否成功、以及可选的提交 ID(从成功跳转后的 URL 参数里提取)。这样即使平台没有立即显示“提交成功”,也能靠这些数据排查哪份出问题。

验证脚本是否真正提交成功的技巧,是监听提交请求的响应。问卷星提交用的是 Ajax POST,响应 JSON 里有个字段标识提交状态。在 Console 里debugger一下网络面板就能看清楚结构。脚本侧可以用MutationObserver监听页面跳转或提示层的出现,但更快的方式是直接给submit_button绑定一个监听,在点击后 2 秒检查页面是否出现了“感谢您参与问卷”的提示文案。

最后分享一个实际开发中的习惯:无论脚本运行得多顺,我都保留一份手动填写的备用方案。因为问卷星页面结构升级会一次性让所有基于 DOM 的选择器失效,脚本能快速修复当然好,修不了时手动提交也不至于断掉整个工作流。有一次我因为页面模板升级,脚本连续三天都是悄悄 failed,落后了许多进度,从那以后宁可麻烦点也要在批量前先跑两轮人工验证。这类脚本的本质是替你做重复劳动,不是替你理解问卷逻辑,保持对工具结果的人工抽检习惯,才是走得更远的保障。

希望这些经验对你有实质帮助。

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

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

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

立即咨询