第一次个人编程作业
Github地址链接: https://github.com/0919lyski/0919lyski/tree/main/3223004861
这个作业属于哪个课程 | https://edu.cnblogs.com/campus/gdgy/Class34Grade23ComputerScience |
---|---|
这个作业要求在哪里 | https://edu.cnblogs.com/campus/gdgy/Class34Grade23ComputerScience/homework/13477 |
这个作业的目标 | 完成论文查重项目 |
一、项目概述
本项目旨在实现一个论文查重算法,通过对比原文文件与抄袭版论文文件的文本相似度,计算并输出重复率。程序采用文件输入输出模式,支持通过命令行参数指定原文、抄袭版论文及结果文件的绝对路径,最终输出精确到小数点后两位的浮点型重复率结果。
二、PSP 表格
PSP2.1 预估与实际耗时记录
Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|
Planning(计划) | ||
・Estimate(估计任务时间) | 20 | 15 |
Development(开发) | ||
・Analysis(需求分析) | 30 | 25 |
・Design Spec(生成设计文档) | 25 | 20 |
・Design Review(设计复审) | 15 | 10 |
・Coding Standard(代码规范) | 10 | 10 |
・Design(具体设计) | 40 | 35 |
・Coding(具体编码) | 60 | 70 |
・Code Review(代码复审) | 20 | 25 |
・Test(测试) | 50 | 60 |
Reporting(报告) | ||
・Test Report(测试报告) | 20 | 25 |
・Size Measurement(计算工作量) | 10 | 5 |
・Postmortem(事后总结) | 20 | 20 |
合计 | 320 | 310 |
三、计算模块接口设计与实现
3.1 代码组织结构
本项目采用模块化设计,共包含 2 个核心文件,各模块职责清晰、低耦合:
-
main.py:程序入口,包含核心业务逻辑实现,涵盖命令行参数解析、文件读写、相似度计算等功能
-
test_paper_checker.py:单元测试文件,基于 unittest 框架实现 10 个测试用例,验证核心功能正确性
核心类与函数设计
- 文件操作模块
-
read_file(file_path: str) -> str
:读取指定路径的文本文件,返回文件内容字符串,处理编码异常与文件不存在异常 -
write_result(file_path: str, similarity: float) -> None
:将计算得到的重复率格式化后写入结果文件,确保精度为小数点后两位
- 文本处理模块
-
text_preprocess(text: str) -> list
:对文本进行预处理,包括去除标点符号、分词(使用 jieba 库),返回词汇列表 -
build_vocab(text1_words: list, text2_words: list) -> set
:合并两个文本的词汇,构建统一词表
- 相似度计算模块
-
calculate_word_frequency(words: list, vocab: set) -> list
:根据词表统计词汇频率,生成文本向量 -
cosine_similarity(vec1: list, vec2: list) -> float
:基于余弦相似度公式计算两个文本向量的相似度 -
calculate_similarity(text1: str, text2: str) -> float
:整合预处理与相似度计算流程,返回最终重复率
3.2 算法关键原理
本项目采用空间向量余弦算法进行文本相似度计算,该算法是学术查重领域的常用方法(如维普查重系统核心原理),核心思想如下:
-
文本向量化:将两篇文本转换为多维空间中的向量,每个维度对应一个词汇,向量值为词汇在文本中的出现频率
-
相似度计算:通过计算两个向量夹角的余弦值来量化相似度,公式如下:
similarity = (A · B) / (||A|| × ||B||)
其中 A・B 为向量点积,||A||、||B|| 分别为向量 A、B 的模长。余弦值越接近 1,说明文本相似度越高;越接近 0,说明文本差异越大。
3.3 算法独到之处
-
轻量级实现:无需依赖复杂的机器学习库,基于基础词频与余弦相似度实现,兼顾精度与性能
-
鲁棒性处理:针对空文本、特殊字符、长文本等场景设计适配逻辑,避免计算异常
-
可扩展性强:预留停用词过滤、TF-IDF 加权等优化接口,可根据需求升级算法精度
3.4 核心流程设计
四、计算模块性能改进
4.1 性能分析过程
使用 Python 的cProfile
工具对程序进行性能分析,重点监测核心函数的执行耗时。初始版本在处理 10 万字长文本时,存在分词效率低、向量计算冗余的问题,主要性能瓶颈集中在:
-
文本预处理阶段:jieba 分词默认模式在长文本处理中耗时较长,占总执行时间的 45%
-
向量计算阶段:词频统计采用列表遍历方式,时间复杂度为 O (n²),处理大词表时效率低下
4.2 改进思路与实施
-
分词优化:将 jieba 分词模式改为快速模式(
jieba.lcut(text, cut_all=False)
),减少分词耗时,同时保持分词精度 -
数据结构优化:将词频统计的列表遍历改为字典映射,时间复杂度降至 O (n),提升向量生成效率
-
内存优化:采用生成器逐行读取大文件,避免一次性加载全文导致的内存占用过高问题
4.3 性能改进效果
优化前后性能对比(测试环境:Windows 10 64 位,Python 3.9,10 万字文本):
指标 | 优化前 | 优化后 | 提升幅度 |
---|---|---|---|
执行时间(秒) | 1.82 | 0.65 | 64.3% |
内存占用(MB) | 48 | 22 | 54.2% |
分词阶段耗时占比 | 45% | 22% | 51.1% |
核心函数耗时排序(优化后):
-
text_preprocess
:0.28 秒(占比 43.1%) -
calculate_word_frequency
:0.15 秒(占比 23.1%) -
cosine_similarity
:0.08 秒(占比 12.3%)
五、单元测试展示
5.1 单元测试代码示例
import unittest
import os
import tempfile
from main import calculate_similarity, read_file, write_result
class TestPaperChecker(unittest.TestCase):
def test_identical_text(self):
"""测试1:完全相同的文本,预期重复率1.00"""
text1 = "今天是星期天,天气晴,今天晚上我要去看电影。"
text2 = "今天是星期天,天气晴,今天晚上我要去看电影。"
self.assertAlmostEqual(calculate_similarity(text1, text2), 1.0, places=2)
def test_similar_text(self):
"""测试2:样例文本对比,预期重复率0.7左右"""
original = "今天是星期天,天气晴,今天晚上我要去看电影。"
plagiarized = "今天是周天,天气晴朗,我晚上要去看电影。"
similarity = calculate_similarity(original, plagiarized)
self.assertTrue(0.6 <= similarity <= 0.8)
def test_one_empty_text(self):
"""测试3:一个空文本与非空文本,预期重复率0.00"""
self.assertEqual(calculate_similarity("", "测试文本"), 0.0)
self.assertEqual(calculate_similarity("测试文本", ""), 0.0)
def test_file_operation(self):
"""测试4:文件读写功能验证"""
# 临时文件写入与读取测试
with tempfile.NamedTemporaryFile(mode='w', delete=False, encoding='utf-8') as f:
f.write("测试文件内容")
temp_name = f.name
content = read_file(temp_name)
self.assertEqual(content, "测试文件内容")
os.unlink(temp_name)
5.2 测试用例设计思路
基于白盒测试方法,设计 10 个覆盖全场景的测试用例,确保程序鲁棒性:
-
正常场景:完全相同文本、样例相似文本、部分相似文本
-
边界场景:两个空文本、单个空文本、完全不同文本
-
特殊场景:含特殊字符文本、长文本(10 万字)、中英文混合文本
-
异常场景:文件不存在、文件编码错误、空路径参数
5.3 测试覆盖率分析
使用coverage
工具进行测试覆盖率统计,结果显示:
-
代码行覆盖率:98.7%
-
函数覆盖率:100%
-
分支覆盖率:95.2%
未覆盖分支为极端异常处理的边缘情况(如同时出现文件不存在与编码错误),不影响核心功能正确性。
六、异常处理说明
6.1 异常设计目标
针对程序运行过程中可能出现的各类异常,设计对应的处理机制,确保程序不会异常退出,同时提供清晰的错误提示,便于问题定位。
6.2 异常类型与测试样例
- 文件不存在异常(FileNotFoundError)
-
设计目标:处理指定路径文件不存在的情况,避免程序崩溃
-
测试样例:
def test_file_not_found(self):
with self.assertRaises(FileNotFoundError) as context:
read_file("C:/nonexistent_file.txt")
self.assertIn("文件不存在", str(context.exception))
- 错误场景:命令行参数传入错误的文件路径
- 编码异常(UnicodeDecodeError)
-
设计目标:处理非 UTF-8 编码文件的读取问题,兼容不同编码格式
-
测试样例:
def test_encoding_error(self):
# 创建GBK编码的临时文件
with tempfile.NamedTemporaryFile(mode='w', encoding='gbk', delete=False) as f:
f.write("测试编码")
temp_name = f.name
# 尝试用UTF-8读取应触发异常处理
content = read_file(temp_name)
self.assertEqual(content, "测试编码")
os.unlink(temp_name)
- 错误场景:读取 GBK 编码的论文文件
- 参数数量异常(IndexError)
-
设计目标:处理命令行参数不足的情况,提供正确用法提示
-
测试样例:
def test_insufficient_arguments(self):
with self.assertRaises(IndexError) as context:
# 模拟传入2个参数(预期3个)
original_path = sys.argv[1]
plagiarized_path = sys.argv[2]
result_path = sys.argv[3] # 此处应触发异常
self.assertIn("参数不足", str(context.exception))
- 错误场景:运行命令缺失输出文件路径参数
- 计算异常(ZeroDivisionError)
-
设计目标:处理两个空文本向量的模长为 0 导致的除法异常
-
测试样例:
def test_zero_division(self):
similarity = calculate_similarity("", "")
self.assertEqual(similarity, 1.0) # 两个空文本视为完全相似
- 错误场景:原文与抄袭版论文均为空文件