背景:培训需要定期组织考试,手动从 800 道题库里抽题出卷,一次至少半小时。用 WorkBuddy + Python 做了个自动化工具,点击即出卷。
我们部门每月要组织一次专业培训考试,考试范围是 800 道题的题库。以前出卷的流程是这样的:
整个过程最快也要半小时,如果题目类型要调整、数量要变化,基本重新来一遍。而且有时候要用 A 题库,有时候要用 B 题库(不同题库的 Excel 格式还不完全一样),切换起来更是噩梦。
想做一个自动化工具很久了,但自己从头写 Python 脚本——查文档、调试、处理中文编码问题——想想都觉得烦。后来试了 WorkBuddy,发现一句话描述需求,它就能给你整出来,效率完全不同。
跟 WorkBuddy 聊了几个回合,把需求敲定下来:
功能 | 说明 |
|---|---|
题库读取 | 从 xlsx 文件读取题目,支持单选/多选/判断三种题型 |
多题库切换 | 支持切换不同题库文件,不同格式自动适配 |
随机抽题 | 指定每种题型抽多少道,不重复抽取 |
试卷输出 | 生成格式化的试卷 xlsx + 答案 xlsx |
PDF 导出 | 试卷导出为 PDF,带页眉页脚(单位名称、考试日期) |
这次选型很简单,WorkBuddy 直接帮我锁定了方案:
不需要数据库、不需要 Web 框架,就一个 Python 脚本,双击运行。
A 题库的 Excel 结构(接触网 800 题):
| 序号 | 题型 | 题目 | 选项A | 选项B | 选项C | 选项D | 答案 |
|------|---------|------------------------|---------|---------|---------|---------|------|
| 1 | 单选题 | 接触网额定电压是? | 25kV | 27.5kV | 35kV | 10kV | B |
| 2 | 多选题 | 接触网由哪些组成? | 支柱 | 腕臂 | 接触线 | 钢轨 | ABC |
| 3 | 判断题 | 接触网是高压设备 | — | — | — | — | √ |B 题库的格式略有不同(多了一列"知识点分类"),需要做兼容处理,这是后面的一个坑点。
这是整个系统的基础。WorkBuddy 帮我写了读取函数,并且加了一个「自动探测表头」的逻辑,这样不同格式的题库也能自动适配:
from openpyxl import load_workbook
from pathlib import Path
def load_question_bank(file_path: str) -> dict:
"""读取题库 xlsx,返回按题型分类的题目列表"""
wb = load_workbook(str(Path(file_path)))
ws = wb.active
# 读表头,自动建立列名→列号的映射
headers = {}
for col_idx, cell in enumerate(ws[1], 1):
if cell.value:
headers[cell.value.strip()] = col_idx
# 根据实际列名匹配字段(兼容不同题库的表头叫法)
col_map = {}
for field, aliases in [
('type', ['题型', '题目类型', '类别']),
('question', ['题目', '题干', '问题']),
('A', ['选项A', 'A', '选项 A']),
('B', ['选项B', 'B', '选项 B']),
('C', ['选项C', 'C', '选项 C']),
('D', ['选项D', 'D', '选项 D']),
('answer', ['答案', '正确答案', '参考答案']),
]:
for alias in aliases:
if alias in headers:
col_map[field] = headers[alias]
break
# 按题型归类
banks = {'单选题': [], '多选题': [], '判断题': []}
for row in ws.iter_rows(min_row=2, values_only=False):
qtype = str(row[col_map.get('type', 1) - 1].value or '').strip()
if qtype not in banks:
continue
question = {
'question': str(row[col_map.get('question', 2) - 1].value or ''),
'A': str(row[col_map.get('A', 3) - 1].value or ''),
'B': str(row[col_map.get('B', 4) - 1].value or ''),
'C': str(row[col_map.get('C', 5) - 1].value or ''),
'D': str(row[col_map.get('D', 6) - 1].value or ''),
'answer': str(row[col_map.get('answer', 7) - 1].value or '').strip(),
'type': qtype,
}
banks[qtype].append(question)
return banks这里 WorkBuddy 给了一个很巧妙的思路——用别名表做表头匹配,不同人做的 Excel 表头叫法可能不一样(比如有人写"题型",有人写"题目类型"),用别名映射后不管怎么叫都能对上。
核心就一句话,但细节不少:
import random
def generate_paper(bank: dict, config: dict) -> list:
"""从题库中按配置随机抽取题目"""
questions = []
for qtype, count in config.items():
if qtype not in bank or len(bank[qtype]) < count:
raise ValueError(f"「{qtype}」题库只有 {len(bank.get(qtype, []))} 题,"}
f"要求抽取 {count} 题,不够!")
selected = random.sample(bank[qtype], count)
questions.extend(selected)
# 打乱题型顺序,让试卷看起来自然
random.shuffle(questions)
return questions这里有个小细节:如果题库里某类题型不够抽,直接报错而不是默默少抽——这是 WorkBuddy 建议加的,避免生成不完整的试卷。
生成的试卷要排版清爽,可以直接打印:
from openpyxl import Workbook
from openpyxl.styles import Font, Alignment, Border, Side
def export_paper(questions: list, output_path: str, title: str = "专业培训考试试卷"):
"""将抽取的题目输出为格式化的试卷 xlsx"""
wb = Workbook()
ws = wb.active
ws.title = "试卷"
thin_border = Border(
left=Side(style='thin'),
right=Side(style='thin'),
top=Side(style='thin'),
bottom=Side(style='thin')
)
# 标题行
ws.merge_cells('A1:F1')
title_cell = ws.cell(row=1, column=1, value=title)
title_cell.font = Font(size=16, bold=True)
title_cell.alignment = Alignment(horizontal='center', vertical='center')
# 信息行
info_row = 2
ws.cell(row=info_row, column=1, value="姓名:").font = Font(size=11)
ws.cell(row=info_row, column=3, value="部门:").font = Font(size=11)
ws.cell(row=info_row, column=5, value="得分:").font = Font(size=11)
question_num = {'单选题': 0, '多选题': 0, '判断题': 0, '总计': 0}
row = 4 # 从第4行开始输出题目
for q in questions:
question_num['总计'] += 1
question_num[q['type']] += 1
# 题目
ws.merge_cells(start_row=row, start_column=1, end_row=row, end_column=6)
num = question_num[q['type']]
if q['type'] == '单选题':
prefix = f"{question_num['总计']}. 【单选】"
elif q['type'] == '多选题':
prefix = f"{question_num['总计']}. 【多选】"
else:
prefix = f"{question_num['总计']}. 【判断】"
qcell = ws.cell(row=row, column=1, value=f"{prefix} {q['question']}")
qcell.font = Font(size=11)
qcell.alignment = Alignment(wrap_text=True)
ws.row_dimensions[row].height = 25
row += 1
# 选项(判断题不显示选项)
if q['type'] != '判断题':
for opt in ['A', 'B', 'C', 'D']:
if q.get(opt):
ws.cell(row=row, column=1, value=f" {opt}. {q[opt]}").font = Font(size=11)
row += 1
row += 1 # 题间空行
# 单独输出答案表
ws_ans = wb.create_sheet("参考答案")
ws_ans.cell(row=1, column=1, value="题号").font = Font(bold=True)
ws_ans.cell(row=1, column=2, value="题型").font = Font(bold=True)
ws_ans.cell(row=1, column=3, value="答案").font = Font(bold=True)
for i, q in enumerate(questions, 1):
ws_ans.cell(row=i+1, column=1, value=i)
ws_ans.cell(row=i+1, column=2, value=q['type'])
ws_ans.cell(row=i+1, column=3, value=q['answer'])
wb.save(output_path)现象: 题库文件放在 题库/接触网题库.xlsx 这种中文路径下,load_workbook() 直接报错。
排查过程: 把文件移到英文路径就没问题,确认是中文路径的问题。
WorkBuddy 给出的方案:
# ❌ 会报错
wb = load_workbook("题库/接触网题库.xlsx")
# ✅ 用 pathlib.Path 包装一下
from pathlib import Path
wb = load_workbook(str(Path("题库/接触网题库.xlsx")))同样的问题也出现在 wb.save() 时,统一用 Path() 处理搞定。
A 题库的表头是 | 序号 | 题型 | 题目 | 选项A | ... |,B 题库多了一列 "知识点",而且"题目"叫"题干"。
WorkBuddy 说直接用之前写的那个别名表自动匹配就能解决,果然没问题。适配代码在前面 3.1 节已经贴了,核心就是 aliases 表。
第一次跑的时候报了错:「判断题」题库只有 15 题,要求抽取 20 题。回去一翻题库,判断题确实一共就 15 道。
WorkBuddy 给的建议是在配置文件里加一个校验,抽题前先检查题库够不够,不够就自动调整:
def smart_config(bank: dict, desired: dict) -> dict:
"""智能调整抽题配置:如果某题型不够,自动降到题库上限"""
adjusted = {}
for qtype, count in desired.items():
available = len(bank.get(qtype, []))
adjusted[qtype] = min(count, available)
if adjusted[qtype] < count:
print(f"⚠️ {qtype} 题库仅有 {available} 题,已自动调整为 {adjusted[qtype]} 题")
return adjusted用 JSON 配置,改抽题数量不用改代码:
{
"题库路径": "题库/接触网题库.xlsx",
"抽题配置": {
"单选题": 40,
"多选题": 20,
"判断题": 15
},
"试卷标题": "接触网专业培训考试试卷",
"输出目录": "output/"
}@echo off
cd /d %~dp0
python paper_generator.py
echo.
echo 试卷已生成在 output 目录下
pause双击 生成试卷.bat → 3 秒出卷 → output/ 目录下拿到试卷 xlsx + 答案 xlsx。
当然也有局限性:生成出来的代码不会一次性完美,偶尔需要微调——但比起从零开始写,效率已经不是一个维度了。
如果你也在做类似的内部小工具,强烈推荐试试 Python + openpyxl + WorkBuddy 这个组合。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。