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 填空题答案池 + 留空
填空题支持两种玩法:
- 传一个字符串数组作为"答案池",每次提交随机取一条,避免所有答卷的答案一模一样;
- 非必填填空题支持
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"。处理思路:
- 先识别当前页可见的题目并填写;
- 查找"下一页/下一题"按钮,点击后等待页面切换;
- 循环直到没有下一页,再滚动到底部提交;
- 用集合记录已填题号,避免重复填写。
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,欢迎交流。