Python+Selenium问卷星自动化建题:工程分层与实战解析
2026/9/16 6:39:20 网站建设 项目流程

简介:一份面向问卷星高频设计场景的Python自动化脚本源码,主要面向需要频繁创建问卷、发布链接、回收答题数据的科研人员、市场调研团队、教育工作者和企业运营者,针对传统手动设计问卷耗时易错的问题,提供一套低门槛、可扩展的自动化方案。压缩包内共包含35个文件,整体大小约16.18MB,其中26个py脚本负责问卷创建、题型渲染、跳转逻辑、单选多选与地图题处理、提交及重试等核心功能,另有2个ini配置文件、2个日志文件、1个txt说明、1个YAML配置、1个JSON数据文件、1个题型配置文件夹和1个exe可执行程序,方便不同角色使用。已有555人学习/下载这份源码。通过学习源码,读者既能借助exe程序直接体验问卷自动生成与收集数据的完整链路,也能从中理解问卷星自动化操作的技术要点,包括页面元素定位、数据随机比例、代理IP切换、异常重试等细节,还能参考其配置驱动的设计思路,快速定制符合自身业务场景的问卷题目、分页规则和导出格式,有效提升在线调研的标准化与效率。

1. 把问卷星手动建题压缩成一条命令

做过调研项目的人都有体会:问卷星上一份 30 题的问卷,选题型、填选项、调逻辑、配分页,手点下来至少 40 分钟;如果再改一版题目,就要重来一遍。自动化问卷设计这套基于 Python 的脚本,核心思路是绕开页面的逐项点击,用 Selenium 驱动浏览器按配置批量生成题目块,把「选题型—填选项—翻页—提交」这条链路脚本化,关键技术点是配置解析与题型映射的分离。源码里 26 个 Python 文件覆盖了配置解析、题型映射、页面识别、随机比例和重试机制,适合需要高频创建问卷的调研团队,也适合想学页面自动化分层设计的 Python 工程师。下面从工程结构开始拆。

2. 从 32 个文件看工程分层:配置、驱动与用例分离

2.1 文件树里藏着三层设计

拿到源码解压后,第一件事不是跑起来,而是看目录。反映到工程上,这个项目的分层很清晰:conf/放题型配置和 YAML 规则,utils/放可复用的浏览器控制与页面操作,config/放运行参数,main/ActionWJX.py作为执行入口,public/pages/base_page.py封装页面基类,例如元素定位和显式等待逻辑。这些目录名可能不同,但所有成熟的自动化工程都在做同一件事:把"题目长什么样"(配置)、"怎么操作浏览器"(驱动)、"什么时候做哪一步"(用例)拆开。

src/ ├── utils/ # 可复用的控制逻辑 │ ├── driver_options.py # Chrome 启动参数 │ ├── obtain_all_blocks.py │ ├── danxuan_duoxuan.py │ ├── select_option.py │ ├── next_page.py │ ├── submit.py │ └── retry.py ├── conf/ # 题型与页面规则 │ ├── questionTypeConfiguration.yaml │ ├── public/pages/base_page.py │ └── main/ActionWJX.py ├── config/ # 运行参数 │ ├── config.ini │ ├── setting.py │ └── question_config.json └── globalparam.py

多数人容易犯的错误是把所有代码堆在ActionWJX.py里,一旦题目结构变化就要改主逻辑。这个项目把「题型识别」「翻页」「提交」拆成独立模块,好处是某个环节崩了可以单独重试,另一个好处是新增题型时不需要动主流程,只加一个处理器并注册进 YAML 映射即可。

2.2 三种配置文件的读取方式

配置拆成了三个文件,各有分工:config.ini是线性结构脚本配置,里面放浏览器路径、超时时间、是否无头模式这类运行参数;questionTypeConfiguration.yaml描述题型映射规则;question_config.json存放本次问卷的题目内容。与之对应,read_config.py用 Python 内置configparser读 INI,yaml_control.pyPyYAML读 YAML,setting.py负责把 JSON 加载成全局字典。这样拆分之后,改问卷内容不需要碰代码,改运行环境不需要碰题目数据。

