软工作业:用python实现论文查重
这个作业属于哪个课程 | https://edu.cnblogs.com/campus/gdgy/CSGrade22-34 |
---|---|
这个作业要求在哪里 | https://edu.cnblogs.com/campus/gdgy/CSGrade22-34/homework/13229 |
这个作业的目标 | python实现论文查重并进行单元测试 |
一、PSP表格
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | 20 | 25 |
Estimate | 估计这个任务需要多少时间 | 30 | 35 |
Development | 开发 | 1000 | 1400 |
Analysis | 需求分析 (包括学习新技术) | 120 | 150 |
Design Spec | 生成设计文档 | 30 | 30 |
Design Review | 设计复审 | 30 | 35 |
Coding Standard | 代码规范 (为目前的开发制定合适的规范) | 15 | 20 |
Design | 具体设计 | 60 | 60 |
Coding | 具体编码 | 70 | 120 |
Code Review | 代码复审 | 20 | 30 |
Test | 测试(自我测试,修改代码,提交修改) | 60 | 60 |
Reporting | 报告 | 20 | 60 |
Test Repor | 测试报告 | 20 | 20 |
Size Measurement | 计算工作量 | 10 | 15 |
Postmortem & Process Improvement Plan | 事后总结, 并提出过程改进计划 | 10 | 10 |
合计 | 1500 | 1800 |
二、运行环境
IDE:PyCharm 2024.1.4
三、主函数设计
流程图
一、程序分析
程序实现了以下主要功能:
- 文件读取功能 (read_file):
- 从指定的文件路径读取文件内容,如果文件不存在,则抛出 FileNotFoundError 并退出程序。
- 使用了 sys.exit(1) 来确保程序在找不到文件时以状态码 1 退出,表示异常状态。
- 该函数的设计非常简单,读取文件内容时使用了上下文管理(with open),确保即使在异常情况下文件也会被正确关闭。
- 错误处理部分使用了 try...except 捕获 FileNotFoundError,这很好地避免了文件不存在时的崩溃。
- 文本预处理功能 (preprocess_text):
- 对文本进行预处理,步骤包括:
- 使用正则表达式去除所有非单词字符和空白字符之外的内容(即去掉标点符号等)。
- 使用 jieba.lcut 进行中文分词,之后将分词结果用空格连接为一个字符串。jieba 是一种常用的中文分词库,适合处理中文文本。
- 使用正则表达式去除文本中的标点符号,这是非常有效的清理方式。
- jieba.lcut 可以将中文文本进行分词,分词后再通过 ' '.join 拼接成适合向量化处理的格式。这一部分对中文文本处理非常重要,因为中文是没有空格分隔的,直接向量化处理中文可能会产生不正确的结果。*
- 相似度计算功能 (calculate_similarity):
- 使用 TfidfVectorizer 对输入的两个文本进行词频-逆文档频率(TF-IDF)向量化处理。
- 生成的TF-IDF矩阵用于计算两个文本之间的 余弦相似度,这是一种常用的文本相似度度量方法。计算的余弦相似度结果在0到1之间,0表示完全不同,1表示完全相同。
- 相似度写入功能 (write_output):
- 将计算出的相似度结果写入指定的文件。结果会被格式化为保留两位小数的形式,并在文件中显示。
- 该函数直接将相似度结果格式化并写入指定的输出文件,功能简单明了。
- 使用 with open 确保文件操作安全,不会因异常情况导致文件未关闭或数据未写入。
二、设计优点
该程序通过合理的模块化设计、可靠的错误处理、适当的中文处理工具、稳健的文本相似度计算方法,以及安全的文件操作,展现了以下优点:
- 简洁易用:每个功能都十分明确,接口简单,用户可以轻松使用。
- 高效准确:利用 TF-IDF 和余弦相似度计算文本相似度,具有较高的准确性。
- 可维护性高:由于功能模块独立、职责单一,代码非常易于修改和维护。
三、主要函数设计代码
1.文件读取功能 (read_file):
def read_file(file_path):
"""
读取指定路径的文件内容,并返回去掉空白符的字符串。
:param file_path: 文件路径
:return: 去掉首尾空白符后的字符串
"""
try:
with open(file_path, 'r', encoding='utf-8') as file:
return file.read().strip() # 读取文件内容并去掉首尾空白符
except FileNotFoundError:
# 文件未找到时,打印错误信息并退出程序
print(f"File {file_path} not found.")
sys.exit(1)
2.文本预处理功能 (preprocess_text):
def preprocess_text(text):
"""
预处理输入文本,去除标点符号并使用结巴分词对中文进行分词。
:param text: 输入的原始文本
:return: 经过标点去除和分词后的文本
"""
# 使用正则表达式去除文本中的标点符号
text = re.sub(r'[^\w\s]', '', text)
# 使用结巴分词对文本进行分词,并通过空格连接分词结果
return ' '.join(jieba.lcut(text))
3.相似度计算功能 (calculate_similarity):
def calculate_similarity(text1, text2):
"""
计算两个文本之间的TF-IDF余弦相似度。
:param text1: 文本1
:param text2: 文本2
:return: 计算得到的余弦相似度值
"""
# 初始化TF-IDF向量化器
vectorizer = TfidfVectorizer()
# 将两个文本转换为TF-IDF矩阵
tfidf_matrix = vectorizer.fit_transform([text1, text2])
# 计算两个文本的余弦相似度,返回第一个文本与第二个文本的相似度值
return cosine_similarity(tfidf_matrix[0], tfidf_matrix[1])[0][0]
4.相似度写入功能 (write_output):
def write_output(output_path, similarity):
"""
将相似度结果写入指定的输出文件。
:param output_path: 输出文件路径
:param similarity: 计算得到的相似度值
"""
# 以写入模式打开文件,并将相似度写入文件,保留两位小数
with open(output_path, 'w', encoding='utf-8') as f:
f.write(f"{similarity:.2f}\n")
四、性能分析
通过cprofile可以对整个程序的执行进行性能分析,找到耗时较多的部分,然后进一步优化。
main函数运行
cProfile.run("main(orig_path, plagiarized_path, output_path)", filename="performance_analysis_result")
命令行运行
snakeviz.exe -p 8080 .\performance_analysis_result
得到性能分析图
可见在主函数中preprocess_text运行时间最长,故可以考虑对函数preprocess_text进行优化。
五、单元测试
关于测试点的思维导图
测试代码如下:
1.文件操作测试
测试点1: 文件读取成功
测试点2: 文件不存在错误处理
测试点3: 文件输出 - 正确写入相似度
测试点4: 文件输出 - 文件不可写错误处理
# 文件操作测试
# 测试点1: 文件读取成功
def test_read_file_success():
mock_data = "这是一个测试文件"
with patch("builtins.open", mock_open(read_data=mock_data)) as mock_file:
result = read_file("test.txt")
mock_file.assert_called_once_with("test.txt", "r", encoding="utf-8")
assert result == mock_data.strip()
# 测试点2: 文件不存在错误处理
def test_read_file_not_found():
with patch("builtins.open", side_effect=FileNotFoundError):
with pytest.raises(SystemExit):
read_file("non_existent.txt")
# 测试点3: 文件输出 - 正确写入相似度
def test_write_output_success():
similarity = 0.85
with patch("builtins.open", mock_open()) as mock_file:
write_output("output.txt", similarity)
mock_file.assert_called_once_with("output.txt", "w", encoding="utf-8")
mock_file().write.assert_called_once_with("0.85\n")
# 测试点4: 文件输出 - 文件不可写错误处理
def test_write_output_file_permission_error():
with patch("builtins.open", side_effect=PermissionError):
with pytest.raises(PermissionError):
write_output("output.txt", 0.85)
2.文本预处理测试
测试点5: 文本预处理 - 去除标点符号
测试点6: 文本预处理 - 分词功能
测试点7: 文本预处理 - 空白文本处理
测试点8: 长文本处理
# 文本预处理测试
# 测试点5: 文本预处理 - 去除标点符号
def test_preprocess_text_remove_punctuation():
text = "这是一个测试,包含标点符号!"
expected_output = "这是 一个 测试 包含 标点符号"
result = preprocess_text(text)
assert result == expected_output
# 测试点6: 文本预处理 - 分词功能
def test_preprocess_text_segmentation():
text = "这是一个测试"
expected_output = "这是 一个 测试"
result = preprocess_text(text)
assert result == expected_output
# 测试点7: 文本预处理 - 空白文本处理
def test_preprocess_empty_text():
text = ""
result = preprocess_text(text)
assert result == ""
# 测试点8: 长文本处理
def test_long_text_processing():
long_text = "这是一个非常长的测试文本。" * 10000 # 模拟非常长的文本
result = preprocess_text(long_text)
assert isinstance(result, str)
assert len(result) > 0 # 检查处理后的文本不为空
3.相似度计算测试
测试点9: 计算相似度 - 完全相同文本
测试点10: 计算相似度 - 完全不同文本
测试点11: 计算相似度 - 部分相似文本
测试点12: TF-IDF 向量化处理
# 相似度测试
# 测试点9: 计算相似度 - 完全相同文本
def test_calculate_similarity_identical_text():
text1 = "测试文本"
text2 = "测试文本"
similarity = calculate_similarity(text1, text2)
assert similarity == 1.0
# 测试点10: 计算相似度 - 完全不同文本
def test_calculate_similarity_different_text():
text1 = "这是测试A"
text2 = "这是测试B"
similarity = calculate_similarity(text1, text2)
assert similarity == 0.0
# 测试点11: 计算相似度 - 部分相似文本
def test_calculate_similarity_partial_similarity():
text1 = "这是一个测试"
text2 = "这是一个不同的测试"
orig_text1 = preprocess_text(text1)
orig_text2 = preprocess_text(text2)
similarity = calculate_similarity(orig_text1, orig_text2)
assert 0 < similarity < 1.0
# 测试点12: TF-IDF 向量化处理
def test_tfidf_vectorization():
text = "我喜欢学习"
vectorizer = TfidfVectorizer()
tfidf_matrix = vectorizer.fit_transform([text])
assert tfidf_matrix.shape == (1, len(vectorizer.get_feature_names_out()))
测试覆盖率