Python + Playwright 实现问卷星自动化填写

Python + Playwright 实现问卷星自动化填写:从自动解析到按比例批量填写

本文将分享一个基于 Python + Playwright 的问卷星自动化工具的实现思路与完整代码,涵盖问卷结构解析、隐藏控件的点击处理、按权重比例填写、多选题概率控制、多页问卷翻页,以及调用 DeepSeek 自动生成建议占比等实用技巧。

一、前言

最近在做一个问卷相关的小工具,需要批量测试问卷的填写流程。手动一份一份填显然不现实,于是我用 Python + Playwright 写了一套问卷星自动化脚本,顺手把过程中踩的坑和解决思路整理成这篇文章。

项目链接

这套工具的主要能力:

  • 自动识别问卷中的题型:单选、多选、填空、下拉、矩阵(量表)、评分星;
  • 按权重比例填写:单选题按占比分布(比如男 40% / 女 60%),多选题按每个选项的"被选中概率"独立决定;
  • 批量提交 N 份,结束后自动统计每题的实际分布;
  • 自动解析任意问卷链接并生成配置文件,可选调用 DeepSeek 生成符合人群分布的建议比例和填空题答案池;
  • 支持多页问卷自动翻页、提交前必填校验、失败才截图。

使用须知:本工具仅适用于填写自己创建或已获授权的问卷,请勿用于伪造数据、刷票或干扰他人问卷。

二、技术栈

组件 说明
Python 3.9+ 主开发语言
Playwright 浏览器自动化框架,支持 Chrome / Edge
DeepSeek API OpenAI 兼容接口,用于生成建议比例(可选)
标准库 urllib 调用 DeepSeek API,避免额外依赖

选择 Playwright 而不是 Selenium 的原因:安装简单、自带等待机制、locator API 更现代,而且可以直接复用系统已安装的 Chrome/Edge,国内用户不需要额外下载浏览器内核。

三、核心实现思路

3.1 问卷结构解析:不依赖固定的 class 名

问卷星有多个模板,class 命名差异很大。比如手机版模板的题目容器是 div.field(带 data-role="fieldcontain"),经典模板是 fieldset,还有一种模板用 .qnr-question。所以解析时按优先级依次尝试:

javascript 复制代码
let containers = Array.from(document.querySelectorAll('div.field[data-role="fieldcontain"]'));
if (!containers.length) {
  containers = Array.from(document.querySelectorAll('div.field'));
}
if (!containers.length) {
  containers = Array.from(document.querySelectorAll('fieldset'));
}
if (!containers.length) {
  containers = Array.from(document.querySelectorAll('.qnr-question, .div_question'));
}

每个容器内部,再根据控件类型判定题型:

javascript 复制代码
const radios = c.querySelectorAll('input[type="radio"]');
const checks = c.querySelectorAll('input[type="checkbox"]');
const selects = c.querySelectorAll('select');
const textareas = c.querySelectorAll('textarea');

如果容器内存在多个不同 name 的 radio/checkbox,就判定为矩阵题(量表题)。

3.2 最大的坑:输入框全是 display:none

问卷星手机版模板里,所有 radio/checkbox 的 <input> 都是 display:none,页面上显示的是旁边的 <a class="jqradio"><div class="label">。如果像常规做法一样"只筛选可见元素",会发现一道题都识别不到。

解决办法是不做可见性过滤 ,只排除 type="hidden" 的输入框;点击时依次尝试多个可见的点击目标:

python 复制代码
def click_option(c, input_type, idx):
    input_loc = c.locator(f'input[type="{input_type}"]').nth(idx - 1)
    candidates = [
        input_loc.locator("xpath=ancestor::label[1]"),          # 经典模板:label 包裹
        input_loc.locator("xpath=following-sibling::a[1]"),     # 手机版:a.jqradio
        input_loc.locator('xpath=ancestor::span[contains(@class,"wrapper")][1]'),
        input_loc.locator('xpath=ancestor::div[contains(@class,"ui-radio") or contains(@class,"ui-checkbox")][1]//div[contains(@class,"label")][1]'),
        input_loc.locator('xpath=following-sibling::div[contains(@class,"label")][1]'),
    ]
    for loc in candidates:
        if loc.is_visible(timeout=1000):
            loc.click()
            if input_loc.is_checked():
                return True
    # 兜底:强制勾选
    input_loc.check(force=True)
    return input_loc.is_checked()

