说句实话,看到"问卷自动填写"这几个字,很多人的第一反应是拿来刷投票、刷报名、搞校验。但我这边的情况不太一样——纯粹是手里攒了一堆内部调研问卷,每周都要点一遍相同的问题,点得手指头发酸,才下定决心写个脚本把这活儿外包给浏览器。
这个项目用的是 Python + Selenium + Chrome,目标平台是超星问卷、麦客问卷这类常见的表单系统。原理不复杂,无非是让浏览器像人一样去定位选项、点击按钮、填写内容、提交表单,但真正把它跑稳,要处理的细节不少:元素定位策略、页面动态加载、下拉框和矩阵题的适配、偶发的弹窗干扰、提交成功与否的判定。今天这篇就把整个过程拆开讲透,适合刚接触 Selenium 的读者,也适合已经写过一点脚本但总在"为什么偶尔失效"上挠头的人。
1. 这个脚本解决的痛点与适用边界
1.1 重复填表的需求从哪来
先说一个很多人都遇到过的场景:每周要提交一份固定模板的问卷,里面有十几个问题,题型基本不变,只是个别选项偶尔波动。一次填完大概三到五分钟,听起来不多,但乘以每周、乘以多份问卷,一个月下来就是几个小时,而这些时间在做的事完全是机械的——打开页面、点几个单选、选几个下拉框、输入一两段固定文字、提交。
我最早的做法是浏览器自带的表单自动填充,但问卷系统大多会禁用 autocomplete,或者字段名不固定,根本存不住。后来又试过直接抓接口、模拟 POST 提交,结果更麻烦——超星问卷和麦客问卷的提交逻辑不在前端源码里,有些参数是动态生成的,接口还做了加密和时效校验,手动分析的成本远高于收益。最后回归到 Selenium 这条路:模拟真实浏览器的点击和输入,从根源上绕开了前端加密和接口校验的问题。
1.2 为什么选 Selenium 而不是 requests
这个选型值得多说两句。用requests直接提交表单,理论上速度最快,但有几个现实问题。
第一,问卷系统通常在页面加载时动态生成 token、签名或者时间戳参数,这些参数是通过 JavaScript 计算出来的,单纯靠阅读 HTML 源码很难还原。第二,部分问卷在提交后还有二次校验,比如弹窗确认、拼图验证码,这在接口层面很难模拟。第三,一旦问卷平台调整了页面逻辑,你的接口脚本可能全军覆没,而 Selenium 脚本只需要改个选择器就能继续跑。
Selenium 的方案相当于"我不管你内部怎么加密、怎么生成参数,我只要真实地操作浏览器,你所有的前端逻辑都会自动执行"。它的代价是慢——打开一个页面要一两秒,逐题填写又是几秒——但对这种批量填问卷的场景来说,慢一点完全不是问题,稳定才是关键。
1.3 什么场景不能用这个脚本
必须先把边界说清楚:不是所有问卷都能自动填,也不是所有自动填都合规。
遇到这几类情况,脚本基本无能为力:
- 强校验类问卷:包含图形验证码、滑块验证、短信验证码。图形验证码虽然可以用 OCR 方案破解,但成功率不稳定,滑块更是需要模拟人的轨迹,复杂度会翻好几倍。
- 实时检测类场景:平台如果检测到高频、同 IP、无鼠标轨迹的自动化操作,会直接屏蔽提交或标记异常数据。
- 涉及个人隐私或他人账号的场景:如果脚本被用来批量填写他人信息、冒用身份、干扰正常调研数据的统计结果,那就不只是技术问题,而是合规问题了。
我这边脚本只用于自己名下的内部培训问卷、课程调研问卷这类低风险场景,填完之后也会人工抽查一遍。大家在用类似工具时,务必确认自己的使用场景是合法合规的,问卷平台的设定是"每人限填一次"还是"可以重复练习",这两者的脚本策略完全不同。
2. 环境搭建与第一版脚本骨架
2.1 安装依赖与版本匹配
Selenium 的安装本身很简单,一条命令就够:
pip install selenium但真正的坑在浏览器驱动上。Selenium 是通过 ChromeDriver 这个中间件和 Chrome 浏览器通信的,ChromeDriver 的版本必须与 Chrome 主版本号匹配。比如你的 Chrome 是 126 版本,那就最好下载 126 版本的 ChromeDriver,差一个主版本都不行,否则脚本启动时会报类似SessionNotCreatedException: This version of ChromeDriver only supports Chrome version xxx的错误。
判断自己 Chrome 版本的方法是:在地址栏输入chrome://version,看"版本号"字段的完整信息。下载 ChromeDriver 时,注意选chromedriver-win64.zip(Windows)或chromedriver-linux64.zip(Linux),解压后把 exe 文件放到一个固定目录,比如D:/tools/chromedriver.exe,后续代码里Service对象会用到这个路径。
现在 Selenium 4.x 还引入了 Selenium Manager,可以自动适配驱动版本,但前提是电脑能正常联网。如果你所在的网络环境访问不了官方下载源,还是老老实实手动下载更靠谱。
2.2 Chrome 的启动配置
问卷页面通常都是真实浏览器渲染后的 DOM,所以我一般直接打开可见的 Chrome 窗口,好处是能看到脚本每一步在干什么,排查问题非常直观。但有时候需要长时间挂机跑,窗口一直开着也碍事,就可以启用无头模式,让浏览器在后台运行。
这里给出一份我常用的基础配置:
from selenium import webdriver from selenium.webdriver.chrome.service import Service from selenium.webdriver.chrome.options import Options options = Options() # 关闭自动化提示条 options.add_experimental_option("excludeSwitches", ["enable-automation"]) # 防止被检测出自动化特征 options.add_experimental_option("useAutomationExtension", False) # 禁用某些会被网站识别出来的特征(效果有限,但比没有强) options.add_argument("--disable-blink-features=AutomationControlled") # 无头模式,需要跑很多份问卷时可以打开 # options.add_argument("--headless=new") # 不要关闭弹窗拦截,有些问卷提交前会弹窗二次确认 options.add_experimental_option("excludeSwitches", ["disable-popup-blocking"]) service = Service("D:/tools/chromedriver.exe") driver = webdriver.Chrome(service=service, options=options) driver.implicitly_wait(10)implicitly_wait(10)的意思是在查找元素时最多等 10 秒,如果页面元素还没加载出来就持续重试。这行配置在后面处理动态加载页面时会省掉很多sleep。
2.3 一个最简单的最小脚本
先跑通"打开页面、点击一个选项、提交"再谈别的。下面这段代码以某个问卷页面为例:
from selenium import webdriver from selenium.webdriver.common.by import By from selenium.webdriver.chrome.service import Service from selenium.webdriver.chrome.options import Options options = Options() options.add_experimental_option("excludeSwitches", ["enable-automation"]) options.add_experimental_option("useAutomationExtension", False) options.add_argument("--disable-blink-features=AutomationControlled") driver = webdriver.Chrome( service=Service("D:/tools/chromedriver.exe"), options=options ) driver.implicitly_wait(10) # 1. 打开问卷页面 driver.get("https://example-survey.com/form/1") # 2. 定位一个单选题的【同意】选项并点击 option = driver.find_element( By.XPATH, "//label[contains(text(), '同意')]" ) option.click() # 3. 截个图确认当前状态 driver.save_screenshot("before_submit.png") # 4. 点击提交按钮 submit_btn = driver.find_element(By.XPATH, "//button[contains(text(), '提交')]") submit_btn.click() # 5. 等提交结果页面出现 import time time.sleep(3) driver.save_screenshot("after_submit.png") driver.quit()这个版本不考虑任何异常处理,但已经是一个完整的闭环了。后面所有的优化,都是在这个骨架上一步步加健壮性。
3. 问卷控件的定位策略:从"能点"到"点得准"
3.1 先搞清楚问卷页面的 DOM 结构
问卷系统虽然界面各异,但底层的 HTML 结构高度相似,最常见的是这样一层嵌套关系:
<div class="question-item"> <div class="question-title">您对本次培训的满意度是?</div> <div class="option-item"> <label> <input type="radio" name="q1" value="satisfied"> <span>满意</span> </label> <label> <input type="radio" name="q1" value="normal"> <span>一般</span> </label> </div> </div>有一种新手常见的写法是直接定位input元素然后click()。这在很多页面能成功,但遇到 label 和 input 分离、或者有 CSS 覆盖的情况时点击不到,因为用户实际点击的是一个隐藏的 radio,视觉上看到的选项是 label 里的文字。
所以我的定位偏好是把label作为定位目标,用"文字可读性"最强的表达式,而不是那些input[name=q1]这种跟页面具体实现强绑定的方式。
3.2 文本框与文本域
文本框是最直接的控件,定位到元素后调send_keys即可。
from selenium.webdriver.common.by import By # 按 placeholder 定位 name_input = driver.find_element( By.CSS_SELECTOR, "input[placeholder='请输入您的姓名']" ) name_input.clear() name_input.send_keys("某开发者") # 按相邻标题定位 company_input = driver.find_element( By.XPATH, "//div[contains(text(), '单位名称')]/following-sibling::div//input" ) company_input.send_keys("某公司技术部")这里有两个容易踩的细节:
一是clear()的顺序。如果输入框默认有内容,不先清空就send_keys,文字会追加到末尾。在填空题里,这可能导致提交的数据是"旧内容+新内容"。
二是定位文本域时,要注意有些问卷平台用的是textarea不是input。用 CSS 选择器时写textarea或直接统配//div[contains(text(),'建议')]//textarea,这样能同时覆盖两种情况。
对于内容比较长的开放式填空题,脚本里最好预置几个固定的模板文本,按问题类型随机切换,别每次都填一模一样的。原因在后面"填表质量"那一节会细讲。
3.3 单选题、多选题与选项定位
单选的本质是"在 N 个候选中点击一个",多选的本质是"点击至少一个"。在 Selenium 里,它们的实现方式一致,区别只在于点击的数量。
我的定位写法基本统一:
def click_option(text): xpath = f"//label[contains(text(), '{text}')]" driver.find_element(By.XPATH, xpath).click() click_option("满意")如果页面里选项是li或者p包裹的,可以稍微调整一下:
def click_option_flexible(text): # 优先 label,其次 li,再次 span for tag in ["label", "li", "span", "p"]: elements = driver.find_elements(By.XPATH, f"//{tag}[contains(text(), '{text}')]") if elements: elements[0].click() return True return False这个"轮流尝试"的小技巧在麦客问卷上特别管用。它的单选题有些版本是label包input,有些是div套span,写死哪一种都容易在平台改版后失效。
多选题的差别只在于:点击完第一个选项后,页面可能不会立即刷新,但选项的选中状态会变化,所以脚本要处理的是"点击多个选项后视觉状态是否都正常"。我在代码里加了一个截图步骤,跑完多选后截一张图,人工扫一眼就能确认。
3.4 下拉框与评分矩阵
下拉框在问卷里通常有两种形态。第一种是原生select元素,可以直接用 Selenium 自带的Select类:
from selenium.webdriver.support.ui import Select select_el = Select(driver.find_element(By.XPATH, "//select[@name='department']")) select_el.select_by_visible_text("技术部")第二种是自定义下拉框,长得像是文本框加一个下拉箭头,实际上是div+ul渲染的,Select类派不上用场。这种要分两步:点击下拉框本身,再点击列表项。
driver.find_element(By.XPATH, "//div[contains(text(), '请选择省份')]").click() time.sleep(0.5) # 等列表展开 driver.find_element(By.XPATH, "//li[contains(text(), '广东省')]").click()这里sleep(0.5)其实是无奈之举,更好的方案是下面第四节讲的显式等待。在自定义下拉框未展开时,列表项在 DOM 里可能根本不存在,必须等它渲染出来才能点击。
评分矩阵题是相对麻烦的一类。常见表现是 N 行 × M 列,每行有一组 radio。定位策略是"先锁定行,再锁定列对应的 radio":
row_xpath = "//tr[contains(.//td, '课程内容')]" cells = driver.find_elements(By.XPATH, row_xpath + "//input[@type='radio']") # 第3列对应评分"8分" cells[2].click()这个写法的关键是contains(.//td, '课程内容')里的那个点,它表示在整行里搜索文本,而不是只在某一行当前单元格里搜索。写错这个细节,定位会大量失效。
4. 动态加载与弹窗干扰的处理
4.1 显式等待优先于乱 sleep
新手最快踩的坑就是"我明明定位到了元素,怎么运行时报 NoSuchElementException?"答案十有八九是:元素是动态加载的,你定位的那一刻它还没渲染出来。
implicitly_wait解决了一部分问题,但它的机制是"每次 find_element 时轮询查找",不是全局等待。还有一种情况是元素已经存在于 DOM 中,但处于不可点击状态——比如题目未加载完时选项是 disabled 的,这时就算找得到元素,点击也会报错。
标准做法是用显式等待配合期望条件:
from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC wait = WebDriverWait(driver, 10) # 等选项可点击 option = wait.until( EC.element_to_be_clickable( (By.XPATH, "//label[contains(text(), '满意')]") ) ) option.click()与无脑time.sleep(3)相比,显式等待的好处是"快的地方不等、慢的地方不长不短地等",整个脚本跑下来能快好几秒。更重要的是,sleep 在网速稳定时可能没问题,一旦网络波动或者页面卡顿,固定 sleep 就会导致随机失败,而显式等待天然适应这种不确定性。
4.2 iframe 与嵌套页面
问卷系统有时会把某些模块塞进 iframe 里,比如"加载中"的过渡页、验证码弹层、或者当前步骤的确认浮窗。Selenium 默认只能操作主文档里的元素,遇到 iframe 必须切进去:
iframe = driver.find_element(By.CSS_SELECTOR, "iframe[id='confirm_frame']") driver.switch_to.frame(iframe) # 在 iframe 内定位按钮 driver.find_element(By.XPATH, "//button[contains(text(), '确定')]").click() # 操作完切回主文档 driver.switch_to.default_content()一个容易忽略的点是:切进 iframe 之后再执行find_element,作用域只在 iframe 内部;如果切不回来,后面在主文档中的定位会一直报错。所以切 iframe 的动作要尽量和里面的操作保持在一个代码块里,操作完马上切回。
如果你不确定目标元素是否在 iframe 里,可以先试一种简化的调法:用driver.page_source搜索关键词,如果搜到元素但find_element定位失败,那基本就能确认是 iframe 的问题。
4.3 alert 弹窗与"二次确认"
问卷提交前经常弹出一个 alert 或 confirm 框,询问你是否确认提交。alert 不是普通元素,只能用driver.switch_to.alert来处理:
# 点击提交按钮后可能出现 alert submit_btn.click() try: alert = WebDriverWait(driver, 5).until(EC.alert_is_present()) alert.accept() except: pass需要特别注意的是,有些问卷平台的"二次确认"是一个普通 DOM 弹层,不是原生 alert,这种情况下switch_to.alert是拿不到的。处理方式就是正常定位弹层里的"确定"按钮,跟处理普通元素没有区别。
判断到底是原生 alert 还是 DOM 弹层,有一个简单办法:看页面的 URL 和标题有没有变。原生 alert 不会改 DOM,而 DOM 弹层会在page_source里多出一些标记。
4.4 分页问卷的"下一页"逻辑
超星问卷有些是多页结构,填写过程中需要多次点击"下一页"或"下一题"。我的处理方式是写一个循环,直到找不到下一个按钮为止:
while True: try: next_btn = driver.find_element( By.XPATH, "//button[contains(text(), '下一页') or contains(text(), '下一题')]" ) next_btn.click() time.sleep(0.3) except: break这种写法比硬编码"点击三次"更健壮。因为问卷题目数量可能有变化,写死次数在题目被删减时会导致定位空页面报错。
但这里要注意:如果某页有必填项没填,点击"下一页"后平台会弹出红字提示,此时页面会停留在当前页。脚本需要判断当前页是否有"未通过校验"的提示,最笨但有效的方法是:点击下一页后,立刻检查当前页是否还包含"下一页"按钮,如果还在,就说明没有翻页成功,需要回头检查当前页必填项。
我一般把这个逻辑封装成"点击下一页 → 等待 → 判断当前页是否变化",用当前页任何一个独特元素的可见性作为标志,而不是靠 sleep 硬等。
5. 填表质量与"尽量别被看出来是脚本"
5.1 选项随机化的必要性
如果脚本每次填的都一模一样,比如所有单选题固定选第一个选项,平台的反作弊系统很容易识别出异常模式。更实际的好处是:如果问卷是内部练习用的,固定答案会导致练习流于形式,回头看数据也没有参考价值。
所以我在脚本里加了一个简单的随机选择逻辑:
import random def click_random(option_list): """option_list 是定位表达式的列表,随机点击其中一个""" target = random.choice(option_list) driver.find_element(By.XPATH, target).click()多选题可以用random.sample从候选中取出两到三个再逐个点击。单选题则可以是"满意"与"非常满意"随机二选一,而不是永远选第一个。
这类随机策略对"模拟真实用户"来说只是很小的一步,但它的意义在于:脚本产出数据的分布更像人工填写,减少被平台以"填表模式异常"为由拦截的可能。
5.2 模拟真实输入速度
脚本如果用send_keys一次性输出整段话,浏览器其实会以极快的速度触发keydown事件,这会让部分平台的输入监控发现端倪。更稳妥的方式是逐字符输入,字符之间加一个随机的短间隔:
import time import random def human_type(element, text): for char in text: element.send_keys(char) time.sleep(random.uniform(0.02, 0.08))这种写法在个别页面会引发一个副作用:如果输入框有实时校验逻辑(比如每输入一个字就检查长度),逐字输入的效率会降低。这时候要把time.sleep(random.uniform(0.1, 0.2))调大一些,避免一输入就被判定为异常输入速度。
5.3 隐藏 ChromeDriver 的自动化特征
现在的问卷平台基本都会在前端检查navigator.webdriver属性。正常浏览器这个属性是false,而通过 ChromeDriver 启动的浏览器这个属性是true。虽然这个属性可以被覆盖,但覆盖的方式也有限,属于"防君子不防小人"的方案。
其中比较普适的做法是在页面加载前执行一段 JavaScript,把属性改掉:
driver.execute_cdp_cmd("Page.addScriptToEvaluateOnNewDocument", { "source": "Object.defineProperty(navigator, 'webdriver', {get: () => undefined})" })这段代码要在driver.get()之前执行,才能真正在页面脚本执行前把属性覆盖掉。
另外还可以通过启动参数禁用一些自动化开关,就像前面代码里--disable-blink-features=AutomationControlled那样。坦白讲,这些手段只解决"看起来像正常人"的问题,不解决"操作模式异常"的问题。如果你填问卷的行为本身和人类差异过大——访问频率离谱、鼠标完全没有任何移动、答题速度恒定 0.03 秒——那就算webdriver标志藏得再好,平台也能从行为模式上发现端倪。
所以我在实践中采取的策略是:脚本只用于低频率、低风险的场景,每次填完之后设置一个随机间隔,而不是一口气把几十份问卷全刷完。
5.4 提交后的确认逻辑
问卷提交后,平台一般会跳转到"感谢您的参与"之类的页面。脚本需要确认提交成功,方法通常是等待某个成功标志元素出现:
from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC success_texts = ["感谢", "提交成功", "问卷已完成", "提交已记录"] found = False for text in success_texts: try: WebDriverWait(driver, 10).until( EC.visibility_of_element_located( (By.XPATH, f"//*[contains(text(), '{text}')]") ) ) found = True break except: continue if not found: driver.save_screenshot("submit_failed.png") raise Exception("未检测到提交成功标志") else: driver.save_screenshot("submit_success.png")逻辑上更稳妥的是"找不到成功标志就截图留证",后面排查时回看截图,比直接看异常堆栈直观多了。
6. 实战踩坑复盘:从"能跑"到"稳定跑"
6.1 元素被遮挡导致点击无效
第一次跑完整脚本时,我最头疼的报错是ElementClickInterceptedException。大意是"目标元素被其他元素遮挡,无法点击"。调查后发现,问卷页面的"下一页"按钮上方有一个浮动的提示条,遮挡了按钮的一部分,Selenium 计算出的点击坐标落在提示条上,于是点击失败。
解决方法是尝试页面滚动的微调,把遮挡元素移开:
from selenium.webdriver.common.action_chains import ActionChains # 先把元素滚动到视野中央 element = driver.find_element(By.XPATH, "//button[contains(text(), '下一页')]") driver.execute_script("arguments[0].scrollIntoView({block: 'center'});", element) time.sleep(0.3) # 如果还是被遮挡,改用 JavaScript 直接点击 driver.execute_script("arguments[0].click();", element)execute_script直接调用 DOM 元素的click()方法,能绕过遮挡层,因为它在事件层面模拟点击,不走"计算可视坐标"的流程。代价是它不会触发某些依赖坐标或鼠标事件的前端逻辑,不过对问卷的"下一页"按钮来说,绝大多数情况下没问题。
这个技巧也可以用在整个脚本的"兜底点击"函数上:WebDriverWait等元素可点击通常没问题,但如果反复出现点击异常,就加上这个 JavaScript 点击的备选路径。
6.2 StaleElementReferenceException:元素过期了
这个异常可能是问卷自动化里最经典的坑。报错场景通常是:页面发生了一次局部刷新,之前已经找到的WebElement对象还在 Python 手里,但底层 DOM 里的那个节点已经不存在了,再对这个对象调用.click()或.text,Selenium 就直接抛异常。
最典型的触发点是"点击某个选项后,题目分支导致页面其他部分重新渲染"。比如问卷里有一道"是否在校生"的选择题,你选了"是",页面立刻渲染出"学校名称"的下拉框,这时候你再看之前下拉框的引用就已经过期了。
我的处理方案很朴素:所有元素都通过"重新查找"的方式使用,而不是把元素对象存到变量里传来传去。如果确实需要连续操作,就在每次操作前再find_element一次。
如果实在要持有元素,就配合 try-except 在异常时重新获取:
try: element.click() except StaleElementReferenceException: element = driver.find_element(By.XPATH, same_xpath) element.click()6.3 提交按钮定位到多个按钮
有次脚本稳定跑了好几天,突然在某一份问卷上报错:"expected to find 1 element, but found 2"。原因是那份问卷页面底部除了"提交"按钮,还有一个"暂存草稿"按钮,两个按钮的文案都包含"提交"两个字,XPath 的contains(text(), '提交')一下子匹配到两个。
之后我把所有"提交"定位写成了这样:
# 优先找类型为 submit 的按钮或包含"提交并结束"文字的按钮 xpath = "//button[contains(text(), '提交') and not(contains(text(), '草稿'))]"其实这里更值得反思的是定位表达式的"精确度"。contains(text(), '提交')是个模糊匹配,它相当于是"包含这个文本就行",一旦页面出现多个符合条件的按钮就出事。更稳妥的做法是优先用=精确匹配,或者给按钮加一个层级关系限定:找"最后一个按钮"或者"位于表单最底部区域"的按钮。
6.4 问卷选项文本的偶然变化
问卷平台上有一类题目是开放式的,"其他"选项后面跟着一个输入框。点击"其他"这个选项后,输入框才会变成可见状态。但我的脚本在个别问卷上定位"其他"选项时,因为页面里同时有两个"其他"文本(一个是选项、一个是输入框的 placeholder),导致点击出错。
处理方法是缩小范围:把定位限定在选项容器内,而非整个页面:
# 先拿到当前题目的容器 question = driver.find_element( By.XPATH, "//div[contains(@class, 'question')][.//span[contains(text(), '其他')]]" ) other_label = question.find_element(By.XPATH, ".//label[contains(text(), '其他')]") other_label.click()用一个相对路径把 XPath 限定在某个上下文里,比直接在全局用contains(text(), '其他')安全得多。
我在实际项目中积累下的复盘结论是:问卷自动化脚本的大部分 bug,不是 Selenium 本身的问题,而是"定位表达式的假设"与"页面真实结构"之间的偏差。页面一旦改版,这些假设就要跟着改。所以脚本里尽量把定位表达式集中放到一个文件或一个字典里,方便后续一键维护——比如我用了一个简单的 JSON 配置来集中管理题型对应的 XPath:
{ "radio_option": "//label[contains(text(), '__TEXT__')]", "next_button": "//button[contains(text(), '下一页') or contains(text(), '下一题')]", "textarea": "//textarea[contains(@placeholder, '请输入')]" }这样页面改版时,我只需要改配置,不用动核心代码逻辑,维护成本大幅降低。
7. 经验的最后一点补充
跑这套脚本跑了大半年,我的体会是:自动化填问卷真正难的不是"写一个能点的脚本",而是"写一个能持续稳定跑的脚本"。定位策略要统一、等待条件要显式、异常路径要留截图证据、页面结构变化要能快速定位到问题。
另一个很实际的建议是:别把流程做成"全自动无人值守"。哪怕脚本再稳定,我也总会留着最后一步的人工确认——提交前弹一张截图,扫一眼确认所有必填项都没有填写异常,再点提交。这个习惯帮我避免了很多次"批量填错"的麻烦,因为问卷里的题型偶尔会加一两道新的必填题,脚本没覆盖到,直接提交会报错,而人工看一眼就能发现。
如果你现在也想给自己写一个问卷自动填写脚本,建议从最小闭环开始:先打开一个测试问卷,手动验证单个选项的点击,再逐步加定位策略、等待逻辑、提交确认,最后才是批量随机化。那些炫酷的自动化技巧从来不是最难的,最难的是把"偶尔会出问题"变成"已知在什么情况下会出问题,并且已经做好了应对"。