论文查重
论文查重
作业GitHub链接:[https://github.com/LAYG13/LAYG13/tree/main/3124004050]
一、PSP 表格(预估)
在开始实现程序之前,我对各个开发阶段的时间做了如下估计:
| PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
|---|---|---|---|
| · Planning | 计划 | 30 | |
| · Estimate | 估计这个任务需要多少时间 | 30 | |
| · Development | 开发 | 60 | |
| · Analysis | 需求分析(包括学习新技术) | 60 | |
| · Design Spec | 生成设计文档 | 45 | |
| · Design Review | 设计复审 | 25 | |
| · Coding Standard | 代码规范(为目前的开发制定合适的规范) | 25 | |
| · Design | 具体设计 | 60 | |
| · Coding | 具体编码 | 100 | |
| · Code Review | 代码复审 | 30 | |
| · Test | 测试(自我测试,修改代码,提交修改) | 45 | |
| · Reporting | 报告 | 45 | |
| · Test Repor | 测试报告 | 30 | |
| · Size Measurement | 计算工作量 | 30 | |
| · Postmortem & Process Improvement Plan | 事后总结,并提出过程改进计划 | 25 | |
| 合计 | 620 |
二、计算模块接口的设计与实现过程
1.总体设计:模块如何组织
代码组织:本项目按 "入口层与算法层分离" 的原则组织,共分为两个包和三个层次:
(1)类设计:共4个类,全部位于checker/exceptions.py,采用统一异常层次结构。基类PaperCheckerError继承自Exception,作为所有自定义异常的父类。其下派生3个子类:UsageError(命令行参数个数不为 3 时抛出)、FileReadError(输入文件不存在、无权限或编码无法识别时抛出)、FileWriteError(答案文件写入失败时抛出)。设计目的是让入口函数只需捕获一次基类即可统一处理所有错误,同时保留错误类型的区分度,便于定位问题。
(2)函数设计:共 9 个函数,按模块划分
- main.py(4 个):parse_args(argv)校验命令行参数;read_text(path)读取文本,按UTF-8到GBK顺序回退解码;write_result(path, rate)将结果保留两位小数写入答案文件;main(argv)为程序主流程,串联全部步骤并负责异常捕获。
- checker/preprocess.py(2个):clean_text(text)完成全角空格归一化与首尾空白去除;tokenize(text)调用jieba分词,过滤标点与空token、英文转小写,并通过LRU缓存提升重复分词性能。
- checker/similarity.py(1个):cosine_similarity(words_a, words_b) 统计两侧词频构成向量,按余弦公式计算相似度,任一向量为空时返回0以避免除零。
- tests/(其余为测试函数,31个用例分属4个测试文件)。
(3)函数间关系:

2.关键函数
- clean_text(text)
负责最基础的文本清洗,把全角空格 \u3000 替换成普通空格,再去掉首尾空白,让后续分词不会受这些无关字符干扰。

- tokenize(text)
先调用 clean_text 清洗文本,再用 jieba.cut 分词,过滤掉空字符串和纯标点 token,英文统一转小写,并用 @lru_cache 缓存结果,避免同一段文本被反复分词。

- cosine_similarity(words_a, words_b)
它接收两个参数:words_a 和 words_b,分别是原文和抄袭文经过 tokenize 处理后的词列表。通过公式cos(θ) = A·B / (|A|×|B|)计算余弦相似度,返回一个[0, 1]之间的浮点数,表示两段文本的相似程度:越接近1越相似,越接近0越不相关。如果任一词表为空就直接返回0.0,避免除零。

3.算法关键:
(1)分词:选择jieba而非自建词典。 中文没有天然空格分隔,必须先分词jieba基于前缀词典和动态规划(DAG),对星期天、天气晴这类常见词切分准确,且是纯Python库,可随requirements.txt一键安装。预处理时统一过滤纯标点token(正则 ^[\W_]+$,Unicode模式下中文属于\w,不会误删),并把英文统一转小写,避免The/the这类大小写差异干扰结果。
(2)相似度:选择余弦相似度。 把论文表示为“词->词频”向量,重复率即两向量夹角的余弦值。相比编辑距离(O(n²))更适合长文本,相比 SimHash 更简单直观、便于解释。且天然带词频加权:高频重复词对相似度贡献更大,符合"查重"直觉。
(3)独到之处:
- 多编码自动兼容:read_text先按二进制读取,再依次尝试utf-8-sig -> tf-8 -> gbk解码。Windows下作业文件可能是任一种编码,全部覆盖,避免"乱码导致重复率失真"。
- 空文本语义明确:任一文本分词后为空,重复率定义为0.00(无内容可比对),避免除零,也符合评测预期。
- 防御性异常体系:三类自定义异常统一被入口捕获,程序绝不产生未处理traceback崩溃,不会发生异常退出。
- 分词LRU缓存:同一文本重复分词直接命中缓存。
4.代码质量分析
使用 pylint 对项目代码进行静态质量分析。初始评分为9.95/10,唯一警告为文件末尾缺少换行符。在文件末尾补上换行后重新分析,评分达到10.00/10,无任何警告。

三、计算模块接口部分的性能改进
1.性能分析工具
作业要求使用VS2017 Profiling Tools/JProfiler,二者分别面向C++与Java。本项目选用Python3,故采用Python官方标准库cProfile+pstats进行等价分析,对程序的行为建模与热点定位能力一致。
分析样本:约一万字的长文本。
2.改进前的数据

结论:有两个瓶颈:
(1)jieba.__cut_DAG累计 0.493s,是分词核心(不可避免的算法成本,线性复杂度)。
(2)marshal.load单次 0.392s,是每次启动时加载词典的固定开销,且启动时会向 stdout 打印三行日志。
3.改进思路
(1)日志抑制:模块加载时jieba.setLogLevel(logging.WARNING),去掉 3 行词典加载日志,减少I/O,stdout 干净。
(2)LRU 缓存:tokenize加@lru_cache(maxsize=128),同一文本重复分词直接命中缓存,从O(n)降为O(1)。
4.改进后数据对比:


改进说明:
改进后程序总耗时为0.537s,较改进前下降5.8%。由于分词本身是O(n)核心成本、无法消除,而词典加载是一次性固定成本,评测中单次运行无可避免。两处改进消除了可去除的开销,大样本整体运行 0.54s 左右。
四、计算模块部分单元测试展示
1.测试框架与覆盖工具
- 单元测试:pytest(31 个用例,全部通过)
- 覆盖率:coverage.py,python -m coverage html生成可视化报告
2.测试用例设计思路
按照语句覆盖+分支覆盖+路径覆盖设计,对每个模块的关键分支逐一构造输入:
| 模块 | 用例数 | 覆盖的分支/路径 |
|---|---|---|
| test_similarity.py | 8 | 相同/不同/部分重合、词频加权、三个空列表分支 |
| test_preprocess.py | 6 | 全角空格、标点过滤、英文小写、空文本分支 |
| test_main.py | 12 | 正常流程 + 参数错误/文件缺失/编码错误/写失败 4 条异常路径 |
| test_exceptions.py | 5 | 异常继承层次与错误信息 |
关键测试代码(构造数据的思路:用可手工验证的数值断言精确值):
def test_partial_overlap_known_value():
"""部分重合:共同词占 2/3,验证数值精确性。"""
assert cosine_similarity(["我", "爱", "编程"], ["我", "爱", "学习"]) == \
pytest.approx(2.0 / 3.0, abs=1e-6)
def test_frequency_weight_affects_result():
"""词频权重:手工计算值 2/sqrt(10),验证加权正确性。"""
value = cosine_similarity(["a", "a", "b"], ["a", "c"])
assert value == pytest.approx(2.0 / math.sqrt(10.0), abs=1e-6)
def test_gbk_encoded_input_readable():
"""GBK 编码输入应被自动识别(分支:多编码尝试路径)。"""
_write(orig, "天气很好,适合出游。", encoding="gbk")
_write(plag, "天气很好,适合出游。", encoding="gbk")
assert main_module.main([str(orig), str(plag), str(ans)]) == 0
assert ans.read_text(encoding="utf-8") == "1.00"
白盒覆盖思路说明:不仅测"正确结果",还针对cosine_similarity的空列表守卫、read_text的编码回退循环、parse_args的参数个数判断、write_result的OSError捕获等每个分支设计用例,因此覆盖率能达到99%。
3.测试覆盖率截图

说明:
- checker三个算法模块100%覆盖;
- main.py缺失的1行为if name == "main"入口保护行,属于解释器执行路径,单元测试无法(也无需)覆盖;
4.测试文本的测试结果

五、计算模块部分异常处理说明
程序定义了统一异常基类PaperCheckerError,派生三类异常,入口main()统一捕获:
| 异常 | 设计目标 | 触发场景 | 处理 |
|---|---|---|---|
| UsageError | 在最早阶段拦截调用错误,避免对不完整参数做无意义的文件操作 | 命令行参数个数 ≠ 3 | 打印参数用法,退出码 1 |
| FileReadError | 区分"输入问题"与"算法问题",把 IO 失败统一成可预期的错误 | 文件不存在、无读取权限、三种编码均无法解码 | 自动尝试 utf-8-sig/utf-8/gbk 后仍失败则报错,退出码 1 |
| FileWriteError | 防止答案文件静默写失败(用户以为成功实际没写) | 输出目录不存在、无写入权限 | 报错退出码 1 |
每个异常对应的单元测试样例:
# 场景1:UsageError —— 参数不足
def test_main_usage_error_exit_code(capsys):
code = main_module.main(["a", "b"]) # 只传 2 个参数
assert code == 1
assert "参数" in capsys.readouterr().err # stderr 有可读错误信息
# 场景2:FileReadError —— 文件不存在
def test_main_missing_input_file(tmp_path, capsys):
missing = str(tmp_path / "not_exist.txt")
code = main_module.main([missing, missing, str(tmp_path / "ans.txt")])
assert code == 1
assert "无法读取文件" in capsys.readouterr().err
# 场景3:FileReadError —— 编码无法识别
def test_read_text_invalid_encoding_raises(tmp_path):
bad = tmp_path / "bad.txt"
bad.write_bytes(b"\xff\xfe\x00\x81\x82\x83") # 三种编码均非法
with pytest.raises(FileReadError):
main_module.read_text(str(bad))
# 场景4:FileWriteError —— 输出目录不存在
def test_main_write_error_exit_code(tmp_path, capsys):
ans = str(tmp_path / "no_such_dir" / "ans.txt")
code = main_module.main([str(orig), str(plag), ans])
assert code == 1
assert "无法写入" in capsys.readouterr().err
设计要点:所有异常都被main()捕获并写入stderr,程序以非零码退出,不会产生未处理的traceback崩溃。
六、PSP表格
完成项目后,各阶段实际耗时记录如下:
| PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
|---|---|---|---|
| · Planning | 计划 | 30 | 25 |
| · Estimate | 估计这个任务需要多少时间 | 30 | 20 |
| · Development | 开发 | 60 | 70 |
| · Analysis | 需求分析(包括学习新技术) | 60 | 60 |
| · Design Spec | 生成设计文档 | 45 | 60 |
| · Design Review | 设计复审 | 25 | 20 |
| · Coding Standard | 代码规范(为目前的开发制定合适的规范) | 25 | 20 |
| · Design | 具体设计 | 60 | 80 |
| · Coding | 具体编码 | 100 | 150 |
| · Code Review | 代码复审 | 30 | 20 |
| · Test | 测试(自我测试,修改代码,提交修改) | 45 | 40 |
| · Reporting | 报告 | 45 | 60 |
| · Test Repor | 测试报告 | 30 | 30 |
| · Size Measurement | 计算工作量 | 30 | 20 |
| · Postmortem & Process Improvement Plan | 事后总结,并提出过程改进计划 | 25 | 30 |
| 合计 | 620 | 705 |

浙公网安备 33010602011771号