每次点击后都回读 is_checked() 确认生效,失败就换下一个候选目标,最后用 check(force=True) 兜底。

3.3 按权重比例填写

单选题用"加权随机":每个选项给一个权重,按权重占比随机选一个。权重不要求和为 100,代码会自动归一化:

python 复制代码
def weighted_index(weights, options, rng):
    entries = []
    for i, o in enumerate(options):
        w = float(weights.get(o["label"], weights.get(str(i + 1), 0)))
        if w > 0:
            entries.append((i + 1, w))
    total = sum(w for _, w in entries)
    r = rng.random() * total
    for idx, w in entries:
        r -= w
        if r <= 0:
            return idx
    return entries[-1][0]

多选题则把每个选项的权重当成"被选中的概率"(0-100),独立随机决定是否勾选,并保证至少选一个:

python 复制代码
def weighted_checkbox_picks(weights, options, rng):
    picks = []
    for i, o in enumerate(options):
        w = float(weights.get(o["label"], 0))
        if w <= 1:
            w *= 100
        if rng.random() * 100 < w:
            picks.append(i + 1)
    if not picks:  # 至少选一个:选权重最高的
        picks.append(max(range(len(options)), key=lambda i: weights.get(options[i]["label"], 0)) + 1)
    return picks

3.4 填空题答案池 + 留空

填空题支持两种玩法:

  1. 传一个字符串数组作为"答案池",每次提交随机取一条,避免所有答卷的答案一模一样;
  2. 非必填填空题支持 blank_rate 留空概率,比如 20% 的概率留空。
python 复制代码
if blank_rate > 0 and not q.get("required") and rng.random() * 100 < blank_rate:
    v = ""  # 留空
else:
    v = rng.choice(answers)  # 随机取一条

必填题强制填答案,否则问卷星会拦截提交。

3.5 多页问卷自动翻页

问卷星的多页问卷在 DOM 里通常分多个 fieldset,后面的页 style="display:none"。处理思路:

  1. 先识别当前页可见的题目并填写;
  2. 查找"下一页/下一题"按钮,点击后等待页面切换;
  3. 循环直到没有下一页,再滚动到底部提交;
  4. 用集合记录已填题号,避免重复填写。
python 复制代码
while True:
    struct = collect_structure(page)
    for q in struct["questions"]:
        if q["n"] in filled_ns:
            continue
        container = page.locator(struct["containerSelector"]).nth(q["rawIndex"])
        if not container.is_visible(timeout=1000):
            continue  # 当前页不可见,等翻页后再填
        # ... 填写逻辑 ...
        filled_ns.add(q["n"])
    next_btn = find_next_button(page)  # 查找"下一页/下一题"
    if next_btn is None:
        break
    next_btn.click()

3.6 提交前的必填校验

为了避免"没填就点提交、卡在页面校验"的问题,提交前会检查所有必填题是否都填成功,有未填的就不提交并截图,方便排查:

python 复制代码
if unfilled:
    print(f"[警告] 有 {len(unfilled)} 道题未填成功,本次不提交")
    take_shot(page, cfg, "unfilled")
    return False

3.7 调用 DeepSeek 自动生成建议比例

解析工具 parse_survey.py 在识别完题目后,可以把题目清单发给 DeepSeek,让它按"公开调研常识"给出每道题的占比:

  • 单选题:各选项权重之和等于 100;
  • 多选题:每项给出被选中概率(0-100);
  • 填空题:生成 10-20 条简短候选答案。
python 复制代码
payload = {
    "model": "deepseek-chat",
    "messages": [
        {"role": "system", "content": "你是一名问卷数据分析专家,只输出 JSON。"},
        {"role": "user", "content": prompt},
    ],
    "response_format": {"type": "json_object"},
}

API Key 放在 .env 文件中(已被 gitignore 忽略),代码里从环境变量或 .env 读取,不硬编码。

3.8 结果分布统计

