ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

问卷星自动随机填写脚本:DOM操作与随机策略详解

问卷星自动随机填写脚本:DOM操作与随机策略详解 简介问卷星自动随机填写脚本插件是一款面向问卷设计者、市场调研人员及需要批量模拟填写场景的浏览器扩展工具主要解决问卷填写耗时、重复劳动的问题。它基于用户脚本运行支持单选题、多选题、比重题等常见题型能够自动生成随机答案并快速完成填写。压缩包内共五个文件包含一个核心脚本文件、一个说明文本及三个相关网址链接整体大小仅9KB轻量易部署。目前已有9956人学习下载在同类工具中较受欢迎。用户可拿到可直接安装的用户脚本并借助随包的文本说明和快捷链接快速配置到Chrome等浏览器无需编程基础即可使用脚本本身结构清晰也方便后续根据问卷变化调整题型或答案规则适合日常问卷测试与调研场景。1. 问卷星自动随机填写脚本把“手动刷问卷”变成可复现的js脚本很多做过问卷测试、表单演示或者批量数据录入的人都有过这种经历问卷星上一道题一道题地人工点选几十道题下来手酸眼花还得保证每次填的答案不一样。有人靠鼠标连点器瞎点但连点器解决不了“随机选”“按题型映射”“跳题逻辑”这些实际问题。问卷星自动随机填写脚本就是一套用 js 写的浏览器端脚本它直接操作页面 DOM按题型随机勾选、随机填文本把一份问卷从人工四五分钟压到脚本两三秒跑完。它既不是爬虫也不是什么黑科技本质上是把表单交互自动化。适合给问卷做流程演示、功能测试、压力预填这类场景的开发者也适合想知道“别人家的问卷脚本到底怎么绕过点击限制”的人拿去做技术拆解。2. 脚本原理与随机策略从DOM定位到防呆设计2.1 问卷星表单的DOM结构与节点定位问卷星页面渲染出来的结构其实比很多人想象的要规整。每一道题都包在一个div容器里常见的类名是.div_question题目文本在.div_title里选项则是被label标签包裹的单选框或复选框。这里有个关键点问卷星为了样式统一通常会把原生input隐藏掉真正显示的是label里的文字和伪元素。也就是说如果你直接input.click()页面上的视觉状态可能变了但问卷星的监听逻辑未必认。所以脚本第一步要做的是拿准节点我给你一段基础定位代码// 定位所有题目容器 const questions document.querySelectorAll(.div_question); console.log([问卷脚本] 找到题目数量:, questions.length); // 按题目索引取选项 function getOptions(questionEl) { // 每个题目内的 label 就是可点选项 return questionEl.querySelectorAll(label); } // 演示输出第一道题的选项文本 const firstQ questions[0]; if (firstQ) { const opts getOptions(firstQ); opts.forEach((opt, i) { console.log(选项${i}:, opt.innerText.trim()); }); }逻辑说明先用querySelectorAll(.div_question)拿到全部题目的类数组然后针对每个题目容器再用querySelectorAll(label)去找该题下的选项。为什么不用input前面说了隐藏 input 会被 CSS 覆盖而且部分题型比如矩阵量表的 input 不直接挂在题目容器下面抓label更稳定。参数方面.div_question是问卷星 PC 端的典型类名但这份脚本我见过两种变体新版页面用fieldset标签部分嵌入页用.div_question建议代码里做一个兼容判断function findQuestionRoot() { return document.querySelector(.div_question) ? .div_question : fieldset; }这样做的好处是你拿到脚本后不用因为页面改版就立刻改全逻辑只动根容器选择器就够了。2.2 随机填入策略权重、置信区间与必选规则很多人写随机问卷脚本上来就Math.random()点一下这太想当然了。真实问卷里有两类约束你必须处理第一类是“必选几项”比如题目限定“最多选3项”第二类是“跳题逻辑”比如选了“否”后面几道题直接隐藏。如果不处理这些脚本填出来的问卷十有八九提交失败或者提交了也是废卷。我一般采用的随机策略分三层先洗牌再按配置取数最后检查可见性。洗牌不是为了炫技是为了避免“随机”变成“每次都选同一个”。直接用Math.random()做多次选择重复概率比想象中高很多。这里用 Fisher-Yates 洗牌function shuffle(arr) { const a [...arr]; for (let i a.length - 1; i 0; i--) { const j Math.floor(Math.random() * (i 1)); [a[i], a[j]] [a[j], a[i]]; } return a; } function pickRandomOptions(optionList, minCount, maxCount) { const visible [...optionList].filter(opt { // 过滤掉不可见选项处理跳题逻辑 const rect opt.getBoundingClientRect(); return rect.width 0 rect.height 0; }); if (visible.length 0) return []; // 随机决定选多少个范围要在题目限制内 const count Math.min( maxCount, Math.max(minCount, Math.floor(Math.random() * visible.length) 1) ); return shuffle(visible).slice(0, count); }逻辑说明shuffle函数先把选项数组打乱然后用.slice(0, count)取前 N 个这样既保证随机又不会重复选同一个。minCount和maxCount是脚本内置配置单选场景两者都传 1多选题按实际限制传比如“最多3项”就传[0, 3]。代码里还做了一步可见性过滤用getBoundingClientRect()判断元素是否显示在页面上这是处理跳题逻辑的关键。参数说明getBoundingClientRect()返回元素的尺寸和位置如果元素的宽或高是 0说明它被隐藏了。这一招在处理问卷星跳题时非常管用因为隐藏题目只是display:none元素还在 DOM 里但不可见。你如果不过滤这些选项脚本就会“点空气”。洗牌算法的复杂度是 O(n)对于几个选项的题目完全够用。你也可以用库里的lodash.shuffle但既然是个轻量 js 脚本自己实现十几行就够了没必要为这个引入重依赖。2.3 事件触发click 在问卷星里不一定够用这是脚本实战里最容易翻车的环节。你在控制台手写label.click()页面有时候会选中但问卷星的提交校验会告诉你“请完成必填项”。原因是问卷星监听的是mousedown、mouseup、click一组事件而且部分版本还会检查鼠标坐标或者pointerdown。经验是不要单纯依赖.click()而是完整派发一遍鼠标事件链。function simulateClick(el) { const rect el.getBoundingClientRect(); const x rect.left rect.width / 2; const y rect.top rect.height / 2; const opts { bubbles: true, cancelable: true, clientX: x, clientY: y }; el.dispatchEvent(new MouseEvent(mousedown, opts)); el.dispatchEvent(new MouseEvent(mouseup, opts)); el.dispatchEvent(new MouseEvent(click, opts)); }逻辑说明clientX和clientY我特意算成元素中心点因为有些问卷星版本的事件处理函数里会读取坐标做点击判定。bubbles: true保证事件能冒泡到外层监听器问卷星的全局事件委托基本都挂在 document 上。参数里你可以看到我连续派发了mousedown / mouseup / click这和真实用户的点击顺序一致。有人问为什么不直接触发change事件问卷星的选项状态管理比较特殊change事件通常会触发但一旦遇到“点击后还要联动显示其他选项”的场景change就不够了。我踩过这个坑矩阵题里触发change后分数是画上了但下一题被跳出来了卷面状态错乱。所以统一用鼠标事件链最省心。3. 跑起脚本前要准备的东西油猴插件与控制台双通道3.1 浏览器与油猴插件环境这份脚本的载体是 js运行环境推荐浏览器 Tampermonkey油猴插件。油猴的好处是可以通过match规则限定只在问卷星域名生效不会污染其他页面。问卷星有多个域名wjx.cn、wjx.top、wjx.com都存在建议把几个常见域名都写进匹配规则。// UserScript // name 问卷星随机填写脚本 // namespace wjx-auto-fill // version 1.0.0 // description 按题型随机填写问卷星问卷用于测试与演示 // match https://*.wjx.cn/* // match https://*.wjx.top/* // match https://*.wjx.com/* // grant none // /UserScript这段不是完整脚本只是油猴的头部声明。match的写法是通配符域名*.wjx.cn能匹配www.wjx.cn也能匹配edu.wjx.cn这类子域名。grant none表示不申请特殊 API脚本只跑在页面上下文里。为什么要特别声明域名因为油猴默认只在你配置的页面上运行如果你不加match脚本在别的站点挂着没用且可能误伤。3.2 两种安装方式油猴注入与控制台直贴油猴安装适合长期复用但如果你只是想临时跑一次或者公司电脑不让装插件控制台直贴就是第二通道。两种方式我都给控制台直接粘贴的方式打开问卷页按F12在 Console 里粘贴脚本再回车。这种方式脚本不会持久化刷新页面就没了适合验证改版后的逻辑。油猴方式则适合把脚本存下来反复用。# 打开浏览器开发者工具 # 按 F12 - Console 面板 # 粘贴上述脚本代码回车执行需要说明的是控制台执行和油猴执行有个细微差别油猴脚本默认在页面空闲时执行控制台粘贴则是立即执行。如果页面还没渲染完直接执行会拿到空的questions列表。所以不管是哪种方式我都推荐脚本入口包一层“等待页面渲染完成”的逻辑function ready(fn) { if (document.readyState loading) { document.addEventListener(DOMContentLoaded, () fn()); } else { setTimeout(fn, 500); // 给问卷星自己的 js 留出绑定事件的时间 } } ready(() { // 主流程 });这段代码的逻辑很好理解document.readyState loading说明还在解析 HTML那就等DOMContentLoaded如果已经加载完了再用setTimeout额外等一下问卷星的内部脚本。这个 500ms 是我调出来的经验值太快容易漏绑定太慢影响批量填写效率。3.3 全局参数表哪些值值得你改脚本跑之前先认识几个核心参数。所有随机策略和延时策略都围绕它们展开参数名默认值作用MIN_CHOICE1每道多选题最少选的项数MAX_CHOICE3每道多选题最多选的项数FILL_BLANK_RATE0.6填空题非空概率避免全填或全不填DELAY_PER_QUESTION800每道题之间的随机延时上限单位毫秒FIXED_DELAYfalse是否关闭随机延时固定为 800ms我一般建议DELAY_PER_QUESTION不要设成 0 或固定值道理后面避坑章节会详细说。FILL_BLANK_RATE这个参数很多人忽略但它在“看起来像真人”这一点上非常关键。问卷星后台有提交速度分析如果每一道填空都写了同一句话或者所有卷子都完全一样很容易被判疑似机器。脚本里我通常留一个默认答案池每次从这里随机取一条写入而不是固定填充。const config { MIN_CHOICE: 1, MAX_CHOICE: 3, FILL_BLANK_RATE: 0.6, DELAY_PER_QUESTION: 800, FIXED_DELAY: false, ANSWER_POOL: [ 基本符合, 偶尔遇到, 比较满意, 建议增加功能 ] };这段代码是配置对象的示例。MIN_CHOICE和MAX_CHOICE是整数FILL_BLANK_RATE是 0 到 1 的小数。ANSWER_POOL是自定义答案池脚本会在填空题里随机取其中一条。你如果做的是满意度调研答案池可以改成「满意 / 一般 / 需要优化」之类如果是产品反馈类问卷也可以换成更具体的描述。这个参数直接决定脚本产出的答案有没有“人味”。4. 不同题型怎么映射单选、多选、矩阵与下拉框4.1 单选题与多选题点击逻辑的差异问卷星最常见的两种题型脚本处理逻辑截然不同。单选题只需从可见选项里随机点一个多选题则需要先随机决定选几个选项再洗牌取数组。很多脚本只做单选遇到多选就自动跳过这显然不行。正确映射方式如下function fillSingleChoice(questionEl) { const opts getOptions(questionEl); const visibleOpts opts.filter(opt { const rect opt.getBoundingClientRect(); return rect.width 0 rect.height 0; }); if (visibleOpts.length 0) return; const pick visibleOpts[Math.floor(Math.random() * visibleOpts.length)]; simulateClick(pick); } function fillMultiChoice(questionEl) { const opts getOptions(questionEl); const visibleOpts opts.filter(opt { const rect opt.getBoundingClientRect(); return rect.width 0 rect.height 0; }); if (visibleOpts.length 0) return; const chooseCount Math.min( config.MAX_CHOICE, Math.max(config.MIN_CHOICE, Math.floor(Math.random() * visibleOpts.length) 1) ); const picks shuffle(visibleOpts).slice(0, chooseCount); picks.forEach(opt simulateClick(opt)); }逻辑说明fillSingleChoice与fillMultiChoice共用了“过滤可见选项”这步因为跳题逻辑下部分选题会被隐藏。区别在于单选取一个多选先算数量再洗牌。simulateClick是前面定义过的事件链函数这里直接复用。这两个函数的输入都是题目根节点不关心题目的具体字符串所以标题是“请选择您的年龄段”还是“您使用过以下哪些功能”脚本一律不关心只看题型。这里有个细节多选题如果有“至少选2项”的校验但visibleOpts只剩 1 个可见选项脚本还是会去点那一个提交时问卷星会报错。解决办法是在主流程里加一个检查if (visibleOpts.length config.MIN_CHOICE) { console.warn([布局警告] 可见选项数少于最小选择数); }这个警告的意义在于它帮你快速定位是不是跳题逻辑把选项隐藏了。我实际调试时很多次都是靠这个 log 发现是题目“其他”选项的一个输入框影响了布局。4.2 矩阵量表、下拉与填空题不按套路出牌的三种题型矩阵量表在 DOM 结构上跟单选完全不同它是表格行是题目列是分值。每行的选项是input[typeradio]但直接点 radio 依然会碰到事件绑定的问题。我的处理方式是把每行的 radio 收集起来随机选一个然后用dispatchEvent触发事件。function fillMatrix(questionEl) { // 矩阵量表行容器通常带 ui-table-row 或类似类名 const rows questionEl.querySelectorAll(.ui-table-row, [class*table-row]); rows.forEach(row { const radios row.querySelectorAll(input[typeradio]); if (radios.length 0) return; const pick radios[Math.floor(Math.random() * radios.length)]; simulateClick(pick); }); }逻辑说明rows选择器我写了两个.ui-table-row是问卷星老版结构的类名[class*table-row]是容错写法匹配任何 class 里包含table-row的元素。这样做的好处是页面结构微调时脚本不容易挂。radios只取input[typeradio]因为矩阵里没有label包裹点原生 input 反而更直接。下拉框的处理是很多人忽略的一个坑。问卷星的下拉框有两种原生select和自绘 div。原生 select 可以直接设置value然后触发change事件自绘 div 则需要先点开它再点击列表项。function fillSelect(questionEl) { const nativeSelect questionEl.querySelector(select); if (nativeSelect) { const opts nativeSelect.querySelectorAll(option); const validOpts [...opts].filter(o o.value ! ); if (validOpts.length 0) return; const pick validOpts[Math.floor(Math.random() * validOpts.length)]; nativeSelect.value pick.value; nativeSelect.dispatchEvent(new Event(change, { bubbles: true })); return; } // 自绘下拉框点击触发器后选择列表项 const trigger questionEl.querySelector([class*select-trigger], [class*dropdown]); if (trigger) { simulateClick(trigger); setTimeout(() { const items document.querySelectorAll(.ui-select-item, [class*select-item]); const visibleItems [...items].filter(it { const rect it.getBoundingClientRect(); return rect.width 0 rect.height 0; }); if (visibleItems.length 0) { visibleItems[Math.floor(Math.random() * visibleItems.length)].click(); } }, 200); } }逻辑说明原生select的处理快而直接设置value后派发change问卷星能识别。自绘下拉框复杂一些它也需要模拟点击展开等列表渲染后再点击列表项。setTimeout(..., 200)是给下拉列表的渲染留时间200ms 在低配机器上够用如果你发现经常点空可以调到 400ms。填空题的映射相对简单但要注意控制“填什么”。问卷星的填空题分为单行文本和多行文本DOM 上都是textarea或input[typetext]。脚本写入值后需要派发input事件否则问卷星认为输入框为空。function fillBlank(questionEl) { const textarea questionEl.querySelector(textarea); const input questionEl.querySelector(input[typetext]); const target textarea || input; if (!target) return; if (Math.random() config.FILL_BLANK_RATE) { return; // 按概率留空 } const answer config.ANSWER_POOL[ Math.floor(Math.random() * config.ANSWER_POOL.length) ]; target.value answer; target.dispatchEvent(new Event(input, { bubbles: true })); target.dispatchEvent(new Event(blur, { bubbles: true })); }逻辑说明FILL_BLANK_RATE控制留空概率避免每份问卷填空题都填满导致卷面太假。写入答案后派发input事件这是让 Vue 或 React 这类框架更新状态的通用手段问卷星内部也做了数据绑定没派发input事件等于白填。blur事件是触发失焦校验有些必填项在失焦时才会真正登记数据。5. 避坑问卷星反作弊与脚本失效的常见问题5.1 现象脚本一跑就报错Cannot read properties of null这个问题最常见新手遇到会以为脚本坏了其实 90% 是因为在页面还没渲染完时就执行了脚本。问卷星的页面是 SPA 风格的动态渲染DOMContentLoaded只代表 HTML 骨架出来了问卷内容可能还在异步加载。我遇到过打开问卷后 1 秒内执行脚本.div_question一个都拿不到。解决办法有两个控制台方式下先等到右上角转圈消失再粘贴执行油猴方式下用setTimeout做轮询而不是只等一次。function waitForQuestions(maxWait 5000) { return new Promise((resolve, reject) { const start Date.now(); const timer setInterval(() { const qs document.querySelectorAll(.div_question, fieldset); if (qs.length 0) { clearInterval(timer); resolve(qs); } else if (Date.now() - start maxWait) { clearInterval(timer); reject(new Error(题目容器未找到)); } }, 200); }); }逻辑说明setInterval每 200ms 检查一次题目容器是否出现最多等 5000ms。resolve(qs)把题目列表返回给调用方这样脚本就可以保证在数据就绪之后才开始操作。参数maxWait是一个防御性上限防止页面真的没有问卷时脚本死循环。这个轮询思路也可以迁移到其他需要等待动态渲染的场景不局限在问卷星。5.2 现象提交时提示“请完成必填项”但脚本明显勾选了这个坑出在事件触发上。前面我强调过不要用原生.click()而是要派发完整鼠标事件链。问卷星的选项状态绑定是“事件驱动”的如果你触发的change事件没有冒泡到页面的全局监听器它内部数据里你选中的选项是空的但界面上看起来是选中了。所以提交校验一跑就露馅。解决方式就是统一使用simulateClick函数。如果用了事件链还是不行检查你的dispatchEvent参数里bubbles是否为true。我见过有人写new MouseEvent(click)时不传参数默认bubbles是false事件根本传不出去监听器一个都没收到。5.3 现象脚本在 iframe 里没有任何反应问卷星有两种嵌入方式直接访问问卷链接时是普通页面但如果是通过别人网站内嵌的问卷或者企业微信里打开的链接内容可能在iframe里。你的脚本默认在当前窗口执行document.querySelector拿的是顶层页面自然找不到问卷内容。解决方式要靠iframe切换先找到问卷所在的 iframe再在它内部执行查找function getQuestionRootDocument() { const frames document.querySelectorAll(iframe); for (const frame of frames) { try { const doc frame.contentDocument; if (doc doc.querySelector(.div_question)) { return doc; } } catch (e) { // 跨域 iframe 会抛异常跳过 } } return document; } const rootDoc getQuestionRootDocument(); const questions rootDoc.querySelectorAll(.div_question, fieldset);逻辑说明frame.contentDocument是访问 iframe 内部文档的接口如果 iframe 跨域浏览器会抛异常所以用try/catch包住。一旦找到包含问卷题目的 iframe后续所有querySelector都基于这个rootDoc执行。这个思路对“问卷星嵌入到其他系统”的场景非常关键。5.4 现象被问卷星判为异常问卷提交后无效这个现象不一定是脚本报错而是你提交之后问卷星后台把这份答案标记成“无效”或者直接弹出一个验证码。原因大多数是提交速度异常。正常人填一份 30 题左右的问卷需要 3~5 分钟你的脚本 2 秒跑完而且每道题选项都一样这个反作弊规则不抓你抓谁。解决办法分两个维度延时随机化 答案内容随机化。function randomDelay() { if (config.FIXED_DELAY) { return config.DELAY_PER_QUESTION; } // 0.5倍 ~ 1.5倍 之间随机避免固定节奏 const base config.DELAY_PER_QUESTION; const factor 0.5 Math.random(); return Math.floor(base * factor); }FIXED_DELAY默认是falsefactor的取值范围在0.5到1.5之间也就是说延时会在 400ms 到 1200ms 之间波动。实测下来固定延时比随机延时更容易被判定因为机器痕迹太重。再加上答案池的随机抽取每份卷子的文本答案不完全相同后台看会更接近真人操作。5.5 现象随机选择时老是重复选中同一个选项这是随机函数没写好的经典症状。很多人直接写Math.floor(Math.random() * len)反复调用表面看是随机但在短时间多次取样时会出现聚集效应。尤其是选项只有两三个时连续几次随机结果很容易相同。我的解法前面已经写了用 Fisher-Yates 洗牌后.slice取前 N 个。这种做法的数学特性决定了在一组选项中取多个值时任意组合出现的概率相等不会因为重复随机导致某些项被多次选中。如果你手上是一份已经写好的脚本发现多选总是重复重点看它是不是没有洗牌而是循环里反复调用随机函数。6. 验证脚本正确性日志回放与自定义答案池脚本写完不等于能直接用我每次都会先跑一遍“日志回放”也就是把每次填写的关键动作输出到控制台然后人工核对这些日志是否符合预期。这一步花不了两分钟但能省下大量填错卷子重跑的后悔药时间。const log []; function logAction(type, questionIndex, value) { log.push({ type, questionIndex, value, time: Date.now() }); console.log([${type}] 第${questionIndex}题 -, value); }逻辑说明logAction把题型、题号、选中值和时间戳全部记入log数组同时在控制台输出。你可以在脚本末尾把log转成 JSON检查每道题的选择是否符合预期。比如单选是否每题只选一个、多选数量是否在MIN_CHOICE和MAX_CHOICE之间、填空是否按概率留空。还有一个技巧是生成“答案摘要”而不是看全量日志function summarize() { const counts {}; log.forEach(item { const key ${item.type}_Q${item.questionIndex}; counts[key] (counts[key] || 0) 1; }); console.table(counts); }这个函数会把重复出现的题目选择次数统计出来一眼就能看出哪些题目被多次点击。如果多选题选项总数是 5但你看到某个选项被点了 6 次那肯定是去重逻辑出了问题。自定义答案池是这份资源里最容易改成自己业务的模块。默认的答案池是针对一般满意度问卷的你要是做产品调研可以把ANSWER_POOL改成具体功能描述你要是做教育类问卷可以改成与课程相关的短语。注意ANSWER_POOL里的回答最好不要超过 15 个字因为很多问卷星的填空题自带字数限制超长文本会被后端截断或校验失败。从那以后我每次拿到一份新的问卷星链接都会先打开控制台跑一遍带日志的脚本确认各种题型都能正确映射再考虑批量使用。这个习惯帮我躲过了至少三次因为问卷改版带来的脚本失效问题。希望这份脚本和踩坑记录能帮你在处理问卷自动化时少走几步弯路。本文还有配套的精品资源点击获取
返回列表