# utils/read_config.py(常见做法:configparser + 默认值兜底) import configparser def load_ini(path: str, section: str = "DEFAULT") -> dict: parser = configparser.ConfigParser() parser.read(path, encoding="utf-8") # getboolean 能把 "true" 字符串转成布尔,避免手写类型判断 return {k: v for k, v in parser.items(section)} def get_timeout(config: dict) -> float: # 秒数:外部传入的字符串统一转 float,异常时给 10 秒兜底 try: return float(config.get("timeout", 10)) except (TypeError, ValueError): return 10.0

parser.items(section)返回当前节全部键值对,getboolean是 configparser 自带的布尔转换方法。把"转换+兜底"封装成函数后,调用方不需要关心 INI 里参数是不是字符串。实际运行中,config.ini里最容易配错的是timeout写成整数,导致后续WebDriverWait直接抛类型异常,有了get_timeout这种转换函数就能提前兜住。

2.3 题型配置 YAML 的写法和读取时机

questionTypeConfiguration.yaml承担题型与处理器的映射,常见结构是把「单选」「多选」「下拉」「矩阵」映射到对应的处理函数名。这个文件是脚本的大脑,决定了某个题型出现在页面上时,代码该走哪条分支。

question_types: danxuan: danxuan_duoxuan.DanxuanHandler duoxuan: danxuan_duoxuan.DuoxuanHandler map: map_handler.MapHandler page_break: next_page.PageBreakHandler default_timeout: 10

读取时机放在程序启动阶段,用yaml.safe_load一次性加载成 dict,后续按题型名取处理器。这里有一个容易踩的坑:yaml.load在旧版 PyYAML 里可以加载任意 Python 对象,有安全隐患,必须用yaml.safe_load。另一个坑是题型名大小写,YAML 里写danxuan,JSON 配置里写danXuan,匹配不上会直接跳过整道题,建议读取时统一lower()

3. 浏览器驱动与页面识别:题目块怎么被自动找到

3.1 Chrome 驱动参数与启动过程

自动化问卷设计本质上是模拟真人操作,driver_options.py负责构造Options对象。这里有几个关键参数直接影响稳定性:是否无头模式、是否复用用户数据目录、是否去掉自动化提示条。download_browser_driver.py负责在本地找不到匹配版本的 chromedriver 时自动下载,版本不匹配是脚本起不来的头号原因。

# utils/driver_options.py(节选) from selenium import webdriver from selenium.webdriver.chrome.options import Options def build_options(headless: bool = False, user_data_dir: str = "") -> Options: opts = Options() if headless: opts.add_argument("--headless=new") # 新版 Chrome 无头模式 if user_data_dir: opts.add_argument(f"--user-data-dir={user_data_dir}") # 保留登录态 opts.add_experimental_option("excludeSwitches", ["enable-automation"]) opts.add_experimental_option("useAutomationExtension", False) # 禁用自动化提示条,减少页面元素识别干扰 return opts

excludeSwitchesenable-automation是 Chrome 检测 Selenium 的标志位,去掉后页面顶部不会出现"正在受自动测试软件控制"的条幅,这个条幅在某些页面上会改变布局,导致后面的坐标点击偏移。user-data-dir参数在需要复用问卷星登录态的场景很有用,首次手动登录后,后续脚本直接沿用 cookie 目录,省去每次扫码登录。日常调试阶段建议不开无头模式,肉眼能看到脚本卡在哪一步。

3.2 页面块识别与题型判定

问卷星编辑页的题目都在一个容器内,obtain_all_blocks.py的核心是找到题目块列表,querySelector.py按 CSS 选择器过滤出当前块内的可交互元素。base_page.py封装显式等待,这是 Selenium 脚本稳定的基础。隐式等待只设定一个全局轮询时间,遇到某些元素提前出现但未可交互时会误判,显式等待按条件轮询,更可靠。

