第一次个人编程作业
作业要求
这个作业属于哪个课程 | https://edu.cnblogs.com/campus/gdgy/CSGrade22-34/ |
---|---|
这个作业要求在哪 | https://edu.cnblogs.com/campus/gdgy/CSGrade22-34/homework/13229 |
github链接 | https://github.com/SiMON-Shen11/3122004877 |
这个作业的目标 | 实现论文查重算法 |
需求分析
题目:论文查重
描述如下:
设计一个论文查重算法,给出一个原文文件和一个在这份原文上经过了增删改的抄袭版论文的文件,在答案文件中输出其重复率。
-
原文示例:今天是星期天,天气晴,今天晚上我要去看电影。
-
抄袭版示例:今天是周天,天气晴朗,我晚上要去看电影。
要求输入输出采用文件输入输出,规范如下:> >抄袭版示例:今天是周天,天气晴朗,我晚上要去看电影。 -
从命令行参数给出:论文原文的文件的绝对路径。
-
从命令行参数给出:抄袭版论文的文件的绝对路径。
-
从命令行参数给出:输出的答案文件的绝对路径。
我们提供一份样例,课堂上下发,上传到班级群,使用方法是:orig.txt是原文,其他orig_add.txt等均为抄袭版论文。
注意:答案文件中输出的答案为浮点型,精确到小数点后两位从命令行参数给出:输出的答案文件的绝对路径。
接口设计及代码实现
设计思路
流程图:
1.文本预处理
#读取并处理文件内容
def load_and_process_files(filepath1, filepath2):
try:
# 读取文件内容
with open(filepath1, 'r', encoding='utf-8') as file1, open(filepath2, 'r', encoding='utf-8') as file2:
content1 = file1.read()
content2 = file2.read()
except FileNotFoundError:
print("文件路径错误,请检查输入的文件路径!")
return None, None
# 去除文件中的标点符号
punctuation = "\n\r 、,。;:?‘’“”''""!《》,.;:?!<>"
for p in punctuation:
content1 = content1.replace(p, '')
content2 = content2.replace(p, '')
return content1, content2
读取文档内容并文本预处理:读取文件内容:尝试打开并读取指定路径的两个文件。如果文件路径不正确,会捕获 FileNotFoundError 异常,并提示错误信息。
- 处理内容:将文件内容中的标点符号(包括中文和英文标点)替换为空字符,从而去除这些标点符号。
- 返回结果:返回处理后的两个文件内容。如果出现错误,则返回 None。
2.分词:
# 分词函数
def tokenize_content(content1, content2):
# 使用 jieba 分词,将内容进行分词处理
tokens1 = ' '.join(tokenizer.lcut(content1))
tokens2 = ' '.join(tokenizer.lcut(content2))
return tokens1, tokens2
对文档内容分词:使用 jieba 库中的 lcut 函数对输入的文本 content1 和 content2 进行分词。
- jieba 是一个中文文本分词库,可以将中文句子切分成一个个词语。
- tokenizer.lcut(content) 是 jieba 库的一个函数,它将输入的文本切分成一个词语列表。
- 将分词后的结果用空格 ' ' 连接成一个字符串。使用 ' '.join() 将词语列表合并成一个由空格分隔的字符串。
- 返回两个分词后的字符串 tokens1 和 tokens2。
3.向量化:
# 文本向量化
def convert_to_vector(text1, text2):
# 利用 TF-IDF 向量化分词后的文本内容
vectorizer = TfidfVectorizer()
# 输入两段文本进行向量化
transformed_vectors = vectorizer.fit_transform([text1, text2]).toarray()
return transformed_vectors[0], transformed_vectors[1]
分词后的文本转换为向量:将两个文本通过 TF-IDF 方法转换为特征向量,以便于进行后续的文本比较、分类或其他文本分析任务。
- 初始化 TF-IDF 向量化器:使用 TfidfVectorizer 创建一个 TF-IDF 向量化器对象。TF-IDF(Term Frequency-Inverse Document Frequency)是一种常用的文本向量化方法,它可以将文本转换为特征向量,衡量词语在文本中的重要性。
- 向量化文本内容:使用 vectorizer.fit_transform([text1, text2]) 将两个输入文本 text1 和 text2 转换为 TF-IDF 向量。fit_transform 方法会首先根据输入的文本训练向量化器,然后将文本转换为 TF-IDF 特征矩阵。
- toarray() 方法将稀疏矩阵(默认的输出格式)转换为普通的二维数组(密集矩阵),每一行代表一个文本的 TF-IDF 向量。
- 返回向量化结果:transformed_vectors[0] 和 transformed_vectors[1] 分别表示两个输入文本的 TF-IDF 向量。函数将这两个向量作为元组返回。
4.余弦相似度计算
# 计算向量的余弦相似度
def compute_cosine_similarity(vec1, vec2):
# 计算向量的点积和模的乘积
dot_product = sum(a * b for a, b in zip(vec1, vec2))
magnitude1 = math.sqrt(sum(a ** 2 for a in vec1))
magnitude2 = math.sqrt(sum(b ** 2 for b in vec2))
# 计算并返回余弦相似度
if magnitude1 == 0 or magnitude2 == 0:
return 0.0
return dot_product / (magnitude1 * magnitude2)
计算两个向量之间的余弦相似度:
- 计算点积:通过 sum(a * b for a, b in zip(vec1, vec2)) 计算两个向量 vec1 和 vec2 的点积。zip 函数将两个向量的对应元素配对,然后计算这些配对元素的乘积之和。
- 计算模(向量长度):magnitude1 和 magnitude2 分别计算 vec1 和 vec2 的模。使用 math.sqrt(sum(a ** 2 for a in vec1)) 和 math.sqrt(sum(b ** 2 for b in vec2))
- 计算每个向量元素平方和的平方根,即向量的长度。
- 计算余弦相似度:如果任一向量的模为零(表示向量为零向量),余弦相似度设为 0.0,避免除以零的错误。
否则,计算余弦相似度:dot_product / (magnitude1 * magnitude2),这是点积除以两个向量模的乘积。
5.输出相似度结果到指定文件:
# 将结果保存到文件中
def save_similarity_result(output_path, similarity_score):
try:
if isinstance(similarity_score, float):
with open(output_path, 'w') as file:
# 保留两位小数写入文件
file.write(f'{similarity_score:.2f}')
return round(similarity_score, 2)
else:
raise ValueError("结果应为浮点类型!")
except FileNotFoundError:
print("结果保存路径不存在,请检查路径!")
return None
- 检查 similarity_score 的类型:确保 similarity_score 是一个浮点数。如果不是,则抛出 ValueError 异常,并提供错误信息 "结果应为浮点类型!"。
- 写入文件:如果 similarity_score 是浮点数,使用 with open(output_path, 'w') as file 打开指定路径 output_path 的文件(以写入模式 'w')。文件打开后,会将 similarity_score 格式化为保留两位小数的字符串,并写入文件中。使用 f'{similarity_score:.2f}' 格式化浮点数为两位小数的字符串。
- 返回值:写入文件成功后,使用 round(similarity_score, 2) 将 similarity_score 四舍五入到两位小数,并返回这个值。
- 处理文件错误:如果文件路径不存在或无法找到(抛出 FileNotFoundError),打印错误信息 "结果保存路径不存在,请检查路径!",并返回 None。
接口部分的性能改进
用于余弦相似度计算的两个函数和用于分词处理的函数在查重过程中占据了程序大部分的执行时间,是整个程序的核心。且分词过程的性能受文章长度和内容结构的影响,很难进一步优化。
可以考虑以下改进措施:
-
增强错误处理:在 try 块中增加对可能出现的其他异常(如权限错误)的处理,使用 except Exception as e 捕获并打印更多信息。
-
类型检查改进:可以使用 isinstance 函数更灵活地检查是否为浮点数,并提供更详细的错误信息。
-
文件路径验证:在打开文件前检查文件夹路径是否存在,并确保路径有效。
-
返回值一致性:返回值可以统一为 None 或 similarity_score,避免不同场景下返回不同类型的值
模块部分单元测试展示
图中展示10个测试通过的总耗时802ms,文章查重测试为698ms,分词消耗101ms。大部分时间用在文章查重和分词消耗,对不正确的用例能够检测得出。
可能出现的异常情况及相关处理
文件处理模块中的异常处理
异常情况:文件路径不正确或不存在
处理:当文件路径不正确或文件不存在时,open() 函数会抛出 FileNotFoundError,返回 None, None,并提示用户检查文件路径。
分词模块中的异常处理
异常情况:传入值为空
处理:在 tokenize_content 函数中,首先检查传入的 content1 和 content2 是否为 None。如果为空,说明文件读取出现了问题或文本内容为空,此时直接返回空字符串,不进行分词操作。
向量化模块中的异常处理
异常情况:空文本或无效文本输入、向量化器的初始化或执行错误
处理建议:在 TfidfVectorizer 进行向量化时,如果输入数据不符合预期(如文本太短或为空),可能会抛出 ValueError。这里通过捕获异常,避免程序崩溃,并返回 None, None。
相似度计算模块中的异常处理
异常情况:向量长度为零、向量数据格式问题
处理:当两个向量中有一个是零向量时(向量长度为零),计算余弦相似度会遇到除以零的问题。为了避免这个错误,代码检查向量的模是否为零。如果是零向量,直接返回相似度为 0,避免出现 ZeroDivisionError。
结果保存模块中的异常处理
异常情况:无效文件路径、文件写入权限问题、传入非浮点数或非整数的结果
处理:如果保存路径不存在或不正确,open() 函数会抛出 FileNotFoundError。捕获这个异常,并输出错误提示,确保程序不崩溃。 捕获所有文件写入相关的 I/O 错误(如磁盘满、权限不足等),避免由于 I/O 问题导致的崩溃。
PSP表格
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | 20 | 25 |
·Estimate | 估计任务完成时间 | 20 | 25 |
Development | 开发 | 190 | 225 |
· Analysis | 需求分析 | 75 | 75 |
· Design Spec | 设计文档 | 15 | 15 |
· Design Review | 设计复审 | 10 | 10 |
· Coding Standard | 代码规范 | 10 | 20 |
· Design | 具体设计 | 40 | 40 |
· Coding | 具体编码 | 60 | 70 |
· Code Review | 代码复审 | 10 | 10 |
· Test | 测试 | 35 | 40 |
Reporting | 报告 | 60 | 75 |
· Test Repor | 测试报告 | 70 | 70 |
· Size Measurement | 计算工作量 | 20 | 15 |
· Postmortem & Process Improvement Plan | 事后总结,提出改进 | 20 | 20 |
· 合计 | 655 | 735 |