本文所采用chatGPT模型,为openai为chatGPT Plus用户提供的GPT-4。本文使用的chatGPT版本为ChatGPT May 24 Version
本文所采用conda版本为23.3.1,python版本为3.10.6
需求概述
这个活是在某个月黑风高的晚上,莫名其妙到我手上的。简单来说,手上有一份word格式题库,需要在这份题库里面抽取题目出成卷子。时间很赶,需要在短时间内出两套结构相同,题目不一样的卷子。
题库的结构如下图:
可以看到这东西将近一百页(其实满打满算正好一百页)的体量,已经让我根本不想去翻。再粗略翻了以下,发现里面有好几个题型,每个题目还指向一个知识点。想要出一张涵盖所有题型和知识点的题目,光是卷面题型安排就已经能把我累死,更别说题目随机性的事情了。更何况干这个活又没有加班费……
但是这个活不干也不行,毕竟刚刚入职,哪个比我大的都是我老板。能不能用一些自动化工具来帮我干呢?Python是个好东西,之前为了捣鼓stable diffusion,电脑上也有miniconda环境,但是我不会写python啊。没有关系,chatGPT是会的。
磕磕碰碰的开始
通过观察,题库中同一题型的题目都放在了一起,以形如“〖判断题〗”这样的文字分隔不同题型的段落,每个题目都是以下格式:
- 试题编号:[6位数字,各不相同]
- 知识点:[某个知识点,大量重复]
- [题目本身及答案]
由于每个题型的题目写法都不一样,和最终试卷上的格式也有很大不同,因此,这次我并不追求全自动化出卷,只需要抽题,抽完了我再去题库里找出来手动处理。
抛给GPT的第一个问题是:我想在word中某个章节统计"知识点:"后各单词的出现频率。啪的一下,很快啊,它就给了我一个回答:
在 Word 文档中统计某些特定单词或短语的频率需要使用一些特定的工具,比如脚本或者宏。但是,Word 自身并没有提供这种功能。
一个可能的方案是使用 Python,一个被广泛使用的编程语言,来处理 Word 文档并统计单词的频率。这种方法的一个关键部分是使用
python-docx
库来读取 Word 文档。然后你可以使用 Python 的collections
库中的Counter
类来统计单词的频率。下面是一个基本的 Python 脚本,它可以打开一个 Word 文档并统计在"知识点:"之后的各单词出现的频率:
from docx import Document from collections import Counter def count_words_after_keyword(doc_path, keyword): doc = Document(doc_path) counter = Counter() for para in doc.paragraphs: if keyword in para.text: words_after_keyword = para.text.split(keyword)[1].split() counter.update(words_after_keyword) return counter # 替换以下路径为你的文档路径 doc_path = "your_document.docx" keyword = "知识点:" counter = count_words_after_keyword(doc_path, keyword) # 打印单词及其频率 for word, freq in counter.items(): print(f"{word}: {freq}")
你需要先安装
python-docx
库才能运行上述代码。你可以通过在命令行中运行pip install python-docx
来安装。注意,这个脚本并不完美,因为它只检查每一段落中的 "知识点:" 关键词。如果 "知识点:" 在一个段落中出现多次,或者跨越多个段落,那么这个脚本可能就不能正确地工作了。为了处理这些更复杂的情况,你可能需要写一个更复杂的脚本,或者使用更复杂的工具。
当时就把我惊呆了,它建议我用python,与我的需求不谋而合(prompt里完全没提),不仅给出了代码,这代码里面甚至还有注释,还将变量单独抽了出来以方便修改,还告诉了我要装什么库,这可用性也太强了吧!
因为题库文档里不止这一百多页的题目(这个文档六百多页),我想把这一部分单独拎到新的word文档上处理,但我又不想在word里面拉呀拉呀拉,于是我问了GPT第二个问题:我想将word文档中其中100页的内容存到另一个word上。
如果你想将 Word 文档中的某些页面复制到另一个 Word 文档,你可以手动做,也可以使用Python脚本自动化这个过程。这个Python脚本将使用
python-docx
库来处理 Word 文档。但是,你需要知道的是,
python-docx
并没有直接提供处理页码的功能,因为 Word 文档的页数取决于许多因素,如页面大小,字体大小,间距等。因此,如果你是按照固定的段落数量(例如100个段落代表100页)来处理的话,可以使用如下的Python脚本:from docx import Document # 打开源文档 source_doc = Document('source.docx') # 创建新文档 new_doc = Document() # 设置要复制的段落数量 num_paragraphs = 100 # 复制段落 for i in range(num_paragraphs): if i < len(source_doc.paragraphs): text = source_doc.paragraphs[i].text new_doc.add_paragraph(text) # 保存新文档 new_doc.save('new_doc.docx')
在这个脚本中,我们打开源文档,然后创建一个新的文档。然后,我们复制源文档的前100个段落到新的文档。最后,我们保存新的文档。
这个脚本可能不完全符合你的需求,因为它是按段落而不是按页面来复制的。如果你确实需要按页面来复制,你可能需要使用其他的工具或者手动复制。如果你可以明确每一页大概有多少段落,那么你可以修改脚本中的
num_paragraphs
变量来达到你想要的效果。
我并不能确定一段有多少段落,所以当时就放弃了用python去复制文档,真的去手动拉了。现在想想,完全可以用一级标题到一级标题的方式去确定复制的范围,让python自个儿去数就好了。
当我返回去执行第一个回答的代码的时候,python直接给我抛了个错:
ImportError: cannot import name 'Document' from partially initialized module 'docx'
我又去问了GPT,它告诉我:
这个错误可能是由于你的Python环境中存在命名冲突,或者
python-docx
模块没有正确安装。以下是一些可能的解决方法:
检查模块安装情况:确保
python-docx
模块已经正确安装。你可以在Python环境中运行以下命令来检查已安装的模块:pip list
如果你看不到
python-docx
在列表中,那么你需要安装它:pip install python-docx
- 避免命名冲突:确保你的Python脚本的文件名或者其所在的目录名没有与
docx
模块发生冲突。换句话说,你的脚本文件名不应该是docx.py
,也不应该在名为docx
的目录下。否则Python在导入模块时可能会混淆你的脚本和python-docx
模块。- 检查Python环境:确保你在运行脚本的Python环境中安装了
python-docx
模块。如果你有多个Python环境(例如,一个系统级别的Python和一个或多个Python虚拟环境),那么你需要确保你在运行脚本的那个环境中安装了python-docx
模块。以上都是可能的解决方案,如果这些都无法解决问题,你可能需要更深入地检查你的Python环境或代码。
好家伙,我的确把执行任务的py命名成docx.py
了,毕竟是处理word文档的py嘛,这么取名也很合情合理……总之,改完名之后,代码顺利运行。
从这三次对话中可以发现,GPT可以很好地理解我的需求,给出可用性和可读性都极高的python代码(它甚至加注释),还可以帮我对报错信息做初步判断。只要我不断地追加和修正需求,GPT完全能写出一些开箱即用的东西来。既然能用,那就继续干活。
来做亿点点调试
先回顾一下这段对话最开始,我给GPT提出的需求:
我想在word中某个章节统计"知识点:"后各单词的出现频率
在这一部分,我只想让告诉我题库里到底有多少题,按照章节、知识点和题型分类统计。在接下来的对话中,我不断追加了各种条件:
- 现在,我需要将统计的结果按照章节标题来划分
- 我的标题都有都以“1.4..”开始,*可能是任何数字
- 在这个的基础上,我还希望统计不同题型的题量。相同题型的题目会放在一起,所以我需要统计两个提醒关键词A和B之间“题目编号”出现的次数,以确定A题型的题目数量
- 我有判断、多选、单选、案例分析、计算和简答六个题型(注:这个条件描述是错误的,后面会提到其实不止这六种,这导致了抽题的时候出现问题)
- 我的题型都是以这种形式划分的“〖判断题〗”
- keyword中的冒号去掉,文档路径为"1.docx"
- 我希望在输出的时候,按题型统计同一章节的题目数量,另外,将question_keyword更换为"试题编号"
到这里,GPT已经给出了一份功能很完整的代码:
import re
from docx import Document
from collections import Counter, defaultdict
def count_words_and_questions(doc_path, keyword, question_keyword):
doc = Document(doc_path)
chapter_counters = {}
question_counters = defaultdict(lambda: defaultdict(int))
current_chapter = None
current_question_type = None
for para in doc.paragraphs:
if re.match(r'1\.4\.\d+\.\d+', para.text):
# 开始一个新的章节
current_chapter = para.text
chapter_counters[current_chapter] = Counter()
current_question_type = None
elif current_chapter is not None:
if keyword in para.text:
# 在当前章节中统计单词
words_after_keyword = para.text.split(keyword)[1].split()
chapter_counters[current_chapter].update(words_after_keyword)
elif question_keyword in para.text:
if current_question_type is not None:
# 在当前题型中统计题目数量
question_counters[current_chapter][current_question_type] += 1
else:
for question_type in ["判断题", "多选题", "单选题", "案例分析题", "计算题", "简答题"]:
if question_type in para.text:
current_question_type = question_type
break
return chapter_counters, question_counters
# 替换以下路径为你的文档路径
doc_path = "1.docx"
keyword = "知识点"
question_keyword = "试题编号"
chapter_counters, question_counters = count_words_and_questions(doc_path, keyword, question_keyword)
# 打印每个章节的单词及其频率以及各题型的题目数量
for chapter in chapter_counters.keys():
print(chapter)
for word, freq in chapter_counters[chapter].items():
print(f"{word}: {freq}")
# 按照题型排序输出
for question_type in sorted(question_counters[chapter].keys()):
print(f"{question_type}数量: {question_counters[chapter][question_type]}")
不过这还不是我最后想要的,别忘了我的需求是抽题,这还没抽呢。
来,怎么抽
首先,我想知道怎么抽题是合适的,这个问题当然也是抛给GPT了:
我要出一份卷子,有〖判断题〗", "〖多选题〗", "〖单选题〗", "〖案例分析题〗", "〖计算题〗", "〖简答题〗几个题型,满分一百分,我应该怎样设定分值的题量
它给出的方案是:
你的需求涉及到6种题型,满分是100分。如何设定分值,这在一定程度上取决于每种题型对于总评价的重要性。以下是一种可能的分配方式:
- 判断题:通常这种题型比较简单,所以可以设定比较低的分值,比如20%的总分,也就是20分。假设每题2分,那么就需要出10个判断题。
- 多选题:这种题型相对复杂一些,可以设定为15%的总分,也就是15分。假设每题3分,那么就需要出5个多选题。
- 单选题:这种题型的难度介于判断题和多选题之间,可以设定为15%的总分,也就是15分。假设每题3分,那么就需要出5个单选题。
- 案例分析题:这种题型的难度较大,需要考生有较强的综合运用知识的能力,可以设定为20%的总分,也就是20分。假设每题10分,那么就需要出2个案例分析题。
- 计算题:这种题型通常也比较难,可以设定为15%的总分,也就是15分。假设每题7.5分,那么就需要出2个计算题。
- 简答题:这种题型需要考生能清晰准确地表述自己的观点,可以设定为15%的总分,也就是15分。假设每题7.5分,那么就需要出2个简答题。
以上是一种可能的分配方式,你可以根据实际情况进行调整。
好怪异的分数这是和出题方法……看来要调教一下了。这一调教,问题就出来了。
我给它指定了判断、单选、多选题的分值:
如果判断题、单选题都是1分,多选题2分,又应该怎么分配
它给出了一个依然很怪异的回答:
如果你希望判断题和单选题都是1分,多选题2分,那么我们需要重新考虑分配方式。假设其余的题型分值保持原样,即案例分析题每题10分,计算题每题7.5分,简答题每题7.5分。那么以下是一种可能的分配方式:
- 判断题:15题,总分15分。
- 单选题:15题,总分15分。
- 多选题:10题,总分20分。
- 案例分析题:2题,总分20分。
- 计算题:2题,总分15分。
- 简答题:2题,总分15分。
这样每种题型的数量和分值相加,总分数就是100分。你可以根据实际需要调整题目的数量和分值。
又做了一个修正:
7.5分太奇怪了,我觉得5分比较合理
它终于给出了看上去比较正常的回答:
明白你的需求,我们可以将计算题和简答题的分值都调整为5分。然后,相应地调整判断题、单选题和多选题的题量。下面是新的分配方式:
- 判断题:20题,总分20分。
- 单选题:20题,总分20分。
- 多选题:15题,总分30分。
- 案例分析题:2题,总分20分。
- 计算题:2题,总分10分。
- 简答题:2题,总分10分。
这样,所有的题型数量和分值相加,总分数仍然是100分。
到这里,读者们可能已经发现了,GPT给出的总分数根本不是它说的100分! 不过当时我还没有发现这个问题,因为我正在处理另一个问题:我看漏题型了。如上所述,这份题库非常的厚,而我之前只是草草翻了一遍,并没有翻到底,实际上除了我上文提到的六种题型之外,还有识绘图题和论述题,一共是八种题型。解决这个事情倒是简单,让GPT加上就行了:
- 我的需求有误,事实上还有识绘图题和论述题
- 我希望识绘图题只有一道
- 我希望识绘图和论述题的分值都是一道5份(没错,就是打错字了,但是GPT还是懂了)
了解你的需求。如果识绘图题和论述题每题都是5分,那么我们可以相应地调整其他题型的分值和题量。以下是一个可能的分配方式:
- 判断题:18题,总分18分。
- 单选题:18题,总分18分。
- 多选题:10题,总分20分。
- 案例分析题:2题,总分10分。
- 计算题:2题,总分10分。
- 简答题:2题,总分10分。
- 识绘图题:1题,总分5分。
- 论述题:1题,总分5分。
这样,所有的题型数量和分值相加,总分数仍然是100分。这只是一个建议,具体的题目数量和分值可以根据你的实际需求进行调整。
这个时候,我也发现事情不对劲了,这个编排下总分压根不是100分。我提醒了一下GPT,结果这货给我整出了加分题。我……算了算了,犯傻是常有的事情,我告诉它没有加分题,它终于给出了一份合理的编排方案,总分也终于是100分了:
非常抱歉之前理解有误,感谢你的耐心。现在,我们重新计算一下题目的分配:
如果判断题和单选题每题都是1分,多选题每题是2分,而案例分析题、计算题、简答题、识绘图题和论述题每题都是5分,那么分配方式可以是:
- 判断题:20题,总分20分。
- 单选题:15题,总分15分。
- 多选题:10题,总分20分。
- 案例分析题:2题,总分10分。
- 计算题:2题,总分10分。
- 简答题:2题,总分10分。
- 识绘图题:1题,总分5分。
- 论述题:2题,总分10分。
这样,所有题型的数量和分值相加,总分数就是100分。这只是一个建议,具体的题目数量和分值可以根据你的实际需求进行调整。
从这一轮的对话可以看出,即便是GPT-4模型,在这种数学计算上犯傻的概率依然不低,有时候这种错误没有办法用prompt进行修正(可能是受到了上下文的影响),在使用其输出进行工作前,需要对涉及数字的部分进行核对,以免出现错误。
开始抽题
有了上面的出题方案,我们可以开始让GPT帮忙写抽题的代码了。在没有发现自己漏了两个题型之前,我将6种题型的方案发给了它:
你做得很好,现在,我需要出一份卷子,题目构成如下:
判断题:20题,总分20分。
单选题:20题,总分20分。
多选题:15题,总分30分。
案例分析题:2题,总分20分。
计算题:2题,总分10分。
简答题:2题,总分10分。
在刚刚给你的文档中,每个题目都有独一无二的试题编号,我需要从里面随机抽出对应数量的题目,且抽出的题目内容需要和各章节题目数量的比例保持相对一致
GPT的回答如下:
这是一个有趣的挑战,我们可以在前面的代码的基础上进行修改。首先,我们需要为每个章节和题型创建一个列表来保存每个题目的编号。然后,我们可以根据你给出的每个题型的题目数量,按章节的题目比例,随机选择题目。这是一个可能的解决方案:
import re from docx import Document from collections import defaultdict from random import choices def gather_questions(doc_path, keyword, question_keyword): doc = Document(doc_path) chapter_questions = defaultdict(lambda: defaultdict(list)) current_chapter = None current_question_type = None for para in doc.paragraphs: if re.match(r'1\.4\.\d+\.\d+', para.text): # 开始一个新的章节 current_chapter = para.text current_question_type = None elif current_chapter is not None: if question_keyword in para.text: if current_question_type is not None: # 在当前题型中添加题目编号 chapter_questions[current_chapter][current_question_type].append(para.text) else: for question_type in ["判断题", "多选题", "单选题", "案例分析题", "计算题", "简答题"]: if question_type in para.text: current_question_type = question_type break return chapter_questions def select_questions(chapter_questions, target_counts): selected_questions = defaultdict(list) for question_type, target_count in target_counts.items(): chapter_weights = [len(chapter_questions[chapter][question_type]) for chapter in chapter_questions.keys()] for _ in range(target_count): selected_chapter = choices(list(chapter_questions.keys()), weights=chapter_weights, k=1)[0] question = chapter_questions[selected_chapter][question_type].pop() selected_questions[question_type].append(question) chapter_weights = [len(chapter_questions[chapter][question_type]) for chapter in chapter_questions.keys()] return selected_questions # 替换以下路径为你的文档路径 doc_path = "1.docx" keyword = "知识点" question_keyword = "试题编号" chapter_questions = gather_questions(doc_path, keyword, question_keyword) target_counts = {"判断题": 20, "单选题": 20, "多选题": 15, "案例分析题": 2, "计算题": 2, "简答题": 2} selected_questions = select_questions(chapter_questions, target_counts) # 打印选出的题目编号 for question_type, questions in selected_questions.items(): print(question_type) for question in questions: print(question)
这个脚本首先创建了一个字典,键是章节和题型,值是该章节和题型的题目编号列表。然后,它根据你给出的目标题目数量,按章节题目比例随机选择题目。
需要注意的是,这个脚本假设每个题目编号都在一个单独的段落中,而且每个题目编号都是唯一的。如果你的文档的结构不同,你可能需要对脚本进行一些调整。
一如既往的开箱即用,注释齐全,解释清楚,它甚至还说“这是个有趣的挑战”,太好用了真的是……代替程序员有点难,但是提高工作的自动化水平已经是绰绰有余了。
因为我之前看漏了两个题型,导致这两个题型的题目被统计入了其他题目里,上面给出的代码抽出的题目有一些是和题型对不上的。我一开始以为是代码的问题,GPT给出的解决方案是在统计每一道题的时候往前遍历它所属的题型(之前的方案是碰到题型关键字开始统计,碰到下一个题型关键字的时候停止统计,开始统计新的题型),但这样一来O(n)的算法直接变成了O(n^2),跑起来大概是一晚上都跑不完。所幸后来发现了是题型的问题,让GPT改了回来。
最终GPT生成的代码如下:
import re
from docx import Document
from collections import defaultdict
from random import choices
def gather_questions(doc_path, keyword, question_keyword):
doc = Document(doc_path)
chapter_questions = defaultdict(lambda: defaultdict(list))
current_chapter = None
current_question_type = None
for para in doc.paragraphs:
if re.match(r'1\.4\.\d+\.\d+', para.text):
# 开始一个新的章节
current_chapter = para.text
elif current_chapter is not None:
for question_type in ["判断题", "多选题", "单选题", "案例分析题", "计算题", "简答题", "识绘图题", "论述题"]:
if question_type in para.text:
current_question_type = question_type
if question_keyword in para.text and current_question_type is not None:
# 在当前题型中添加题目编号
chapter_questions[current_chapter][current_question_type].append(para.text)
return chapter_questions
def select_questions(chapter_questions, target_counts):
selected_questions = defaultdict(list)
for question_type, target_count in target_counts.items():
chapter_weights = [len(chapter_questions[chapter][question_type]) for chapter in chapter_questions.keys()]
for _ in range(target_count):
selected_chapter = choices(list(chapter_questions.keys()), weights=chapter_weights, k=1)[0]
question = chapter_questions[selected_chapter][question_type].pop()
selected_questions[question_type].append(question)
chapter_weights = [len(chapter_questions[chapter][question_type]) for chapter in chapter_questions.keys()]
return selected_questions
# 替换以下路径为你的文档路径
doc_path = "1.docx"
keyword = "知识点"
question_keyword = "试题编号"
chapter_questions = gather_questions(doc_path, keyword, question_keyword)
target_counts = {"判断题": 20, "单选题": 15, "多选题": 10, "案例分析题": 2, "计算题": 2, "简答题": 2, "识绘图题": 1, "论述题": 2}
selected_questions = select_questions(chapter_questions, target_counts)
# 打印选出的题目编号
for question_type, questions in selected_questions.items():
print(question_type)
for question in questions:
print(question)
这份代码抛弃了兼顾知识点的平衡(虽然keyword“知识点”依然在,但没有用上),毕竟题目足够多,根据章节加权抽取已经能够满足基本的随机性。这份代码创建了一份字典,以"1.4.x.x"特征识别章节,以给定的题型关键字识别题型,以"试题编号"识别题目,将编号放入章节和题型对应的位置中。将题库遍历一遍后,调用select_questions
函数,对题目进行加权随机抽取,并设定了每个题型的抽取数目。用这份代码,我从题库中顺利抽取了两套不一样的题目(计算和识绘图题因为题目本身就少,抽来抽去就是二选一),完成了出两套试卷的任务。
这其实不算是我第一次把GPT用在工作当中,之前一些学习心得之类的东西,我也使用了chatGPT和New Bing帮助我写。在写心得这件事情上,GPT产出的东西离最终可以交出去的成品还很远,但它能够提供一个清晰的骨架,告诉我应该先写什么,后写什么。顺着它的回答,我再不断地往prompt上加东西,以至于后期的prompt我需要先在记事本上编辑好再发出去。同时,由于GPT压根就没法处理好”字数“这个事情,给我的成果往往只有我要求的一半甚至更少。我有考虑过是中英文字符差别的影响,将发给它的字数要求翻了一番,但收效甚微,只好给它一个大到离谱的字数要求,让它尽可能多地输出,我再去一点一点删减。总而言之,GPT写的心得绝对不是什么开箱即用的东西。
但这一次的使用体验和写心得是完全不同。在这种”利用简单的python提高工作效率“的使用情景里,GPT的表现堪称完美,它可以准确地识别需求,给出方案,还对代码进行了必要的注释。虽然我在对话中不断地对prompt修修补补,但GPT给出的代码实际上和第一次给出来的大差不差(毕竟有些对话我只是让它改掉关键字而已),期间唯一一次代码运行报错,也是因为我自己的原因,而不是代码有问题。可以说,chatGPT可以成为真正意义上的”个人助手“,而不仅仅是一个有趣的玩具。