# public/pages/base_page.py from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC def wait_and_click(driver, locator: tuple, timeout: float = 10) -> None: # EC.element_to_be_clickable 比 visibility 更严格,确保可点 element = WebDriverWait(driver, timeout).until( EC.element_to_be_clickable(locator) ) element.click()

EC.element_to_be_clickable会同时检查元素是否可见和可用,比单纯用find_elementclick稳定得多。定位器尽量用相对路径配合># utils/select_option.py(节选) import random def select_single_choice(options, strategy: str = "first"): if strategy == "random": return random.choice(options) if strategy == "ratio": # ratio 策略:按随机比例选择,频控在调用方维护 return random.choices(options, weights=[1] * len(options), k=1)[0] return options[0]

strategy参数决定选第几个选项或随机选,这对应random_bili.py里维护的「随机比例」。比例控制的典型用途是模拟真实用户分布,避免所有问卷填同一答案,这在问卷测试阶段很重要——用同一答案批量提交会被平台判定为异常数据。翻页模块在点击"下一页"后要等加载动画结束,next_page.py里一般等待某个"当前页完成"标识出现,比如题号序号变化或下一页按钮从禁用变为可用。

4. 跑通一套问卷:从 YAML 配置到提交重试

4.1 先改四个配置文件再运行

config.iniquestionTypeConfiguration.yamlquestion_config.json线性结构脚本配置.ini按需求改一遍。以question_config.json为例,需要写清楚题型、题干、选项和位置。写配置时有一个原则:能用一个统一结构表达的就不写多个分支,这能减少脚本里的条件判断,降低维护成本。

{ "survey_title": "2024 用户满意度调研", "blocks": [ { "block_name": "基本信息", "questions": [ { "type": "danxuan", "title": "您的年龄段", "options": ["18-25", "26-35", "36-45"], "required": true }, { "type": "duoxuan", "title": "您常用的功能", "options": ["搜索", "推荐", "社区"], "max_select": 3 } ] } ], "strategy": { "select_mode": "ratio", "next_page_wait": 6 } }

blocks表示问卷分了几块,脚本会按块逐题处理;strategy.select_mode对应 3.3 里的选择策略;next_page_wait是翻页后的等待秒数。经验是翻页等待不要低于 5 秒,问卷星页面加载是异步的,等太久则每题浪费两秒,对 30 题累计就是 1 分钟。每道题的type必须能在 YAML 映射表里查到,查不到时脚本应立刻报错而不是静默跳过。

4.2 执行入口 ActionWJX 的主流程

ActionWJX.py是主流程,顺序大致是:读取配置 → 初始化驱动 → 登录或复用登录态 → 新建问卷 → 按 blocks 循环创建题目 → 翻页 → 最终提交。拆开看就是一个串行循环,中间夹着异常处理和重试。这个文件是整个脚本的骨架,理解它就能理解其他模块为什么存在。

# conf/main/ActionWJX.py 核心逻辑(简化) def run(): cfg = load_all_configs() # 合并 ini / yaml / json driver = init_driver(cfg["driver"]) # 复用 driver_options wjx = WenjuanxingEditor(driver) # 页面操作的封装类 wjx.create_survey(cfg["survey_title"]) for block in cfg["blocks"]: # 每个 block 先定位题目容器再逐题填充 for question in block["questions"]: handler = load_handler(question["type"]) handler.fill(driver, question) next_page(driver, wait=cfg["strategy"]["next_page_wait"]) submit_survey(driver, retry_times=3)

load_handler在程序里就是查 2.3 的映射表,如果question["type"]不在表里要立刻抛出异常而不是跳过——跳过会让后续题目错位,排查成本更高。submit.py内部包了一层循环:如果点提交后出现网络错误或未提交成功提示,就重试,retry.py控制重试次数和退避间隔。主流程里不建议再加业务逻辑,所有判断题型的细节都收敛到 handler 里,主流程只负责调度和数据传递。

4.3 中途失败怎么恢复

