TopQB答题系统
2020/01/05
@pingfan
功能:
1、多人同时答题系统
2、在线查看个人得分与答题情况(解析)
3、载入题库,随机抽取题目支持【单选题,多选题,判断题】
4、自定义题目数量与题目分值,题目都是随机但数量不变总分不变
5、在线实时查看总体得分情况,可导出excel
6、适应场景: 各类考试答题等
7、提示:
1、不需要外网,任意内网(局域网)机器运行均可,也可单机运行使用
2、无毒,请添加信任
3、系统要求: 64位win7、win10(当然linux是最好的)
使用说明:
1、题库模板位于【static】->【qb.xlsx】题目索引值不可重复
2、运行app.exe
3、查看本机ip地址
4、访问:
1、答题地址:
http://【本机ip】:9999
如:http://192.168.1.2:9999
2、设置地址:
http://【本机ip】:9999/config
3、得分地址:
http://【本机ip】:9999/results
4、管理地址:
http://【本机ip】:9999/admin
5、答题前请设置题目数量,题目得分
6、实时查看总体得分情况刷新成绩页面即可
==========================================================================================
管理页面
设置页面
成绩汇总页面
答题页面
解析页面
==========================================================================================
小小flask项目,就几个路由系统,没使用蓝图。
完整目录结构
不使用数据库,轻应用读取excel中的题目缓存到内存中
需要一个excel.xlsx(2007以上的题库模板)放置到目录的static文件夹中命名为【qb.xlsx】(读取的是第一个工作表)
==========================================================================================
只依赖flask,提升并发的(协程)gevent库,以及pfExcel(基于openpyxl的封装)用于读取excel中的数据(前面的博客有提到)
前端没有使用任何框架,js都是原生
以下是依赖库的安装
# 安装flask
pip install flask
# 安装gevent
pip install gevent
# 安装openpyxl
pip install openpyxl
部署就是一个(pyinstaller库编译app.py)exe执行文件
# 安装pyinstaller
pip install pyinstaller
# 编译app.py
pyinstaller -F -i 你的logo文件.ico app.py
以下是pyExcel.py源码
"""
基于openpyxl的excel读写模块
提供更简单的读写方式
"""
from openpyxl import load_workbook, Workbook
class ExcelWork:
def __init__(self, filePath):
"""
初始化, 加载excel,默认选择第一个工作表
:param filePath: str: 文件地址
"""
self.filePath = filePath
try:
# 加载excel
self.excel = load_workbook(self.filePath)
except FileNotFoundError:
# 创建excel
self.excel = Workbook(self.filePath)
# 创建sheet
self.createSheet('Sheet1')
# 保存excel
self.close()
# 加载excel
self.excel = load_workbook(self.filePath)
# sheet设置为第一个工作表
self.sheet = self.excel.active
def close(self):
"""
保存并退出
:return:
"""
self.excel.save(self.filePath)
def createSheet(self, sheetName):
"""
创建工作表
:param sheetName: str: 工作表名
:return:
"""
self.excel.create_sheet(sheetName)
def getSheetTitle(self):
"""
获取当前工作表名称
:return: str: 工作表名称
"""
return self.sheet.title
def getSheetTitles(self):
"""
获取excel所有工作表的名称
:return: list: [工作表名称,]
"""
return self.excel.sheetnames
def delSheet(self):
"""
删除当前工作表
:return:
"""
self.excel.remove(self.sheet)
def selectSheet(self, sheetName):
"""
选择工作表, 如果没有将创建
:param sheetName: str: 工作表名
:return:
"""
if sheetName in self.getSheetTitles():
# 选择工作表
self.sheet = self.excel[sheetName]
else:
# 创建工作表
self.createSheet(sheetName)
# 选择工作表
self.sheet = self.excel[sheetName]
def setCell(self, r, c, var):
"""
修改指定行, 列的单元格内容
:param r: int: 行数
:param c: int: 列数
:param var: str: 修改内容
:return:
"""
self.sheet.cell(row=r, column=c, value=var)
def getCell(self, r, c):
"""
获取指定行, 列的单元格内容
:param r: int: 行数
:param c: int: 列数
:return: str: 单元格内容
"""
return self.sheet.cell(row=r, column=c).value
def getRow(self, r):
"""
获取指定行所有数据
:param r: int: 行数
:return: list: [数据,]
"""
rowList = []
for cell in self.sheet[r]:
rowList.append(cell.value)
return rowList
def getColumn(self, c):
"""
获取指定列所有数据
:param c: int: 列数
:return: list: [数据,]
"""
columnList = []
for temp in range(1, self.sheet.max_row + 1):
columnList.append(self.getCell(temp, c))
return columnList
以下是qb.py源码
"""
用于操作题库数据
"""
import socket
from random import shuffle
from pfExcel import ExcelWork
# 问题设置
QUESTION_CONFIG = {
'radioNum': 1,
'checkboxNum': 1,
'trueOrFalseNum': 1,
'radioResult': 1,
'checkboxResult': 1,
'trueOrFalseResult': 1
}
def loadQb(qbPath):
"""
加载题库数据
:param qbPath: list: 题库地址
:return: list: [[单选题,], [多选题,], [判断题,]]
"""
# 单选题列表
radioList = []
# 多选题列表
checkboxList = []
# 判断题列表
trueOrFalseList = []
# 读取excel
excel = ExcelWork(qbPath)
# 获取所有数据
for index, _ in enumerate(excel.getColumn(1)):
# 每行数据: ['索引', '题型', '题目', '选项A', '选项B', '选项C', '选项D', '选项E', '选项F', '答案']
rowList = excel.getRow(index + 1)[0:10]
# 根据题型划分
if rowList[1] == '单选题':
radioList.append(rowList)
elif rowList[1] == '多选题':
checkboxList.append(rowList)
elif rowList[1] == '判断题':
trueOrFalseList.append(rowList)
return [radioList, checkboxList, trueOrFalseList]
def randomQbData(qbData):
"""
随机题库数据直接改变原始数据
:param qbData: list: [[单选题,], [多选题,], [判断题,]]
:return:
"""
for tempData in qbData:
# 打乱列表数据的顺序
shuffle(tempData)
def getUserQbData(qbData):
"""
获取用户题库数据并保存
:param qbData: list: [[单选题,], [多选题,], [判断题,]]
:return: list: [[题],]
"""
radioList = qbData[0][:int(QUESTION_CONFIG['radioNum'])]
checkboxList = qbData[1][:int(QUESTION_CONFIG['checkboxNum'])]
trueOrFalseList = qbData[2][:int(QUESTION_CONFIG['trueOrFalseNum'])]
return radioList + checkboxList + trueOrFalseList
def listToStr(dataList):
"""
将列表中的值提取出来转为字符串
:param dataList: list
:return: str
"""
tempStr = ''
for temp in dataList:
tempStr = tempStr + str(temp)
return tempStr
def checkQbData(userQbData):
"""
检查答案与用户作答数据进行比对为列表添加结果
:param userQbData: list: [[索引, 题型, 题目, 选项A, 选项B, 选项C, 选项D, 选项E, 选项F, 答案, 用户作答],]
:return: list: [[索引, 题型, 题目, 选项A, 选项B, 选项C, 选项D, 选项E, 选项F, 答案, 用户作答, 正确|错误],]
"""
for qb in userQbData:
# 比对结果
if qb[9] == qb[10]:
qb.append('正确')
else:
qb.append('错误')
def getUserResult(userQbData):
"""
获取用户成绩
:param userQbData: list: [[索引, 题型, 题目, 选项A, 选项B, 选项C, 选项D, 选项E, 选项F, 答案, 用户作答, 正确|错误],]
:return: float: 用户成绩
"""
result = 0
for temp in userQbData:
if temp[-1] == '正确':
if temp[1] == '单选题':
result = result + float(QUESTION_CONFIG['radioResult'])
elif temp[1] == '多选题':
result = result + float(QUESTION_CONFIG['checkboxResult'])
elif temp[1] == '判断题':
result = result + float(QUESTION_CONFIG['trueOrFalseResult'])
return result
def getIp():
"""
查询本机ip地址
:return: str: 本机ip
"""
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.connect(('8.8.8.8', 80))
ip = s.getsockname()[0]
s.close()
return ip
以下是app.py源码
from gevent import monkey
monkey.patch_all() # 猴子补丁
from gevent import pywsgi
from copy import deepcopy
import webbrowser
from flask import Flask, request, render_template, redirect
from qb import loadQb, randomQbData, getUserQbData, listToStr, checkQbData, getUserResult, getIp, QUESTION_CONFIG
# app实例
app = Flask(__name__)
"""
缓存
QB_DATA缓存excel表格题库数据
USER_QB_DATA缓存用户随机的题库数据
"""
# 题库数据缓存[[单选题,], [多选题,], [判断题,]]
QB_DATA = loadQb('static/qb.xlsx')
# 用户题库数据缓存 {ip: [单选题, 多选题, 判断题],}
USER_QB_DATA = {}
# 成绩缓存[[name, class, result],]
RESULT_DATA = []
# 打开管理页面
webbrowser.open('http://127.0.0.1:9999/admin')
@app.route('/', methods=['GET', 'POST'])
def question():
"""
答题|解析
:return:
"""
if request.method == 'GET':
# 深拷贝数据,保证原始数据不被修改
data = deepcopy(QB_DATA)
# 随机题库数据
randomQbData(data)
# 获取用户题库数据
userQbData = getUserQbData(data)
# 缓存用户题库数据 {ip: [[单选题],[多选题],[判断题],],}
USER_QB_DATA[request.remote_addr] = userQbData
# 组合数据
cont = {
'data': userQbData
}
return render_template('question.html', **cont)
else:
# 获取用户
name = request.form.get('name')
# 获取班级
qcls = request.form.get('qcls')
# 获取用户题库缓存数据
userQbData = USER_QB_DATA[request.remote_addr]
# 清除用户缓存
del USER_QB_DATA[request.remote_addr]
# 用户题库缓存数据后追加用户答题结果
for qb in userQbData:
qb.append(listToStr(request.form.getlist(str(qb[0]))))
# 检查答题结果
checkQbData(userQbData)
# 获取成绩
result = getUserResult(userQbData)
# 添加成绩缓存
RESULT_DATA.append([name, qcls, result])
# 组合数据
cont = {
'data': userQbData,
'name': name,
'qcls': qcls,
'result': result
}
return render_template('result.html', **cont)
@app.route('/admin')
def admin():
cont = {
'url': f'http://{getIp()}:9999'
}
return render_template('admin.html', **cont)
@app.route('/results')
def results():
"""
成绩
:return:
"""
cont = {
'data': RESULT_DATA
}
return render_template('results.html', **cont)
@app.route('/config', methods=['GET', 'POST'])
def config():
"""
设置
:return:
"""
if request.method == 'GET':
cont = {
'data': QUESTION_CONFIG
}
return render_template('config.html', **cont)
else:
data = request.form.to_dict()
for k, v in data.items():
QUESTION_CONFIG[k] = v
return redirect('/config')
if __name__ == '__main__':
print('-----答题系统服务已启动-----\n')
print('-----后台管理-----\n')
print('http://127.0.0.1:9999/admin\n')
print('#' * 30)
server = pywsgi.WSGIServer(('0.0.0.0', 9999), app)
server.serve_forever()
==========================================================================================
前端页面放置到templates文件夹中
以下是admin.html页面源码
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<title>答题系统</title>
<style>
* {
margin: 0;
padding: 0;
}
ul {
list-style: none;
}
.main {
width: 800px;
margin: 0 auto;
text-align: center;
overflow: hidden;
}
.title {
margin-top: 30px;
font-size: 30px;
font-weight: bold;
}
.info {
margin-top: 30px;
}
.info li {
margin-top: 10px;
}
.url{
font-weight: bold;
margin-top: 30px;
}
</style>
</head>
<body>
<div class="main">
<div class="title">答题系统</div>
<div class="url">答题地址:{{ url }}</div>
<ul class="info">
<li><a href="/config" target="_blank">设置</a></li>
<li><a href="/results" target="_blank">成绩</a></li>
</ul>
</div>
</body>
</html>
以下是config.html源码
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<title>答题系统</title>
<style>
* {
margin: 0;
padding: 0;
}
ul {
list-style: none;
}
.main {
width: 800px;
margin: 0 auto;
text-align: center;
overflow: hidden;
}
.title {
margin-top: 30px;
font-size: 30px;
font-weight: bold;
}
.info {
margin-top: 30px;
}
.info li {
margin-top: 10px;
}
.sub {
margin: 20px;
}
</style>
</head>
<body>
<div class="main">
<div class="title">答题系统</div>
<form action="/config" method="post">
<ul class="info">
<li>
<label>单选题数量:<input type="text" name="radioNum" value="{{ data['radioNum'] }}"></label>
</li>
<li>
<label>多选题数量:<input type="text" name="checkboxNum" value="{{ data['checkboxNum'] }}"></label>
</li>
<li>
<label>判断题数量:<input type="text" name="trueOrFalseNum" value="{{ data['trueOrFalseNum'] }}"></label>
</li>
<li>
<label>单选题分值:<input type="text" name="radioResult" value="{{ data['radioResult'] }}"></label>
</li>
<li>
<label>多选题分值:<input type="text" name="checkboxResult" value="{{ data['checkboxResult'] }}"></label>
</li>
<li>
<label>判断题分值:<input type="text" name="trueOrFalseResult"
value="{{ data['trueOrFalseResult'] }}"></label>
</li>
</ul>
<div class="sub"><input type="submit" value="设置"></div>
</form>
</div>
</body>
</html>
以下是results.html源码
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<title>答题系统</title>
<style>
* {
margin: 0;
padding: 0;
}
ul {
list-style: none;
}
.main {
width: 800px;
margin: 0 auto;
text-align: center;
overflow: hidden;
}
.title {
margin-top: 30px;
font-size: 30px;
font-weight: bold;
}
.get {
margin-top: 20px;
}
.result{
width: 400px;
margin: 20px auto;
}
</style>
</head>
<body>
<div class="main">
<div class="title">答题系统</div>
<div class="get">
<button onclick="copyTable()">导出</button>
</div>
<table class="result">
<tr>
<td>姓名</td>
<td>班级</td>
<td>成绩</td>
</tr>
{% for tempList in data %}
<tr>
{% for temp in tempList %}
<td>{{ temp }}</td>
{% endfor %}
</tr>
{% endfor %}
</table>
</div>
<script>
//将table中的数据放置粘贴板
function copyTable() {
//获取table数据
let data = '';
let tb = document.getElementsByTagName("table")[0];
let rows = tb.rows;
for (let i = 0; i < rows.length; i++) {
let cells = rows[i].cells;
for (let j = 0; j < cells.length; j++) {
data = data + cells[j].textContent + '\t';
}
data = data.slice(0, -1) + '\n';
}
data = data.slice(0, -1);
//将数据添加至粘贴板
let textArea = document.createElement("textarea");
textArea.value = data;
document.body.appendChild(textArea);
textArea.select();
try {
document.execCommand('copy')
} catch (err) {
alert('导出失败')
}
document.body.removeChild(textArea);
alert('数据已导出找个表格粘贴吧~')
}
</script>
</body>
</html>
以下是question.html源码
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<title>答题系统</title>
<style>
* {
margin: 0;
padding: 0;
}
ul {
list-style: none;
}
.main {
width: 800px;
margin: 0 auto;
text-align: center;
overflow: hidden;
}
.title {
margin-top: 30px;
font-size: 30px;
font-weight: bold;
}
.info {
margin-top: 30px;
}
.info li {
margin-top: 10px;
}
.question {
width: 400px;
margin: 30px auto;
text-align: left;
}
.questionType {
font-size: 18px;
font-weight: bold;
}
.questionTitle {
margin-top: 10px;
font-size: 18px;
}
.option {
margin-top: 10px;
}
.sub {
margin: 20px;
}
</style>
</head>
<body>
<div class="main">
<div class="title">答题系统</div>
<form action="/" method="post">
<ul class="info">
<li>
<label>姓名:<input type="text" name="name"></label>
</li>
<li>
<label>班级:<input type="text" name="qcls"></label>
</li>
</ul>
{% for temp in data %}
{# 判断题型 #}
{% if temp[1] != '多选题' %}
{# ['索引', '题型', '题目', '选项A', '选项B', '选项C', '选项D', '选项E', '选项F', '答案'] #}
<ul class="question">
<li class="questionType">{{ temp[1] }}</li>
<li class="questionTitle">{{ temp[2] }}</li>
{% if temp[3] !=None %}
<li class="option">
<label><input type="radio" name="{{ temp[0] }}" value="A">{{ temp[3] }}</label>
</li>
{% endif %}
{% if temp[4] !=None %}
<li class="option">
<label><input type="radio" name="{{ temp[0] }}" value="B">{{ temp[4] }}</label>
</li>
{% endif %}
{% if temp[5] !=None %}
<li class="option">
<label><input type="radio" name="{{ temp[0] }}" value="C">{{ temp[5] }}</label>
</li>
{% endif %}
{% if temp[6] !=None %}
<li class="option">
<label><input type="radio" name="{{ temp[0] }}" value="D">{{ temp[6] }}</label>
</li>
{% endif %}
{% if temp[7] !=None %}
<li class="option">
<label><input type="radio" name="{{ temp[0] }}" value="E">{{ temp[7] }}</label>
</li>
{% endif %}
{% if temp[8] !=None %}
<li class="option">
<label><input type="radio" name="{{ temp[0] }}" value="F">{{ temp[8] }}</label>
</li>
{% endif %}
</ul>
{% else %}
<ul class="question">
<li class="questionType">{{ temp[1] }}</li>
<li class="questionTitle">{{ temp[2] }}</li>
{% if temp[3] !=None %}
<li class="option">
<label><input type="checkbox" name="{{ temp[0] }}" value="A">{{ temp[3] }}</label>
</li>
{% endif %}
{% if temp[4] !=None %}
<li class="option">
<label><input type="checkbox" name="{{ temp[0] }}" value="B">{{ temp[4] }}</label>
</li>
{% endif %}
{% if temp[5] !=None %}
<li class="option">
<label><input type="checkbox" name="{{ temp[0] }}" value="C">{{ temp[5] }}</label>
</li>
{% endif %}
{% if temp[6] !=None %}
<li class="option">
<label><input type="checkbox" name="{{ temp[0] }}" value="D">{{ temp[6] }}</label>
</li>
{% endif %}
{% if temp[7] !=None %}
<li class="option">
<label><input type="checkbox" name="{{ temp[0] }}" value="E">{{ temp[7] }}</label>
</li>
{% endif %}
{% if temp[8] !=None %}
<li class="option">
<label><input type="checkbox" name="{{ temp[0] }}" value="F">{{ temp[8] }}</label>
</li>
{% endif %}
</ul>
{% endif %}
{% endfor %}
<div class="sub"><input type="submit" value="提交"></div>
</form>
</div>
</body>
</html>
以下是result.html源码
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<title>答题系统</title>
<style>
* {
margin: 0;
padding: 0;
}
ul {
list-style: none;
}
.main {
width: 800px;
margin: 0 auto;
text-align: center;
overflow: hidden;
}
.title {
margin-top: 30px;
font-size: 30px;
font-weight: bold;
}
.info {
margin-top: 30px;
}
.info li {
margin-top: 10px;
}
.result {
font-size: 18px;
font-weight: bold;
}
.question {
width: 400px;
margin: 30px auto;
text-align: left;
padding: 10px;
}
.questionType {
font-size: 18px;
font-weight: bold;
}
.questionTitle {
margin-top: 10px;
font-size: 18px;
}
.option {
margin-top: 10px;
}
</style>
</head>
<body>
<div class="main">
<div class="title">答题系统</div>
<ul class="info">
<li>
<label class="result">得分:{{ result }}</label>
</li>
<li>
<label>姓名:{{ name }}</label>
</li>
<li>
<label>班级:{{ qcls }}</label>
</li>
</ul>
{% for temp in data %}
{# 判断题型 #}
{# ['索引', '题型', '题目', '选项A', '选项B', '选项C', '选项D', '选项E', '选项F', '答案', '用户作答', '正确|错误'] #}
<ul class="question">
<li class="questionType">{{ temp[1] }}</li>
<li class="questionTitle">{{ temp[2] }}</li>
{% if temp[3] !=None %}
<li class="option">
<label>{{ temp[3] }}</label>
</li>
{% endif %}
{% if temp[4] !=None %}
<li class="option">
<label>{{ temp[4] }}</label>
</li>
{% endif %}
{% if temp[5] !=None %}
<li class="option">
<label>{{ temp[5] }}</label>
</li>
{% endif %}
{% if temp[6] !=None %}
<li class="option">
<label>{{ temp[6] }}</label>
</li>
{% endif %}
{% if temp[7] !=None %}
<li class="option">
<label>{{ temp[7] }}</label>
</li>
{% endif %}
{% if temp[8] !=None %}
<li class="option">
<label>{{ temp[8] }}</label>
</li>
{% endif %}
<li class="option">
<label>答案是:{{ temp[9] }}</label>
</li>
<li class="option">
<label>您选择的是:{{ temp[10] }}</label>
</li>
<li class="option">
<label class="color">判定结果:{{ temp[11] }}</label>
</li>
</ul>
{% endfor %}
</div>
<script>
let a = document.getElementsByClassName('color')
for (let i = 0; i < a.length; i++) {
//父类
let p = a[i].parentNode.parentNode
if (a[i].innerText === '判定结果:错误') {
p.style.backgroundColor = 'LightCoral'
} else {
p.style.backgroundColor = 'SpringGreen'
}
}
</script>
</body>
</html>
将源码按照文件名保存,运行编译都可以(进行过真实环境测试,效果还可以,写的啰嗦求大佬指正,互相学习!
当然也可以直接拿来用,用的便捷用的完成要求啦,麻烦点个赞哈~~
以下是pyinstaller编译后的目录 9.9M多一点点,够小了没有通过pyinstaller把静态文件与模板打包到exe中(其实是可以的但不利于直接修改模板题库)
templates目录