疯狂刷题python版 | 使用PySide6自制刷题软件【源码+解析】
- 一、前言
- 二、思考
- 三、软件设计
- 四、软件实现
- (一)使用QWebEngineView控件通过JavaScript代码和chrome内核进行数据交互和逻辑控制
- (二)用户分别通过浏览器 GUI和PySide6 GUI进行操作
- (三)使用PySide6 GUI获取用户计算机本地资源
- 五、遇到问题及解决方案
- (一)如何把excel数据转换成json数据?
- (二)如何向chrome内核传递带大量json数据参数的JavaScript代码?
- (三)如何区分不同的题库,同样的题库只保存一份?
- (四)如何保存来自浏览器GUI的答题数据,同样的答题记录只保存一份,并与对应的题库关联?
- 六、完整源代码和可执行程序
一、前言
考试作为一种衡量个人知识和技能水平的评估方式,已融入了我们的工作和生活。求学时期,从幼儿园到高中、大学,每个教育阶段都免不了考试;工作前后,考试以笔试、面试等形式频繁出现,并作为企业选拔和晋升的主要依据;生活中,我们的驾驶证,一些行业专业资格,也是通过考试获取。
在此过程中,复习题库成了大家通过考试的重要法宝,但大多数行业、政企提供的复习题库多是word、excel等文档形式,复习起来效率不高,由此催生出了各类刷题网站、软件、手机app等工具。这些工具提供了导入题库、保存历史答题记录、错题本、标记重点、答题分析评估等功能,极大提高了考试的复习效率和通过率。
这里先放放程序运行效果,如下图:
二、思考
最近在学习PySide6开发,在学习到QT的QWebEngineView控件时,发现PySide6提供了比较完整的浏览器控件支持,而在2017年博主也使用过HTML+JavaScript+IE浏览器AX控件开发了HTML版的刷题工具,但当时没学Python所以只能在IE浏览器中运行。JavaScript|免费导入题库,考试复习工具,刷题神器,支持导入excel题库
缕一缕思路,如使用PySide6提供的QWebEngineView控件改造原来的刷题程序,基于HTML+JavaScript做界面(第一层)UI,基于PySide6 GUI做控制(第二层)UI,使用Python做算法逻辑开发,就能实现类似electron的能力,使得python开发也能利用HTML+JavaScript+CSS海量的UI界面的资源。
三、软件设计
根据以上思路,对原来写的程序设计进行了调整,得到一下程序实现方式简图,这个图是用Markdown提供的mermaid流程图做的,感觉还是挺厉害的,感觉有时间要好好研究一下Markdown。只要简单写写mermaid脚本,就可生成以下简洁明了的流程图。
四、软件实现
(一)使用QWebEngineView控件通过JavaScript代码和chrome内核进行数据交互和逻辑控制
runJavaScript()函数可以在QWebEngineView或QWebEnginePage对象上调用,以在 Web页面上执行JavaScript代码。它接受一个字符串参数,该字符串是要执行的JavaScript代码。
在PySide6中可以使用以下Python代码向浏览器发送待执行的JavaScript脚本(带参)。self.webEngineView.page().runJavaScript(js_string, 0, js_callback)中js_string是需要执行的JavaScript脚本,js_callback是回调函数,若浏览器执行代码成功则调用js_callback代码。
self.js_Button = QPushButton()
self.js_Button.setText('运行JS脚本')
self.js_Button.clicked.connect(self.run_js)
def run_js(self):
js_string, ok = QInputDialog.getMultiLineText(self, "请输入JS脚本", "可输入多行脚本", '')
def js_callback(result):
if result != '':
QMessageBox.information(self, "JS脚本返回信息", str(result))
self.statusBar().showMessage(f'成功执行JS脚本')
self.webEngineView.page().runJavaScript(js_string, 0, js_callback)
(二)用户分别通过浏览器 GUI和PySide6 GUI进行操作
在该程序中,用户使用浏览器GUI进行题目选择、答题、提交答卷等点击交互操作,这里使用HTML+JavaScript+CSS实现交互界面的设计、渲染和浏览器内部逻辑处理。如以下JavaScript脚本,用以判断用户选择的答案是否正确。
JavaScript代码:
function chooseAnswer() {
let A = '';
let B = '';
let C = '';
let D = '';
let valueA = document.getElementById("radioA").checked;
let valueB = document.getElementById("radioB").checked;
let valueC = document.getElementById("radioC").checked;
let valueD = document.getElementById("radioD").checked;
if (valueA === true) {
A = 'A';
}
if (valueB === true) {
B = 'B';
}
if (valueC === true) {
C = 'C';
}
if (valueD === true) {
D = 'D';
}
let user_choose_answer = A + B + C + D;
let daan = ti_ku.ti_mu[user_choose_number - 1].daan;
let x = user_choose_answer.toLowerCase();
let y = daan.toLowerCase();
answers[user_choose_number - 1] = user_choose_answer;
if (x === y) {
alert('回答正确');
fen_shu[user_choose_number - 1] = 1;
if_answer[user_choose_number - 1] = 1;
} else {
alert('回答错误');
fen_shu[user_choose_number - 1] = 0;
if_answer[user_choose_number - 1] = 1;
}
}
该程序使用PySide6 GUI进行导入题库、保存答题记录、查看历史答题等交互操作,由于这些操作需要使用用户计算机本地资源,所以应使用python代码进行逻辑控制。如以代码,实现保存本次用户答题记录。
Python代码:
def submit_answer_js(self):
def js_callback(result):
# js返回的本轮答题json结果,写入本地系统文件,并以系统用户名+时间.js形式保存在history文件夹中
if result != '':
js_result = str(result)
save_dir = self.environment["answer_record_dir"] + os.path.sep + self.environment[
"cur_question_bank_id"]
save_file_name = save_dir + os.path.sep + datetime.now().strftime(
'-%Y-%m-%d %H-%M-%S') + '.js'
print(self.environment["cur_question_bank_id"])
if not os.path.exists(save_dir):
os.makedirs(save_dir)
f = open(save_file_name, 'w', encoding='utf-8')
f.write(js_result)
f.close()
self.statusBar().showMessage(f'已保存本次答题记录,如想查看历史答题记录可点"查看答题历史"按钮')
if self.environment["cur_question_bank_id"] != '':
self.webEngineView.page().runJavaScript("submit()", 0, js_callback)
else:
self.statusBar().showMessage(f'请先导入题库')
(三)使用PySide6 GUI获取用户计算机本地资源
由于安全问题,一般情况下浏览器是禁止直接获取用户计算机本地资源的,因此浏览器GUI无法通过JavaScript代码来获取存储在用户计算机中的题库。在该程序中,我们使用PySide6 GUI控件QFileDialog,通过编写Python代码来获取题库文件路径,接着使用os库读取题库数据。如以下python代码,实现获取题库文件路径,并读取excel题库文件数据传递到浏览器中。
Python代码:
def import_question_bank(self):
def js_callback(result):
if result != '':
self.statusBar().showMessage(f'成功导入选中题库')
file_name, ok = QFileDialog.getOpenFileName(self, caption='Open file', dir=os.path.abspath('.'),
filter="(*.xls)")
if file_name != "":
excel_data = excel2json.read_excel_to_json(file_name)
# 根据内容生成唯一哈希码,并用该哈希码作为题库文件名,用于未来区分导入的是那一份题库
save_file_name = hashlib.sha256(excel_data.encode()).hexdigest()
self.environment["cur_question_bank_id"] = save_file_name
json_save_path = self.environment["question_bank_dir"] + os.path.sep + save_file_name + '.js'
# 判断目录是否存在,不存在则新增
if not os.path.exists(self.environment["question_bank_dir"]):
os.makedirs(self.environment["question_bank_dir"])
# 写入json格式内容,并保存为js文件
encoded_data = base64.b64encode(excel_data.encode("gbk"))
f = open(json_save_path, 'w', encoding='gbk')
f.write(encoded_data.decode("gbk"))
f.close()
self.statusBar().showMessage(f'成功加载选中题库')
self.webEngineView.page().runJavaScript(
"init('{}','{}');".format(self.environment["cur_question_bank_id"], encoded_data.decode("gbk")), 0,
js_callback)
else:
self.statusBar().showMessage(f'导入题库失败,未选择文件或文件格式错误,当前仅支持xls格式')
五、遇到问题及解决方案
(一)如何把excel数据转换成json数据?
该程序的一个主要功能是导入本地题库,题库文件格式一般为:xls、xlsx、word、pdf等。但word和pdf文件涉及图像识别,实现起来比较复杂,所以博主这里只实现了xls和xlsx格式的导入功能。另外,由于后续需用浏览器GUI进行界面渲染,所以导入题库的数据最终应转成json格式,以便在浏览器环境中开展数据处理。
excel格式数据转json格式数据博主之前也研究过,并写过一个python小脚本,所以这里直接拿来微调使用即可。具体实现可以参考下面这篇文章。Python|excel表格数据一键转json格式小工具|支持xlsx、xls格式转json|【源码+解析】
(二)如何向chrome内核传递带大量json数据参数的JavaScript代码?
上文提到,在PySide6中,可用webEngineView.page().runJavaScript()方法向浏览器传递待执行的JavaScript脚本。
查阅PySide6资料发现webEngineView的page()方法返回QWebEnginePage()对象,该对象拥有runJavaScript()方法,可以异步向浏览器发送js代码。
但该方法好像只支持传递字符串,不支持传递对象。因此,基于当前的对该方法的理解,只能把需向浏览器传递的脚本和数据都统一封装成一段完整代码字符串,再执行该方法。这过程还涉及统一字符串编码、保证JavaScript语法正确等问题,需要使用base64、encode等方法进行数据传递前后的编码处理,通过一系列编码转换,最终实现把题库中的大量excel数据传递至浏览器并处理,主要代码如下。
Python代码:
def import_question_bank(self):
def js_callback(result):
if result != '':
self.statusBar().showMessage(f'成功导入选中题库')
file_name, ok = QFileDialog.getOpenFileName(self, caption='Open file', dir=os.path.abspath('.'),
filter="(*.xls)")
if file_name != "":
excel_data = excel2json.read_excel_to_json(file_name)
# 根据内容生成唯一哈希码,并用该哈希码作为题库文件名,用于未来区分导入的是那一份题库
save_file_name = hashlib.sha256(excel_data.encode()).hexdigest()
self.environment["cur_question_bank_id"] = save_file_name
json_save_path = self.environment["question_bank_dir"] + os.path.sep + save_file_name + '.js'
# 判断目录是否存在,不存在则新增
if not os.path.exists(self.environment["question_bank_dir"]):
os.makedirs(self.environment["question_bank_dir"])
# 写入json格式内容,并保存为js文件
encoded_data = base64.b64encode(excel_data.encode("gbk"))
f = open(json_save_path, 'w', encoding='gbk')
f.write(encoded_data.decode("gbk"))
f.close()
self.statusBar().showMessage(f'成功加载选中题库')
self.webEngineView.page().runJavaScript(
"init('{}','{}');".format(self.environment["cur_question_bank_id"], encoded_data.decode("gbk")), 0,
js_callback)
else:
self.statusBar().showMessage(f'导入题库失败,未选择文件或文件格式错误,当前仅支持xls格式')
功能实现后对应的软件截图:
(三)如何区分不同的题库,同样的题库只保存一份?
hashlib.sha256是Python的一个内置模块,用于计算信息的SHA-256哈希值。这是一个计算机加密哈希函数的实例,它可以用于验证数据的完整性,或者生成数据的唯一哈希值。
该程序一开始只考虑到用户只导入一个题库进行复习,但如果用户导入第二份题库就会覆盖第一次导入的题库。为了解决该问题,使得程序支持多题库导入,首先要解决如何确保导入的题库具有唯一的标识,无论用户后续导入多少份、多少次题库,程序都可自动对题库进行去重,只保留不重复题库。博主这里使用python的hashlib库sha256()方法,使用题库内容生成唯一哈希码,并以此哈希码作为题库.js文件名,用以保证唯一性。
Python代码:
if file_name != "":
excel_data = excel2json.read_excel_to_json(file_name)
# 根据内容生成唯一哈希码,并用该哈希码作为题库文件名,用于未来区分导入的是那一份题库
save_file_name = hashlib.sha256(excel_data.encode()).hexdigest()
self.environment["cur_question_bank_id"] = save_file_name
json_save_path = self.environment["question_bank_dir"] + os.path.sep + save_file_name + '.js'
# 判断目录是否存在,不存在则新增
if not os.path.exists(self.environment["question_bank_dir"]):
os.makedirs(self.environment["question_bank_dir"])
# 写入json格式内容,并保存为js文件
encoded_data = base64.b64encode(excel_data.encode("gbk"))
f = open(json_save_path, 'w', encoding='gbk')
f.write(encoded_data.decode("gbk"))
f.close()
self.statusBar().showMessage(f'成功加载选中题库')
self.webEngineView.page().runJavaScript(
"init('{}','{}');".format(self.environment["cur_question_bank_id"], encoded_data.decode("gbk")), 0,
js_callback)
else:
self.statusBar().showMessage(f'导入题库失败,未选择文件或文件格式错误,当前仅支持xls格式')
功能实现后对应的软件截图:
(四)如何保存来自浏览器GUI的答题数据,同样的答题记录只保存一份,并与对应的题库关联?
strftime()是“string format time”的缩写,主要用于格式化日期时间对象为自定义的字符串表示形式。
支持多题库导入,衍生除了另外一个新问题,当用户读取答题记录与当前所选题库不对应时,界面会出现显示错乱的BUG。因此,在每次保存答题记录、查看历史答题记录时应进行校验,保证答题记录和题库一一对应。这里有几个关键点:①用户的答题结果;②用户当前选择的题库;③当前的系统时间。需做以下几个操作:①把用户的答题结果转换成json格式字符串,用于保存为.js格式文件;②获取当前用户选择题库的哈希码,用于建立对应的答题记录一级目录,并以该哈希码命名目录;③获取当前系统时间,使用strftime()将系统时间转换成年月日时分秒格式,并以该格式命名答题记录.js文件的文件名。以下是参考代码:
JavaScript代码:
function submit() {
let x = 0;
let y = 0;
for (let i = 0; i < ti_ku.number; i++) {
x = x + fen_shu[i];
y = y + if_answer[i];
}
alert('正确题数:' + x + '\n错误题数:' + (y - x) + '\n未作答:' + (ti_ku.number - y) );
let de_fen = new Object();
de_fen.answers=answers;
de_fen.fen_shu=fen_shu;
de_fen.if_answer=if_answer;
return JSON.stringify(de_fen);
de_fen = null
}
Python代码:
def submit_answer_js(self):
def js_callback(result):
# js返回的本轮答题json结果,写入本地系统文件,并以系统用户名+时间.js形式保存在history文件夹中
if result != '':
js_result = str(result)
save_dir = self.environment["answer_record_dir"] + os.path.sep + self.environment[
"cur_question_bank_id"]
save_file_name = save_dir + os.path.sep + datetime.now().strftime(
'-%Y-%m-%d %H-%M-%S') + '.js'
print(self.environment["cur_question_bank_id"])
if not os.path.exists(save_dir):
os.makedirs(save_dir)
f = open(save_file_name, 'w', encoding='utf-8')
f.write(js_result)
f.close()
self.statusBar().showMessage(f'已保存本次答题记录,如想查看历史答题记录可点"查看答题历史"按钮')
if self.environment["cur_question_bank_id"] != '':
self.webEngineView.page().runJavaScript("submit()", 0, js_callback)
else:
self.statusBar().showMessage(f'请先导入题库')
功能实现后对应的软件截图:
六、完整源代码和可执行程序
到这里,对原来的纯HTML+JavaScript+CSS刷题程序的PySide6改造基本完成了,实现了文章开头所说的几个主要功能:
①导入题库功能,支持xlsx和xls格式;
②支持多题库导入及选择;
③支持保存答题记录;
④支持查看历史答题记录;
⑤界面由HTML+CSS实现,支持选题、标记正确、错误、未答题目、统计答题情况;
⑥逻辑由Python+JavaScript代码实现。
如果未来有时间还会继续研究和优化该程序。
以下是该Python程序的可执行文件下载地址:
疯狂刷题软件python版 - 使用PySide6自制刷题软件