Python+Selenium问卷星自动填写:从元素定位到防封完整指南
上星期一个老同学突然找到我说有个问卷星的大学生心理测评需要批量填写几十份问卷还得选不同答案手动点实在太折磨人。我帮她填了几份就开始烦躁于是干脆用Python和Selenium写了个自动填写脚本跑完之后整个人都舒坦了。这篇文章就把整个项目的思路、踩过的坑、还有怎么把脚本“泛化”到任意一份问卷星问卷上的方法完整拆开讲。如果你也经常被批量填问卷、数据采集、账号测评这类重复劳动折磨这篇应该能帮你省下不少时间代码可以直接拿去改。先说明一下这套方案适合有基础Python概念的人但就算你只是刚装好Python照着步骤抄也能跑通。重点不只在“跑起来”而是弄懂为什么这么写这样换一份问卷时不会抓瞎。1. 整体设计与思路拆解1.1 问卷星表单的页面特征与自动化难点问卷星本身是个前端渲染很重的表单系统虽然表单最终提交的是普通HTTP请求但整个页面大量依赖JavaScript动态创建DOM。直接分析提交接口、用requests模拟请求的方式不是不行问题是每份问卷的字段名、答题参数、校验逻辑都不一样你分析完这一份下一份照样从头来维护成本高得离谱。Selenium的方式粗暴但有效它启动一个真实的Chrome浏览器脚本像真人一样点击、输入、选择、提交。所见即所得页面长什么样脚本操作的就是什么样拦截和校验逻辑也一并绕过了因为浏览器做的事情和真人没有本质区别。问卷星的自动化难点其实不在“能不能自动”而在题型多样性。常见的就有四种单选题、多选题、填空题、下拉题有些问卷还有矩阵量表题、排序题、地区联动选择。每种题型的DOM结构都不同尤其是下拉题问卷星部分下拉框是原生select但更多是用divulli模拟的伪下拉框传统的Select类根本处理不了。1.2 技术选型为什么是Selenium而不是requests我在这个项目前其实先试了requests直接调接口把页面里的提交URL、参数都挖出来了甚至能成功提交一份空问卷。但很快我就放弃了原因有三点第一问卷星的提交接口字段是动态生成的不同问卷完全不一样参数名里经常带上问卷ID和随机token每次都要改抓包逻辑第二接口提交没有浏览器端的JavaScript环境一些问卷的必填校验和逻辑跳转虽然在服务端也会校验但响应非常不友好定位问题很痛苦第三高频接口提交容易被风控盯上IP都可能被限制。Selenium这种浏览器自动化方案就没有这些烦恼它模拟的是“真实用户操作”只要控制好频率不会触发风控。和Playwright、Puppeteer这类同类工具相比Selenium的资料最多遇到报错搜索一下基本都有解。再加上webdriver-manager这个库能自动管理浏览器驱动版本匹配的坑也填掉了用起来省心不少。1.3 脚本架构配置与代码分离的通用填表引擎从一开始我就想把脚本做成“通用”的而不是写一个只能填某一份问卷的死脚本。思路很简单把“怎么填这份问卷”的信息抽出来单独存成配置脚本本身只负责读取配置、定位元素、执行操作。这样做的好处很明显。以后遇到新的问卷我只需要在配置里增加题目信息就行代码一行不用改。这个设计是我从做爬虫的经验里学来的爬虫最怕的就是页面改版后源码要重写把选择器、字段名这类“定位元数据”从代码里剥离出去维护负担会小很多。这个项目里我管这个叫“定位元数据”也就是描述“某个元素在页面什么位置”的那组信息。2. 环境搭建与工具准备2.1 Python环境配置装完就能跑不管你是Windows、macOS还是Linux第一步都是装Python。很多人卡在第一步是因为安装时没把Python加进PATH环境变量导致在终端敲python会报错热词里那个“python was not found; run without arguments to install from the Microsoft Store”我见过太多次了。在Windows上最容易的办法是去官网下载Python安装包安装第一步必须勾选“Add Python to PATH”再点Install Now。macOS用户建议用Homebrew装Linux用户用系统包管理器就行。装完在终端验证一下python --version能显示版本号就说明环境好了。接下来建议创建一个虚拟环境避免依赖包污染全局Pythonpython -m venv venv venv\Scripts\activate # Windows命令行 source venv/bin/activate # macOS / Linux虚拟环境激活后命令行前面会出现(venv)字样。然后统一安装依赖pip install selenium webdriver-managerselenium是核心库webdriver-manager用来自动下载匹配的浏览器驱动。只有这两个就够不需要装requests之类的其他东西。2.2 WebDriver管理版本匹配的坑与解法Selenium本身不能控制浏览器它需要通过WebDriver这个中间人和浏览器沟通Chrome对应ChromeDriverEdge对应EdgeDriver。早期做法是自己去浏览器驱动官网下载对应版本然后手动配置路径。这套流程有个很头痛的问题Chrome浏览器版本一升级驱动就失效脚本第二天可能就跑不起来了。使用webdriver-manager之后就省心很多它会在首次运行时自动检测你本机Chrome版本然后下载匹配的驱动之后每次运行也都自动检查。初始化浏览器的代码也顺便变得更简洁from selenium import webdriver from selenium.webdriver.chrome.service import Service from webdriver_manager.chrome import ChromeDriverManager def init_driver(): options webdriver.ChromeOptions() # 去掉“Chrome正在受到自动软件控制”的提示 options.add_experimental_option(excludeSwitches, [enable-automation]) options.add_experimental_option(useAutomationExtension, False) driver webdriver.Chrome( serviceService(ChromeDriverManager().install()), optionsoptions ) driver.maximize_window() return driver这里有两个细节值得记住。一个是“去掉自动化控制提示”这个选项不能保证绕过网站检测但能让页面上少一个明显的自动化标识另一个是窗口最大化问卷星有些题目是固定定位的窗口太小会导致元素被折叠或者不可见点击时容易报错。2.3 编辑器选择与调试技巧编辑器这块VS Code和PyCharm都行。VS Code轻量配一下Python扩展、在命令面板里用“Python: Select Interpreter”选中刚才创建的虚拟环境就行。PyCharm社区版免费创建项目时直接可以选虚拟环境在Settings的Project Interpreter里确认一下就好。实际调试脚本时我强烈建议在关键步骤加截图尤其是填完问卷准备提交之前driver.save_screenshot(before_submit.png)自动化的坑往往发生在你看不见的地方。脚本运行时报错你看不到页面当时的状态有了截图就能一眼看出是元素没加载出来、还是窗口滚动位置不对、还是弹窗盖住了按钮。这个习惯帮我排查过好多问题省时省力。3. 核心细节元素定位与下拉框处理3.1 问卷星的DOM特征与四种题型的定位思路打开一份问卷星的问卷按F12看一下Elements面板你会发现结构其实挺有规律的。每道题被一个大的div包裹题目标题在div里选项是label标签包着radio或checkbox填空题是textarea下拉框可能是原生select也可能是divulli的伪下拉。这里有个很重要的经验定位元素时不要过分依赖id和name属性因为问卷星在不同问卷里这些属性经常变化甚至同一份问卷在不同加载时刻都可能不同。更稳的做法是结合class特征和元素层级来定位。单选项的典型结构是这样div classdivquestion div classfield-label性别/div div classoptions labelinput typeradio nameq1 value1男/label labelinput typeradio nameq1 value2女/label /div /div定位思路是找到所有单选input根据选项文本匹配对应的label再点击def click_radio_by_text(driver, name, text): labels driver.find_elements(By.CSS_SELECTOR, finput[name{name}]) for label in labels: if text in label.find_element(By.XPATH, ..).text: label.find_element(By.XPATH, ..).click() return多选题结构类似只是input type是checkbox。填空题则简单得多直接定位textarea或文本输入框然后send_keys填充。如果题目必填一般都会有required属性这个可以作为判断依据。3.2 重点div/ul/li组合下拉框的点击与选择下拉题是问卷星自动化里最容易翻车的地方。原生select/option结构用Selenium的Select类很容易处理from selenium.webdriver.support.ui import Select select Select(driver.find_element(By.CSS_SELECTOR, select[nameq3])) select.select_by_visible_text(本科)但很多问卷模板用的自定义下拉框结构是div包着ul和li外层div点击后展开下拉列表ul里每个li就是一个选项。这种结构Select类完全不适用你必须模拟真人操作先点击触发下拉等选项渲染出来再点击目标选项。完整处理函数如下def select_custom_dropdown(driver, trigger_css, option_text): 处理 divulli 组合的伪下拉框 trigger_css: 触发下拉展开的div选择器 option_text: 需要选择的选项文本 WebDriverWait(driver, 10).until( EC.element_to_be_clickable((By.CSS_SELECTOR, trigger_css)) ).click() # 等待下拉列表的ul出现并完全展开 time.sleep(0.3) options WebDriverWait(driver, 5).until( EC.visibility_of_all_elements_located( (By.CSS_SELECTOR, f{trigger_css} ul li) ) ) for opt in options: if opt.text.strip() option_text: opt.click() return True raise Exception(f下拉选项里没有找到: {option_text})这里有几个容易踩的坑。第一点击展开后不能立即去定位li必须等一下因为选项列表可能是异步渲染的第二选项文本要用strip()去掉首尾空格不然匹配不上第三点击选项时webdriver会自动滚动到该元素但如果选项被遮挡需要手动滚动。最后一种情况可以在点击前先执行JS确保元素进入可视区域driver.execute_script(arguments[0].scrollIntoView(true);, opt)还有一种是各路地区选择器比如“省/市/区”三级联动本质上是三个divulli下拉框组合逐级调用上面的函数每选完一级等下一级刷新再选。3.3 页面元素枚举与定位元数据设计既然要把配置和代码分离首要问题就是“怎么知道每道题该用什么选择器”。我写了一个扫描脚本把页面上所有可交互元素枚举出来打印它们的标签名、类型、name、id、class前50个字符和可见文本然后输出到控制台def scan_page(driver): elements driver.find_elements( By.CSS_SELECTOR, input, textarea, select, label, ul li, div[class*select], div[class*option] ) for idx, el in enumerate(elements): try: if not el.is_displayed(): continue print( idx, tag:, el.tag_name, type:, el.get_attribute(type), name:, el.get_attribute(name), id:, el.get_attribute(id), class:, (el.get_attribute(class) or )[:50], text:, el.text[:30] ) except Exception: pass跑一遍这个脚本你就能拿到一份页面元素的“地图”。根据这些输出把每道题目的定位信息整理到一个JSON配置里这个过程就是设计“定位元数据”。我的配置文件长这样{ url: https://www.wjx.cn/vm/your_questionnaire_id.aspx, wait_timeout: 10, items: [ { type: radio, desc: 性别, container: div.divquestion:has(input[nameq1]), answer_index: 0 }, { type: dropdown, desc: 学历, trigger_css: div.select-warp, answer_text: 本科 }, { type: textarea, desc: 建议, locator: textarea, answer: 这是一段自动填写的测试建议 } ] }这种设计最大的优点就是“问卷变了只改配置”。同一套脚本今天填A问卷明天填B问卷完全不用碰代码。4. 脚本完整实现与适配个人问卷的方法4.1 项目结构与运行主流程我习惯把项目拆成两个文件scan.py负责页面元素枚举main.py负责读取配置并自动填写。主流程是打开问卷、遍历配置列表、按题型分发处理、点击提交、保存结果截图。main.py的核心入口是一个主函数def fill_survey(driver, config): driver.get(config[url]) wait WebDriverWait(driver, config[wait_timeout]) # 等待问卷主体渲染完成 wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, body))) for item in config[items]: handler dispatch(item[type]) handler(driver, item) time.sleep(random.uniform(0.8, 1.5)) # 每次操作之间随机停顿 submit(driver)dispatch函数就是根据题型返回对应的处理函数。这样的主流程非常清晰后续想加日志、加失败重试都方便。4.2 核心代码初始化、显式等待、随机延时等待策略是自动化脚本成败的关键。Selenium有三种等待方式强制等待time.sleep、隐式等待implicitly_wait、显式等待WebDriverWait。我的建议是不推荐隐式等待因为它对每个find_element操作都会生效在元素永远不出现时浪费很多时间强制等待只在特定场景用比如下拉框展开后主等待机制用显式等待配合expected_conditions条件判断。显式等待的下拉框触发点击就用了expected_conditions的element_to_be_clickable这是最稳的写法from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC from selenium.webdriver.common.by import By def click_when_ready(driver, css, timeout10): return WebDriverWait(driver, timeout).until( EC.element_to_be_clickable((By.CSS_SELECTOR, css)) ).click()随机延时是为了模拟真人操作频率。填表不是点一下马上下一步而是有思考和阅读时间的。建议每操作一个题目后sleep 0.8到1.5秒的随机值提交前再sleep 2到3秒。别小看这些停顿它能显著降低被风控盯上的概率。4.3 题型处理函数单选、多选、填空、下拉单选处理函数我采用“容器选项索引”的策略。配置里指定题目的容器CSS选择器再用容器内的label元素列表按下标取值def handle_radio(driver, item): container driver.find_element(By.CSS_SELECTOR, item[container]) labels container.find_elements(By.CSS_SELECTOR, label) if len(labels) item[answer_index]: raise Exception(f单选选项越界: {item[desc]}) labels[item[answer_index]].click()多选题多了一个answer_list依次点击多个选项def handle_checkbox(driver, item): container driver.find_element(By.CSS_SELECTOR, item[container]) labels container.find_elements(By.CSS_SELECTOR, label) for idx in item[answer_index_list]: if idx len(labels): raise Exception(f多选选项越界: {item[desc]} {idx}) labels[idx].click() time.sleep(0.3)填空处理就简单了定位textarea或inputclear后send_keysdef handle_text(driver, item): el driver.find_element(By.CSS_SELECTOR, item[locator]) el.clear() el.send_keys(item[answer])下拉题分成原生select和自定义下拉框两种我这个项目里配置字段一共做了两个分支def handle_dropdown(driver, item): if item.get(native): select Select(driver.find_element(By.CSS_SELECTOR, item[locator])) select.select_by_visible_text(item[answer_text]) else: select_custom_dropdown(driver, item[trigger_css], item[answer_text])4.4 从0到1适配一份个人问卷的完整流程现在说重点拿到一份新问卷怎么入手。我的习惯分四步。第一步手动打开问卷页面F12看网络请求确认页面正常加载。然后跑scan.py把整个页面的元素枚举出来输出到文件。你会看到类似这样的输出0 tag: div class: divquestion div1 text: 性别 1 tag: label text: 男 2 tag: label text: 女 3 tag: div class: divquestion div2 text: 学历 4 tag: div class: select-warp text: 请选择 5 tag: ul text: 高中/大专/本科/硕士/博士 6 tag: textarea name: q5第二步对照页面上肉眼看到的题目顺序把输出里的元素和题目对应起来。比如第0号div是性别题里面的label第0个是“男”第1个是“女”。这些信息整理成上一节那个JSON配置。第三步先跑一次“不提交”的验证模式。我一般把submit函数注释掉脚本走一遍全流程截图确认所有题目都正确填上了。这一步能筛掉大部分定位错误和选项顺序搞反的问题。第四步确认无误后打开submit正式跑完整流程填完后检查页面是否出现“提交成功”提示并把成功截图保存下来。这套流程本质上就是把“人工判断”放在配置阶段脚本阶段只负责执行比在代码里硬编码ID要灵活得多。5. 常见问题与排查技巧实录5.1 元素定位不到与等待策略组合“NoSuchElementException”是我猜你们最常碰到的错误。这个问题九成是元素还没渲染出来就去找了解法是先在关键节点前加显式等待。如果加了等待还是找不到就要怀疑选择器本身写错了。我的排查顺序是先确认页面是否正常加载再手动跑一遍scan.py看元素是否存在然后检查有没有iframe嵌套。问卷星的大部分内容不在iframe里但它有部分弹窗、广告层是iframe包裹的如果元素在这些层里必须先switch_to.frame切换进去再操作操作完再切回来。这个细节很容易漏。5.2 StaleElementReferenceException与点击失效StaleElementReferenceException的意思是“元素引用过期”典型的场景是页面发生了局部刷新而你之前拿到的元素对象还指向旧DOM。解决办法也很简单不要复用元素变量每次都重新查一次。在点击选项前重新定位目标元素再点基本能规避。点击失效的另一种情况是“元素被遮挡”。问卷星经常有悬浮的问卷进度条、广告弹层把提交按钮盖住了。我的处理是在点击前先滚动到目标元素再检查它是否可点击如果被遮挡就先用JS把弹层隐藏掉driver.execute_script( arguments[0].scrollIntoView({block: center});, button ) time.sleep(0.3) button.click()5.3 频率控制、验证码与合规提醒自动化脚本跑得再爽也要注意使用边界。问卷星对同一个IP短时间大量提交是有风控的轻则弹验证码重则整份问卷的链接都被限制填写。我的做法是每份问卷之间至少间隔10到20秒随机时间每次填完答案后也做随机停顿像人一样“犹豫”一下。遇到验证码我的处理是“让人介入”。脚本弹窗提示等待人工输入验证码后回车继续。这比任何自动识别方案都稳妥因为问卷星验证码本来就不难人工几秒就搞定没必要在识别上花时间成本。还有一点必须提醒这套脚本仅用于你本人有权限处理的场景比如填写自己的测试问卷、帮朋友批量导入数据、做网站功能自测。不要用它去刷别人问卷、干扰线上统计这既是对别人劳动成果的尊重也是确保自己账号不惹麻烦的前提。5.4 问题速查表下面这个表是我实测过程中遇到的高频问题整理基本可以当速查手册用。错误现象可能原因解决方案NoSuchElementException元素未渲染完成使用WebDriverWait显式等待确认选择器无误StaleElementReferenceException页面局部刷新旧元素失效操作前重新获取元素对象ElementClickInterceptedException元素被悬浮层遮挡scrollIntoView后检查遮挡必要时隐藏弹层下拉选项点击无效选项在ul展开前未渲染先等待visibility_of_all_elements_located提交按钮找不到按钮在底部需要滚动用JS滚动到按钮位置再find_element提交后提示“请勿频繁填写”频率过高触发风控降低频率增加随机等待时间选项文本匹配不上前后有空格或不可见字符用strip()清理文本再对比脚本偶发失败网络波动在步骤外层增加重试机制失败后重新定位最后分享一个我个人的经验。这套自动填写脚本折腾完之后最大的收获不是省下来的那几小时而是真正理解了“定位元数据”这个设计思路对自动化项目的重要性。你写的时候可能不觉得一旦过几天再打开这段代码你会非常感谢当初那个把配置和代码分开的自己。另外一个小建议凡是涉及大量并发的操作永远把“频率控制”写在第一优先级量变引起质变跑得快不如跑得稳。这个脚本后续还能扩展比如从Excel批量读答案、加入失败重试、问卷填完后把结果回写到本地文件玩法很多留着慢慢折腾吧。