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页面源码
答题系统
以下是config.html源码
答题系统
答题系统
以下是results.html源码
答题系统
答题系统
姓名
班级
成绩
{% for tempList in data %}
{% for temp in tempList %}
{
{ temp }}
{% endfor %}
{% endfor %}
以下是question.html源码
答题系统
答题系统
以下是result.html源码
答题系统
答题系统
-
-
-
{% for temp in data %}
{# 判断题型 #}
{# ['索引', '题型', '题目', '选项A', '选项B', '选项C', '选项D', '选项E', '选项F', '答案', '用户作答', '正确|错误'] #}
- {
{ temp[1] }}
- {
{ temp[2] }}
{% if temp[3] !=None %}
-
{% endif %}
{% if temp[4] !=None %}
-
{% endif %}
{% if temp[5] !=None %}
-
{% endif %}
{% if temp[6] !=None %}
-
{% endif %}
{% if temp[7] !=None %}
-
{% endif %}
{% if temp[8] !=None %}
-
{% endif %}
-
-
-
{% endfor %}
以下是pyinstaller编译后的目录 9.9M多一点点,够小了没有通过pyinstaller把静态文件与模板打包到exe中(其实是可以的但不利于直接修改模板题库)
templates目录