批量刷完后,脚本会把每题各选项的出现次数和百分比打印出来,方便核对实际分布是否符合设定的权重:

text 复制代码
===== 答案分布统计(共 20 份) =====
3.你的性别: 女 75.0%(15), 男 25.0%(5)

四、快速开始

4.1 安装依赖

bash 复制代码
pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple

如果本机没有 Chrome/Edge,再执行 playwright install chromium(国内可用 npmmirror 加速)。

4.2 解析问卷并生成配置

bash 复制代码
python parse_survey.py --url "https://v.wjx.cn/vm/xxxxxxxx.aspx" --out config.json --ai

--ai 会调用 DeepSeek 生成建议比例和填空题答案池;不加 --ai 则使用默认等权重。

4.3 开始填写

bash 复制代码
# 先有头模式试一次
python fill_wjx.py --config config.json --headed

# 确认无误后批量
python fill_wjx.py --config config.json --headless --count 100

五、配置文件详解

json 复制代码
{
  "url": "https://v.wjx.cn/vm/xxxxxxxx.aspx",
  "count": 10,
  "headless": false,
  "delay_min_ms": 800,
  "delay_max_ms": 2000,
  "timeout_ms": 30000,
  "rules": [
    { "match": "性别", "weights": { "男": 48, "女": 52 } },
    { "match": ".*爱好.*", "weights": { "阅读": 70, "运动": 50 } },
    { "match": "建议", "value": ["答案1", "答案2", "答案3"], "blank_rate": 20 }
  ]
}
字段 说明
match 正则匹配题目文字
weights 单选:占比(自动归一化);多选:每项被选中概率
value 填空题固定答案、答案池数组或 "random"
blank_rate 非必填填空题留空概率(0-100)
unmatched 未匹配题目的处理:random 随机填 / skip 跳过
count 提交份数

六、常见问题

1. 遇到验证码/滑块怎么办?

脚本不绕过验证码,改用 --headed 有头模式,手动拖一下滑块,脚本会继续等待提交成功。

2. 提示"未能识别到问卷题目"?

问卷可能已结束、需要登录,或页面模板特殊。用 --debug 输出识别结构排查。

3. 提交后提示未成功?

通常是必填题没填上,查看 screenshots 目录的失败截图,补对应规则。

4. 批量刷被平台拦截?

平台有 IP 频次、答题时长等风控。调大 delay、减少 count,并且只用于合规场景。

七、结语

这套工具的核心价值在于"通用":不管问卷是什么模板、什么题型,都能自动识别并按配置的比例填写;配合 DeepSeek 自动生成占比,换新问卷只需一条命令。

最后再次强调:自动化工具是双刃剑,请只在自己创建或已获授权的问卷上使用,不要用来伪造调查数据或干扰他人问卷。技术本身无罪,用途决定价值。


项目完整代码见 github.com/Ggbikun/wjx,欢迎交流。

相关推荐
无凭1 小时前
字节跳动 DeerFlow:Agent Harness 怎么让大模型主动向用户提问?
人工智能·python
Zane19941 小时前
多开几个线程,为什么算数字反而没变快?一文讲透 CPython 的 GIL
后端·python
W_326001 小时前
Python-OpenCV边缘检测与阈值分割:Sobel、Scharr、Laplacian、Canny、全局与自适应阈值
开发语言·图像处理·python·opencv·机器学习
青 春 记 忆2 小时前
LeetCode 121. 买卖股票的最佳时机|Python 解法详解
python·算法·leetcode
满怀冰雪2 小时前
21-图像分类实战:从 MNIST 到 CIFAR-10
人工智能·python·深度学习·分类·数据挖掘·paddlepaddle
Logintern092 小时前
Celery 的底层架构正是进程池、事件循环、epoll 和协程全部组合在了一起
python·架构·消息队列·进程·celery·事件循环
用户0332126663672 小时前
在 Word 文档快速中插入图片【Python 教程】
python
盖伦发发2 小时前
RAG 能跑≠能用:用 EDD 把 Eval 做成基础设施 (附源码)
人工智能·后端·python·功能测试
郝学胜_神的一滴2 小时前
Python 高级编程 028:字典dict从入门到精通
python·ai编程