本文所采用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模块没有正确安装。以下是一些可能的解决方法:

  1. 检查模块安装情况:确保python-docx模块已经正确安装。你可以在Python环境中运行以下命令来检查已安装的模块:

    pip list

    如果你看不到python-docx在列表中,那么你需要安装它:

    pip install python-docx
  2. 避免命名冲突:确保你的Python脚本的文件名或者其所在的目录名没有与docx模块发生冲突。换句话说,你的脚本文件名不应该是docx.py,也不应该在名为docx的目录下。否则Python在导入模块时可能会混淆你的脚本和python-docx模块。
  3. 检查Python环境:确保你在运行脚本的Python环境中安装了python-docx模块。如果你有多个Python环境(例如,一个系统级别的Python和一个或多个Python虚拟环境),那么你需要确保你在运行脚本的那个环境中安装了python-docx模块。

以上都是可能的解决方案,如果这些都无法解决问题,你可能需要更深入地检查你的Python环境或代码。

好家伙,我的确把执行任务的py命名成docx.py了,毕竟是处理word文档的py嘛,这么取名也很合情合理……总之,改完名之后,代码顺利运行。

从这三次对话中可以发现,GPT可以很好地理解我的需求,给出可用性和可读性都极高的python代码(它甚至加注释),还可以帮我对报错信息做初步判断。只要我不断地追加和修正需求,GPT完全能写出一些开箱即用的东西来。既然能用,那就继续干活。

来做亿点点调试

先回顾一下这段对话最开始,我给GPT提出的需求:

我想在word中某个章节统计"知识点:"后各单词的出现频率

在这一部分,我只想让告诉我题库里到底有多少题,按照章节、知识点和题型分类统计。在接下来的对话中,我不断追加了各种条件:

  1. 现在,我需要将统计的结果按照章节标题来划分
  2. 我的标题都有都以“1.4..”开始,*可能是任何数字
  3. 在这个的基础上,我还希望统计不同题型的题量。相同题型的题目会放在一起,所以我需要统计两个提醒关键词A和B之间“题目编号”出现的次数,以确定A题型的题目数量
  4. 我有判断、多选、单选、案例分析、计算和简答六个题型(注:这个条件描述是错误的,后面会提到其实不止这六种,这导致了抽题的时候出现问题
  5. 我的题型都是以这种形式划分的“〖判断题〗”
  6. keyword中的冒号去掉,文档路径为"1.docx"
  7. 我希望在输出的时候,按题型统计同一章节的题目数量,另外,将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分。如何设定分值,这在一定程度上取决于每种题型对于总评价的重要性。以下是一种可能的分配方式:

  1. 判断题:通常这种题型比较简单,所以可以设定比较低的分值,比如20%的总分,也就是20分。假设每题2分,那么就需要出10个判断题。
  2. 多选题:这种题型相对复杂一些,可以设定为15%的总分,也就是15分。假设每题3分,那么就需要出5个多选题。
  3. 单选题:这种题型的难度介于判断题和多选题之间,可以设定为15%的总分,也就是15分。假设每题3分,那么就需要出5个单选题。
  4. 案例分析题:这种题型的难度较大,需要考生有较强的综合运用知识的能力,可以设定为20%的总分,也就是20分。假设每题10分,那么就需要出2个案例分析题。
  5. 计算题:这种题型通常也比较难,可以设定为15%的总分,也就是15分。假设每题7.5分,那么就需要出2个计算题。
  6. 简答题:这种题型需要考生能清晰准确地表述自己的观点,可以设定为15%的总分,也就是15分。假设每题7.5分,那么就需要出2个简答题。

以上是一种可能的分配方式,你可以根据实际情况进行调整。

好怪异的分数这是和出题方法……看来要调教一下了。这一调教,问题就出来了。

我给它指定了判断、单选、多选题的分值:

如果判断题、单选题都是1分,多选题2分,又应该怎么分配

它给出了一个依然很怪异的回答:

如果你希望判断题和单选题都是1分,多选题2分,那么我们需要重新考虑分配方式。假设其余的题型分值保持原样,即案例分析题每题10分,计算题每题7.5分,简答题每题7.5分。那么以下是一种可能的分配方式:

  1. 判断题:15题,总分15分。
  2. 单选题:15题,总分15分。
  3. 多选题:10题,总分20分。
  4. 案例分析题:2题,总分20分。
  5. 计算题:2题,总分15分。
  6. 简答题:2题,总分15分。

这样每种题型的数量和分值相加,总分数就是100分。你可以根据实际需要调整题目的数量和分值。

又做了一个修正:

7.5分太奇怪了,我觉得5分比较合理

它终于给出了看上去比较正常的回答:

明白你的需求,我们可以将计算题和简答题的分值都调整为5分。然后,相应地调整判断题、单选题和多选题的题量。下面是新的分配方式:

  1. 判断题:20题,总分20分。
  2. 单选题:20题,总分20分。
  3. 多选题:15题,总分30分。
  4. 案例分析题:2题,总分20分。
  5. 计算题:2题,总分10分。
  6. 简答题:2题,总分10分。

这样,所有的题型数量和分值相加,总分数仍然是100分。

到这里,读者们可能已经发现了,GPT给出的总分数根本不是它说的100分! 不过当时我还没有发现这个问题,因为我正在处理另一个问题:我看漏题型了。如上所述,这份题库非常的厚,而我之前只是草草翻了一遍,并没有翻到底,实际上除了我上文提到的六种题型之外,还有识绘图题和论述题,一共是八种题型。解决这个事情倒是简单,让GPT加上就行了:

  1. 我的需求有误,事实上还有识绘图题和论述题
  2. 我希望识绘图题只有一道
  3. 我希望识绘图和论述题的分值都是一道5份(没错,就是打错字了,但是GPT还是懂了)

了解你的需求。如果识绘图题和论述题每题都是5分,那么我们可以相应地调整其他题型的分值和题量。以下是一个可能的分配方式:

  1. 判断题:18题,总分18分。
  2. 单选题:18题,总分18分。
  3. 多选题:10题,总分20分。
  4. 案例分析题:2题,总分10分。
  5. 计算题:2题,总分10分。
  6. 简答题:2题,总分10分。
  7. 识绘图题:1题,总分5分。
  8. 论述题:1题,总分5分。

这样,所有的题型数量和分值相加,总分数仍然是100分。这只是一个建议,具体的题目数量和分值可以根据你的实际需求进行调整。

这个时候,我也发现事情不对劲了,这个编排下总分压根不是100分。我提醒了一下GPT,结果这货给我整出了加分题。我……算了算了,犯傻是常有的事情,我告诉它没有加分题,它终于给出了一份合理的编排方案,总分也终于是100分了:

非常抱歉之前理解有误,感谢你的耐心。现在,我们重新计算一下题目的分配:

如果判断题和单选题每题都是1分,多选题每题是2分,而案例分析题、计算题、简答题、识绘图题和论述题每题都是5分,那么分配方式可以是:

  1. 判断题:20题,总分20分。
  2. 单选题:15题,总分15分。
  3. 多选题:10题,总分20分。
  4. 案例分析题:2题,总分10分。
  5. 计算题:2题,总分10分。
  6. 简答题:2题,总分10分。
  7. 识绘图题:1题,总分5分。
  8. 论述题: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可以成为真正意义上的”个人助手“,而不仅仅是一个有趣的玩具。

最后修改:2024 年 05 月 19 日
如果觉得我的文章对你有用,请随意赞赏