脚本运行中失败是常态,常见场景有三类:题目选项没加载出来、页面弹窗遮挡、网络抖动。第一类靠显式等待解决,第二类需要在点击前关闭已知弹窗,第三类靠重试机制兜底。globalparam.py提供全局标志位,retry.py的退避逻辑如下。

# utils/retry.py import time def retry_call(func, retry_times: int = 3, backoff: float = 2.0): for i in range(retry_times): try: return func() except Exception as exc: if i == retry_times - 1: raise # 指数退避:第二次等 4 秒,第三次等 8 秒 time.sleep(backoff * (2 ** i))

指数退避的核心是"失败次数越多,等待越久",用于应对网络瞬时抖动比固定间隔更有效。如果重试 3 次后仍然失败,脚本会把异常抛到主流程,由ActionWJX.py记录日志后终止。失败时翻日志文件定位到具体题目,改配置重跑即可,不需要从头开始重建问卷。恢复的粒度是块不是题,这是块结构设计的优势——把失败的 block 单独处理,前面的不重复跑,整个脚本维护成本直接下降一个量级。

5. 日志、IP 轮换与地图题的特殊处理

5.1 logurus 统一日志格式

logurus.py是对 logging 的轻量封装,输出统一格式时间 | 级别 | 模块 | 消息,运行日志落到logs/WJX.log。排查问题时先看日志里submit阶段有没有ok标记,没有就顺着时间戳往上找第一个error

# 查看最近一次运行的关键节点 grep -E "block|submit|error" logs/WJX.log | tail -50

tail -50看最近 50 条,基本能覆盖一次完整运行的头部和尾部。日志里每个 block 的开始和结束都打点,题目一旦定位失败,日志会带上题目序号,配合question_config.json里的题目标题,几秒就能定位到是哪道题的问题。建议日志级别默认设为INFO,调试阶段再切DEBUG,不然一次运行会刷出几千行选择器日志。

5.2 IP 轮换在自动化填答里的用途

obtain_ip.pyDaiLiIp.py做的是 IP 轮换:在问卷测试或铺量场景中,单 IP 高频请求会被限流,轮换后每个请求模拟不同出口。这里只讨论合规的业务场景,例如测试不同地域用户看到的问卷差异,不涉及绕过网络限制的任何用法。IP 池的维护要注意可用性检测,失效节点要定时剔除。

# utils/DaiLiIp.py(示意逻辑) import random def get_available_ip(ip_pool: list) -> str: # 从维护的 IP 池里随机取一个,调用前先做连通性探测 candidate = random.choice(ip_pool) if ping_check(candidate): return candidate return get_available_ip(ip_pool)

ping_check只是一个连通性探测,实际项目中会用更精确的 HTTP 请求验证。轮换频率要控制:每 10 次请求换一次 IP,比每题都换更合理,因为问卷星对创建阶段的限流比对提交阶段宽松。脚本里轮换的触发点是automatic_control.py里的计数器,累计到阈值就调一次obtain_ip.py

5.3 地图题等特殊题型的扩展

地图题.py是独立模块,原因是它的交互不是点击选项,而是在地图上选点。通用做法是定位 canvas 地图元素,根据经纬度换算成页面坐标,用ActionChains模拟点击。这类题型是扩展钩子的最好例子,说明这个项目不是写死的,而是预留了题型扩展机制。

from selenium.webdriver.common.action_chains import ActionChains def click_map_point(driver, canvas_locator, x: int, y: int): canvas = driver.find_element(*canvas_locator) # 地图选点题的点击需要落在 canvas 内相对坐标,x/y 由经纬度换算而来 ActionChains(driver).move_to_element_with_offset(canvas, x, y).click().perform()

move_to_element_with_offset以元素左上角为原点按偏移移动,xy来自经纬度投影换算。这类题型没法复用单选逻辑,必须单独注册进题型映射表。如果后续要加排序题、滑块题,做法一致:写一个 handler,注册到questionTypeConfiguration.yaml,复用base_page.py的等待基类,主流程一行都不用动。

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

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

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

立即咨询