
简介一份面向问卷星高频设计场景的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读 INIyaml_control.py用PyYAML读 YAMLsetting.py负责把 JSON 加载成全局字典。这样拆分之后改问卷内容不需要碰代码改运行环境不需要碰题目数据。# utils/read_config.py常见做法configparser 默认值兜底 import configparser def load_ini(path: str, section: str DEFAULT) - dict: parser configparser.ConfigParser() parser.read(path, encodingutf-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.0parser.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 里写danxuanJSON 配置里写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(--headlessnew) # 新版 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 optsexcludeSwitches里enable-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_element再click稳定得多。定位器尽量用相对路径配合># 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), k1)[0] return options[0]strategy参数决定选第几个选项或随机选这对应random_bili.py里维护的「随机比例」。比例控制的典型用途是模拟真实用户分布避免所有问卷填同一答案这在问卷测试阶段很重要——用同一答案批量提交会被平台判定为异常数据。翻页模块在点击下一页后要等加载动画结束next_page.py里一般等待某个当前页完成标识出现比如题号序号变化或下一页按钮从禁用变为可用。4. 跑通一套问卷从 YAML 配置到提交重试4.1 先改四个配置文件再运行把config.ini、questionTypeConfiguration.yaml、question_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, waitcfg[strategy][next_page_wait]) submit_survey(driver, retry_times3)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 -50tail -50看最近 50 条基本能覆盖一次完整运行的头部和尾部。日志里每个 block 的开始和结束都打点题目一旦定位失败日志会带上题目序号配合question_config.json里的题目标题几秒就能定位到是哪道题的问题。建议日志级别默认设为INFO调试阶段再切DEBUG不然一次运行会刷出几千行选择器日志。5.2 IP 轮换在自动化填答里的用途obtain_ip.py与DaiLiIp.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以元素左上角为原点按偏移移动x和y来自经纬度投影换算。这类题型没法复用单选逻辑必须单独注册进题型映射表。如果后续要加排序题、滑块题做法一致写一个 handler注册到questionTypeConfiguration.yaml复用base_page.py的等待基类主流程一行都不用动。本文还有配套的精品资源点击获取