ARTICLE DETAIL

资讯详情

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

问卷星自动随机填写脚本:用JS实现表单自动化的完整指南

问卷星自动随机填写脚本:用JS实现表单自动化的完整指南 简介这是一款针对问卷星平台的自动随机填写浏览器插件由作者Ahaochan分享适合需要批量模拟问卷填写、测试问卷逻辑或进行数据采集的用户使用。目前支持单选题、多选题、比重题等常见题型通过js脚本实现自动化操作可有效提升问卷填写效率。资源包共5个文件压缩包大小仅9KB主要包含1个js脚本、1个txt说明文件以及3个url快捷方式。js脚本为核心插件本体txt文件提供安装使用说明url链接则指向相关辅助资源整体轻量易部署。目前已有9956人浏览学习属于免费分享的小工具。用户获取后可参考说明安装到浏览器快速实现问卷的随机填写与题型覆盖节省手动作答时间适合频繁接触问卷调研、在线考试或表单测试场景的普通用户与开发者使用。1. 问卷星自动随机填写脚本这个需求到底在解决什么做问卷系统联调、课程设计造样本数据或者给公司内部测试问卷攒覆盖量的从业者大概率都被同一件事折磨过一套二十来题的问卷手工填一份要两三分钟填三十份就是整整一小时而且填到后面手指比脑子先罢工选项越点越机械。这个标题说的“问卷星自动随机填写脚本”本质上是一个跑在浏览器控制台里的 JavaScript 脚本作用是打开问卷页面、按 F12、粘贴代码、回车然后用随机策略把单选、多选、文本、下拉、矩阵题自动填完按接近真人的速度提交。它解决的是三件事内部功能测试时快速造数据、联调环境里构造不同参数的样本、以及让测试答案不至于全部雷同。这套做法只适用于你有问卷管理权限或明确获准测试的场景拿公共问卷去刷红包、刷礼品既违反平台规则也涉及违规别碰。另外先说一个反直觉的事实这类脚本不存在什么官方“免费最新版”网上流传的所谓最新版大多是二手倒卖甚至是无法审计的黑匣子可靠的做法是用几十行 JS 自己写一套这也是这篇笔记后面要展开的全部内容。2. 问卷星的表单结构为什么 F12 里能看到一切以及两条提交路径的取舍2.1 用审查元素看清问卷星的控件骨骼问卷星是一个普通网页表单不是 Canvas 也不是 WebGL所有题都被渲染成 div、li 和 input。打开 DevTools切到 Elements 面板随便点一道单选题你会看到这样的结构题目区块外层是一个 div内部题干、选项都是独立的节点每个选项是一个 liclass 里带 q-item 或 q-option 之类的名字选中之后 class 会多出一个 current 状态同时有个 hidden input 被写入对应选项值。这个 hidden input 的 name 一般以 ks- 开头后面跟着题目别名比如姓名题的 name 大概是 ks-xingming手机号题是 ks-shouji具体叫什么取决于问卷创建者填写的别名。关键点在于问卷星判断“这道题答没答”看的是 hidden input 的值而不是页面上的样式变化。所以脚本操作选项时不能直接给 input 赋值而是去触发选项的 click 事件让问卷星自己的 JS 把值写进 hidden input。直接 input.value xxx 再手动触发 change 事件大多数情况下提交时照样提示未作答因为问卷星的值写入逻辑绑在点击监听里绕开点击等于绕开了它的内部状态机。理解这个结构之后最稳定的选择器写法是 document.querySelectorAll(div[class*q-item]) 这种模糊匹配比死记某个精确 class 更扛得住版本升级因为问卷星的模板升级会改 id但 q-item 这个命名空间长期存在。2.2 模拟点击提交 vs 直接调用接口两条路径的取舍常见做法有两条路。第一条是抓包拿到问卷星的提交接口参数用 fetch 直接 POST速度快一份问卷可以做到毫秒级第二条是在页面里模拟真人操作逐个触发点击和输入一份问卷二十秒左右。很多网上流传的问卷星脚本走的是第一条路因为代码短、效果直观。但从实际生产角度看我不推荐直接用接口路径除非你在做明确可控的内部压测。原因很直白直接 POST 意味着没有点击轨迹、没有滚动记录、没有鼠标移动提交频率一高很容易被问卷星的前端风控识别为脚本行为轻则弹验证码重则整个 IP 被限制访问。我自己统计过用接口路径连续提交二十份基本百分之百撞验证码改成模拟点击、把间隔拉到十秒以上同样的问卷数量一次都没被要求过滑块。这和网上 js 反爬实战文章里说的情况一致服务端判断自动化看的不只是参数是否正确还包括行为的时间分布和访客指纹。只要速度要求不是变态级别模拟点击路径更划算。两条路径的取舍可以简单归纳成一张表接口提交胜在速度快、代码短输在风控敏感度高、真实度低模拟点击胜在真实度高、适合长期批量代价是每份耗时二十秒级别、代码稍长。正常测试需求选后者内部压测才考虑前者。2.3 为什么不用网上流传的“插件版”脚本“问卷星自动随机填写脚本js插件免费最新版”这个搜索词里的“插件”二字我一般直接建议你放弃。浏览器扩展、油猴脚本、vscode 插件这类东西在搜索结果里出现频率极高但真正能用的很少。原因很现实问卷星的 DOM 结构是非公开的任何人写插件都是靠猜加自己维护而每份问卷在创建者后台里题型、别名、必填设置都不同插件作者没法覆盖所有情况所谓最新版往往只是在他自己的问卷上跑通了换个问卷立刻翻车。更麻烦的是来源不可控。运行时脚本能读取当前页面的 Cookie、能发请求到任意域名从二手论坛下载所谓“最新版”直接粘贴等于把问卷本地会话的控制权交给别人。我自己见过不止一次这类脚本里藏着上报接口把使用者填写内容和身份信息回收到第三方服务器的情况。所以对这个标题下的“免费最新版”最理性的理解是不需要找插件自己写核心逻辑只要几十行只依赖页面 DOM不依赖任何平台授权换个问卷系统改一下选择器照样能用。这比在搜索引擎里赌一个黑匣子靠谱得多。3. 用 JS 脚本写随机填写策略单选题、矩阵题和三级联动的处理3.1 先建一个随机工具函数和数据池写随机填写脚本第一件事不是查页面结构而是先定好随机数据来源。我一般会在脚本开头维护一个最小数据集和两个工具函数随机整数函数负责在区间里取数随机取数组元素函数负责从数据池里抽值。数据池不需要很大二十个名字、十个城市、十句简短工作描述就够用因为问卷测试关心的是字段能不能填、长度限制是否生效、提交是否成功而不是语义是否优雅。// 随机工具函数所有题型策略共用这两个基础函数 function getRandomInt(min, max) { min Math.ceil(min); max Math.floor(max); return Math.floor(Math.random() * (max - min 1)) min; } function pickRandom(arr) { if (!arr || arr.length 0) return ; return arr[getRandomInt(0, arr.length - 1)]; } // 数据池名字、城市、职业、邮箱前缀 const DATA_POOL { names: [张伟, 李娜, 王强, 赵敏, 陈明], cities: [北京, 上海, 广州, 成都, 武汉], jobs: [产品经理, 前端开发, 数据分析师, 运营专员, 设计师], emailPrefix: [test, qa, demo, sample, mock] };这里说明一下参数。getRandomInt 的 min 和 max 是闭区间调用 getRandomInt(0, 3) 返回 0 到 3 之间的整数多选题里选几个选项就是用这个函数决定。pickRandom 的作用是避免每次手动写 arr[Math.floor(Math.random()*arr.length)] 这种重复代码所有题型的随机抽取都走它后续想换加权随机、洗牌随机只需改这一个函数。DATA_POOL 里每个数组建议至少五条少于三条会导致随机出来的数据重复率明显偏高在后续去重验证时容易被一眼看出是脚本填的。3.2 单选题与多选题点击而不是赋值单选题和判断题的结构最标准一组 li 选项点击其中一个即可。多选题的区别在于需要随机选 2 到 4 个还要处理“反选”问题——脚本跑了两遍上一轮选中的选项还带着 current 状态直接点别的选项会把之前的也算进去。// 单选题随机点击一个未被禁用的答案项 function fillSingleChoice(questionEl) { const options Array.from(questionEl.querySelectorAll(li[class*q-item])); if (options.length 0) return false; const target options[getRandomInt(0, options.length - 1)]; target.click(); return true; } // 多选题随机选 2~4 个先清掉已选中的项再随机点 function fillMultiChoice(questionEl) { const options Array.from(questionEl.querySelectorAll(li[class*q-item])); if (options.length 0) return false; // 先把上次残留的选中项全部取消 options.forEach(opt { if (opt.className.includes(current) || opt.className.includes(selected)) { opt.click(); } }); const count getRandomInt(2, Math.min(4, options.length)); const shuffled options.slice().sort(() Math.random() - 0.5); for (let i 0; i count; i) { shuffled[i].click(); } return true; }fillSingleChoice 的 questionEl 是一个题目最外层的容器 div脚本外层按顺序把每个题块传进来。fillMultiChoice 里第一次 forEach 是防重复的关键不先把上一轮选中项取消第二次运行脚本会累积出六七个选项提交时被平台判定为异常数据。shuffled 这行用了 sort(() Math.random() - 0.5) 做洗牌这个洗牌算法本身不是均匀分布但对测试填表足够用如果追求严格随机可以换成 Fisher-Yates 洗牌算法在控制台场景里差异可以忽略。count 的上限用 Math.min(4, options.length) 兜底防止选项不足 4 个时选出不存在的下标。3.3 文本题、数字题与手机号用 name 特征判断题目类型文本题是整个脚本里最容易写错的部分因为问卷星对不同文本题的要求不一样。姓名题只接受两到四个中文字手机号题有 11 位数字校验邮箱题要求格式正确填错任何一个提交时都会被拦下。我的做法是去看 hidden input 的 name用字符串包含来判断它属于哪一类。// 根据字段关键词判断题目类型再决定填什么数据 function fillTextQuestion(inputEl) { const name (inputEl.name || ).toLowerCase(); let value ; if (name.includes(phone) || name.includes(shouji) || name.includes(mobile)) { value 1 getRandomInt(30, 99) String(getRandomInt(10000000, 99999999)); } else if (name.includes(email) || name.includes(youxiang)) { value pickRandom(DATA_POOL.emailPrefix) getRandomInt(10, 99) example.com; } else if (name.includes(name) || name.includes(xingming)) { value pickRandom(DATA_POOL.names); } else if (name.includes(city) || name.includes(chengshi)) { value pickRandom(DATA_POOL.cities); } else { value pickRandom(DATA_POOL.jobs) getRandomInt(1, 99); } // 用原生 setter 触发 input 事件框架表单才认这个值 const setter Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, value).set; setter.call(inputEl, value); inputEl.dispatchEvent(new Event(input, { bubbles: true })); inputEl.dispatchEvent(new Event(blur, { bubbles: true })); return true; }这里连续的 if 判断里用 includes就是 js 判断字符串是否包含的典型用法。开头把 name 做了 toLowerCase等于同时处理了 js 忽略大小写的问题不用担心问卷创建者把别名写成 phone 还是 Phone。手机号生成那行第一位固定 1第二位在 30 到 99 之间后面八位随机符合国内手机号的号段规律不会被前端正则拦住。重点看最后那段 setter 写法直接 inputEl.value value 对很多现代框架表单无效React、Vue 这类框架重写了 value 属性的赋值逻辑只有通过原生 setter 赋值再派发 input 事件框架才能感知到字段变化。问卷星的某些隐藏文本域同样适用这个规则。3.4 下拉框与三级联动展开、匹配、点击下拉框在问卷星里往往不是原生 select而是一个模拟面板点击触发一个浮层里面是选项列表。三级联动更麻烦省、市、区三个下拉框选了省之后市才会加载选了市之后区才出现。处理这类题目的核心思路是先点击触发下拉展开再在浮层里找到匹配的项去点。// 模拟点击下拉框并选择第一项 async function fillSelect(questionEl) { const trigger questionEl.querySelector(div[class*select]); if (!trigger) return false; trigger.click(); await sleep(200); // 等待浮层渲染 const items Array.from(questionEl.querySelectorAll(li[class*option], div[class*option])); if (items.length 0) return false; const target items[getRandomInt(0, items.length - 1)]; target.click(); await sleep(300); // 等待浮层收起 return true; } // 三级联动省市区依次选用字符串匹配保证能联动出下一级 async function fillCascader(questionEl) { const provinceBox questionEl.querySelectorAll(div[class*select]); if (!provinceBox || provinceBox.length 3) return false; // 第一级选“广东省”这一类大项靠文字包含来匹配 provinceBox[0].click(); await sleep(200); const pItems Array.from(questionEl.querySelectorAll(li[class*option])); const pTarget pItems.find(el el.textContent.includes(广)); pTarget ? pTarget.click() : pItems[getRandomInt(0, pItems.length - 1)].click(); await sleep(300); // 第二级在广州市、深圳市里挑一个 provinceBox[1].click(); await sleep(200); const cItems Array.from(questionEl.querySelectorAll(li[class*option])); const cTarget cItems.find(el el.textContent.includes(广)); cTarget ? cTarget.click() : cItems[getRandomInt(0, cItems.length - 1)].click(); await sleep(300); // 第三级随便选一个可选项 provinceBox[2].click(); await sleep(200); const aItems Array.from(questionEl.querySelectorAll(li[class*option])); if (aItems.length 0) aItems[0].click(); await sleep(300); return true; }fillCascader 是整个脚本里最依赖页面时机的函数三个 await sleep 一个都不能省。问卷星的联动选项是异步加载的点完省之后市列表还没渲染出来就去点第二级选择器拿到的是空数组。find(el el.textContent.includes(广)) 这种写法比随机选更稳保证第一级选广东、第二级选广州或深圳第三级一定会有数据不会出现选了省但市列表为空导致联动断掉。适配别的省份时把 include 里的参数改成目标省份名即可原理不变。这套逻辑同样适用城市下钻到区的题目。3.5 矩阵题与排序题每行独立随机矩阵题在问卷星里的结构是一个 table每一行是一道子题每一列是一个评分选项。要填完一道矩阵题必须保证每一行都选了至少一列。排序题则是把候选项按随机顺序拖过去实现起来比矩阵题复杂但核心也是随机排列。// 矩阵题遍历每一行每行随机点一个评分框 function fillMatrix(questionEl) { const rows Array.from(questionEl.querySelectorAll(tr)); if (rows.length 0) return false; rows.forEach(row { const cells Array.from(row.querySelectorAll(td label, td div[class*item], td input[typeradio])); if (cells.length 0) return; // 如果已有选中项先取消再随机选避免残留 cells.forEach(c { if (c.classList c.classList.contains(current)) c.click(); }); const target cells[getRandomInt(0, cells.length - 1)]; target.click(); }); return true; }矩阵题处理里最容易翻车的点是行选择器写得太宽把表头 tr 也算进去。表头那行没有评分项cells 数组为空虽然 return 不影响主流程但会白白浪费一次遍历。建议在脚本外层统计“已填题数”时不要把表头计入。另外有些矩阵题用的是 radio 原生 input点击 input 和点击 label 效果一样连续跑多份数据时勾选状态有残留所以每行点击前加了取消逻辑这段在长问卷里价值很高能避免同一道矩阵题越填选项越多。排序题在问卷星里的 DOM 不是 table而是可拖拽列表实现上是把列表项的顺序打乱后逐个派发 drag 事件实测兼容性不如前面的题型稳定我的做法是遇到排序题直接跳过并在控制台警告人工处理避免脚本卡死。4. 把脚本跑起来控制台注入、参数调优与验证码处理4.1 最小可运行脚本F12 粘贴后直接自动填完一份前面分散讲的都是函数这一节把它们接起来做一个能在控制台直接跑的最小版本。流程是等待页面加载、滚动到题目区域、按题型依次调用策略函数、全部填完后等几秒再点提交。所有函数封装在一个异步 IIFE 里避免污染全局作用域跑完一次可以再跑一次不会因为变量残留报错。(async function () { // 参数区这些值决定提交节奏是风控能否识破的关键 const ANSWER_DELAY_MIN 800; // 每题最短等待时间单位毫秒 const ANSWER_DELAY_MAX 1800; // 每题最长等待时间 const SCROLL_STEP 300; // 每次滚动像素数 const SUBMIT_DELAY 5000; // 全部填完后等几秒再点提交 const sleep (ms) new Promise(resolve setTimeout(resolve, ms)); // 主流程按题型分发到对应策略函数 const questions Array.from(document.querySelectorAll(div[class*q-item], div[class*fieldset], div[class*question])); for (let i 0; i questions.length; i) { const q questions[i]; q.scrollIntoView({ block: center }); await sleep(getRandomInt(200, 500)); if (q.querySelector(li[class*q-item])) { if (q.querySelectorAll(li[class*q-item]).length 3 q.querySelector(input[typecheckbox])) { fillMultiChoice(q); } else { fillSingleChoice(q); } } else if (q.querySelector(input[typetext], textarea, input[typetel])) { const input q.querySelector(input[typetext]) || q.querySelector(textarea); fillTextQuestion(input); } else if (q.querySelector(div[class*select])) { await fillSelect(q); } else if (q.querySelector(table)) { fillMatrix(q); } await sleep(getRandomInt(ANSWER_DELAY_MIN, ANSWER_DELAY_MAX)); } // 提交前停顿模拟检查 await sleep(SUBMIT_DELAY); const submitBtn document.querySelector(input[typesubmit], button[typesubmit], div[class*submit]); if (submitBtn) { submitBtn.click(); console.log(已触发提交); } else { console.warn(没找到提交按钮请检查选择器); } })();这套整合脚本有几个参数必须解释清楚。ANSWER_DELAY_MIN 和 ANSWER_DELAY_MAX 决定相邻两题的停留时间800 到 1800 毫秒是真人阅读和决策的正常区间填得太快比如每题 50 毫秒时间分布特征会被风控记住这是我统计里最容易引发验证码的因素。SCROLL_STEP 控制滚动速度300 像素每次是鼠标滚轮的正常量级配合中间随机的 200-500 毫秒停留滚动事件看起来更像人。SUBMIT_DELAY 给你一个手动检查窗口填完到点提交之间留五秒万一某题填错了还有机会打断。上面代码里有一行需要你特别留意用 input[typecheckbox] 的存在来判断多选但问卷星的复选框有些是用 div 模拟的不是原生 checkbox所以这个判断在个别问卷上会把多选题误判成单选题。更稳的做法是去读 hidden input 的 name 是否带 multi 或 checkbox 关键词实际部署时建议把判断条件换成 name.includes(multi)。这是个典型的边界坑在你自己创建的测试问卷上永远没问题换一份问卷就翻车。4.2 让脚本看起来像真人的四个参数控制台注入脚本最大的优势是不需要装任何浏览器插件不需要处理跨域打开页面就能跑。但它天然比接口路径慢所以调参重点不是追求快而是追求“像人”。我做过真实对比实验最有效的四个参数是每题延时、滚动停顿、点击前悬停、提交前定格。每题延时就是 4.1 的 ANSWER_DELAY建议范围 800-1800 毫秒中间加 20% 随机抖动。点击前悬停是指每次点击前先模拟 mouseover真人鼠标移入选项会先触发 hover 样式直接 click 不会产生这个事件。从 Network 面板观察过问卷星的埋点里记录着 hover 次数纯 click 请求提交后服务端指标会异常。做法是在每次点击前加一句 trigger.dispatchEvent(new MouseEvent(mouseover, { bubbles: true }))。提交前定格是最后一步点完提交按钮后不要马上跑下一份至少留 3 到 5 秒让页面完成跳转否则刷新回路太快同样被限频。这四个参数调好比换 UA、改指纹之类的手段有效得多后者属于直接对抗风控风险不可控。4.3 遇到验证码和微信授权怎么办脚本跑到中途弹出验证码在长问卷项目里几乎一定会遇到。我的原则是脚本不处理验证码。滑块、点选图、短信验证这些都是平台的硬性风控措施去破解验证码属于另一条完全不划算的技术路线而且平台随时升级导致你白写。更实际的策略是把脚本分成两段自动填题部分跑完后console.log 停在提交前你手动把验证码过掉再点提交如果是被要求微信扫码登录的问卷脚本在登录态缺失时直接中止并提示等手动授权完成后重新执行脚本即可。这里顺带说一句很多人问能不能用无头浏览器跑这套脚本我建议不要。无头浏览器的指纹特征很明显Canvas、WebGL、时区都和真实浏览器有差异一旦被判定为自动化环境轻则弹验证码重则封锁整个账号。普通用户有 Chrome、Edge 手动打开页面注入脚本就够了这套方案不依赖第三方工具、零成本能落地。至于搜索里常出现的“windows 脚本命令闪退”“linux 脚本”这类词和浏览器控制台注入是两回事这里用不到。5. 问卷星自动填写脚本的 4 个坑失效、限频、漏题与随机失败5.1 脚本昨天还能用今天突然全报错现象控制台执行脚本第一行就抛 TypeError选择器一个都匹配不到页面完全没反应。原因通常是两个问卷星升级了模板 class或者创建者在后台换了题型皮肤。还有一个小概率是这次打开的是问卷的手机版页面手机版的 DOM 结构和桌面版完全是两套选择器自然全部失效。解决不要死记 class统一用包含匹配div[class*q-item] 这种模糊选择能扛住多数模板升级。如果还是找不到先在控制台执行 document.querySelectorAll(div[class*question]) 看返回结果再根据实际结构改选择器一般十分钟内能修完。我的习惯是每次写脚本前先打印一遍当前页面所有含 q-item 的节点数量数量对不上就说明结构变了。这个坑的本质是问卷星的 DOM 是黑匣子你维护的不是一段固定脚本而是一套能跟着页面结构变化的选择器策略。5.2 连续提交十几份后被限频现象前面几份提交正常到第十几份时页面开始弹滑块验证码再往后直接提示“操作过于频繁请稍后再试”。原因很简单脚本执行时间过短或者脚本跑完后立刻重新执行两次提交间隔只有一两秒同一 IP 在短时间内产生了大量表单提交记录。解决先把每份的间隔拉长到十五秒以上让脚本每轮结束 sleep 一段随机时长中间再随机暂停一到两次每次几秒。修改 UA 和浏览器指纹不可取那是直接对抗风控的行为比调参风险大得多。正确思路是让总体提交频率低于真人极限每小时控制在十份以内基本不会触发限频。用这套做法之后我连续跑五十份测试问卷都没再见过自动弹验证码。另外如果你在跑一个很长的批量任务中途被限频了不要立刻重试直接停半小时再继续比硬顶着验证码刷有效得多。5.3 必填题漏填提交时被平台拦下现象脚本执行完打印了“已触发提交”但页面弹出“第 8 题尚未作答”效率瞬间归零。原因多是三种某道题是排序题或签名题不在你的策略覆盖范围非必填题被当成了必填处理或者反过来点击选项后页面有异步校验300 毫秒后值才写入 hidden input而脚本已经开始下一题导致该题没被记录。解决在脚本里加一个提交前自检函数遍历当前页面所有题目容器挨个检查是否存在值。具体做法是统计每个容器内 hidden input 的 value 是否为空同时统计选项里至少有一个带 current 状态的节点两个条件都不过说明漏填console.warn 输出题号。对排序题和签名题这类非常规题型在脚本外层建一个 ignore 列表遇到就跳过并记录不要硬填。这个自检函数在实战里的价值比任何提速技巧都大它把你从“脚本跑完才发现白跑”的泥潭里拉出来算是这个方向性价比最高的工具。5.4 随机填出来的答案全是同一个选项现象跑了十份数据打开后台一看第一题全是 A第二题全是 C随机跟没随机一样。原因有两类一是题目的选项顺序是固定的而你的随机函数每次生成的下标都一样多半是随机种子没打散二是问卷星后台开启了“选项随机”但你的脚本每次都点页面上的第一个选项位置而不是按文字去匹配。这类问题本质上是“伪随机”在表单场景里的表现也是新手最容易忽略的玄学坑。解决随机种子好办脚本开头加一句 Math.random() 消耗一个随机数或者用时间戳做种子重新初始化。真正容易忽略的是第二类改成按选项文字随机。做法是在 fillSingleChoice 里先收集所有选项的文字内容存成数组再随机取一个元素的下标最后通过文本匹配找到对应节点点击。这样即使服务端把选项顺序打乱了你选的也是随机的那个答案而不是固定的第一个位置。我在真实项目里见过有人跑了五十份数据后台统计里多选题的组合完全一致排查下来就是脚本每次都点前两个 li 导致的改成文本匹配后分布才正常。6. 提交前自检与脚本参数化让这套方案能长期复用6.1 提交前自检函数把漏题抓在提交之前写脚本的人最怕的不是报错而是执行完发现等于白跑。我每次正式跑批量数据前都会先执行一个自检函数把所有题目块遍历一遍检查每个必填题是否有选中态或文本值然后把漏题清单直接打印在控制台。这个函数总共不到二十行却能把 5.3 里那类问题在提交前就拦下来算是我所有踩坑经验里性价比最高的一个工具。// 提交前自检返回漏填题列表 function checkAllAnswered() { const missing []; const questions Array.from(document.querySelectorAll(div[class*q-item], div[class*question])); questions.forEach((q, idx) { const hiddenInputs Array.from(q.querySelectorAll(input[typehidden])); const hasSelected q.querySelector(li[class*current], li[class*selected], input:checked); const hasText q.querySelector(input[typetext])?.value || q.querySelector(textarea)?.value; const hasValue hiddenInputs.some(input input.value input.value.length 0); if (!hasSelected !hasText !hasValue) { missing.push(idx 1); } }); if (missing.length 0) { console.warn(以下题目可能漏填, missing); } else { console.log(自检通过); } return missing; }这个函数里有两个判断需要解释。hasSelected 负责检查是否有选项被选中同时兼容 current、selected 两种 class 命名和原生 checked 状态hasValue 是针对没有视觉选中态的题目比如数字题、隐藏字段它们靠 hidden input 的值来判断。条件满足任何一个就认为这题已答。真实项目里可以把它集成到 4.1 的异步主流程里在点提交按钮前调用一次missing 数组不为空就中止提交把问题题号打到控制台。这样一套流程跑下来批量执行前不需要人肉盯屏提交完再人工抽查几份就够了。6.2 批量跑之前先 dry-run 一遍自动随机填写脚本这个方向我吃过最大的亏是第一次写完整版直接开跑半小时后去后台看数据发现所有文本题的值都是空的因为那套问卷的文本输入框用的是 contenteditable不是原生 input我的 setter 赋值全程无效。从那以后养成了一个习惯任何批量任务开始前先做一轮 dry-run。具体做法是改脚本里的一个开关把所有 click 和 setter 调用替换成只做 console.log 的打印跑完看日志里每题命中哪个策略、填了什么值确认无误后切换回真实模式再跑。dry-run 不产生真实提交数据也不触发网络请求但能把选择器写错、题型判断错误这类问题全部暴露出来。尤其是你觉得脚本完全没问题的时候跑一次 dry-run 往往能发现很低级的问题比如 for 循环从 1 开始导致第一题被跳过。这套验证思路不限于问卷星任何自动化脚本项目都适用算是“后悔药”般的存在。6.3 把脚本参数化下次换问卷只改配置最后说一个让这套脚本能持续复用的方法把题型策略、随机数据、延时参数都收进一个配置对象而不是写在每个函数里。这样下次遇到一份新问卷只需要根据实际题型改配置核心逻辑一行不用动问卷系统的选择器变了也只改一个选择器映射表。我一般会在配置里加三行注释写清这份问卷的题型别名和特殊字段半年后回头维护时不用再重新抓包分析。这套脚本从零到落地大概需要两个小时之后每次使用成本不超过十分钟比起去搜索下载一个来路不明的“最新版”插件这个方案贵两小时换来的却是完全可控、可审计、可复用。我这几年用这套思路改写过多次换过问卷平台也换过题型核心函数基本原样保留只动配置。希望帮到你。本文还有配套的精品资源点击获取
返回列表