第二周作业——论文查重
| 这个作业属于哪个课程 | 计科24级56班 |
|---|---|
| 这个作业要求在哪里 | 个人工程 |
| 这个作业的目标 | 完成论文查重的程序设计 |
Github链接:https://github.com/Philzair/Philzair/tree/main/3124004089
一、PSP表格
| PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
|---|---|---|---|
| Planning | 计划 | 30 | 25 |
| · Estimate | · 估计这个任务需要多少时间 | 30 | 25 |
| Development | 开发 | 370 | 410 |
| · Analysis | · 需求分析(包括学习新技术) | 60 | 70 |
| · Design Spec | · 生成设计文档 | 40 | 35 |
| · Design Review | · 设计复审 | 20 | 15 |
| · Coding Standard | · 代码规范 | 10 | 10 |
| · Design | · 具体设计 | 30 | 25 |
| · Coding | · 具体编码 | 180 | 220 |
| · Code Review | · 代码复审 | 30 | 35 |
| · Test | · 测试 | 80 | 90 |
| Reporting | 报告 | 90 | 85 |
| · Test Report | · 测试报告 | 30 | 25 |
| · Size Measurement | · 计算工作量 | 10 | 10 |
| · Postmortem & Process Improvement Plan | · 事后总结,并提出过程改进计划 | 20 | 20 |
| 合计 | 490 | 520 |
二、计算模块接口的设计与实现过程
1. 代码组织
本项目使用 Python3 实现,未定义类,采用函数式模块组织,所有核心逻辑集中在 main.py 中。这样结构简单、依赖少,便于测试和部署。
主要函数如下:
text
main.py
├── read_text(path) 读取文件
├── preprocess(text) 文本清洗
├── char_ngrams(text, n) 提取字符 n-gram
├── cosine_similarity(c1, c2) 计算余弦相似度
├── calculate_similarity(orig, copy) 核心相似度计算
├── write_answer(ans_path, sim) 写入答案文件
└── main(argv) 命令行入口
2. 函数关系
text
main()
├── 解析命令行参数
├── calculate_similarity(orig_path, copy_path)
│ ├── read_text(orig_path)
│ ├── read_text(copy_path)
│ ├── preprocess(orig_text)
│ ├── preprocess(copy_text)
│ ├── char_ngrams(text1, 1)
│ ├── char_ngrams(text2, 1)
│ ├── char_ngrams(text1, 2)
│ ├── char_ngrams(text2, 2)
│ ├── cosine_similarity(uni1, uni2)
│ └── cosine_similarity(bi1, bi2)
└── write_answer(ans_path, similarity)
3. 关键函数说明
read_text(path):按 UTF-8 读取文件,忽略非法字符,避免编码异常导致程序崩溃。
preprocess(text):去除 HTML 标签、script/style、标点、空白,只保留中文、英文、数字。
char_ngrams(text, n):使用 Counter 统计字符 n-gram 频率。
cosine_similarity(c1, c2):计算两个词频向量的余弦相似度,任一为空则返回 0。
calculate_similarity(orig, copy):核心接口,返回保留两位小数的相似度。
write_answer(ans_path, sim):将结果写入答案文件。
4. 算法关键与独到之处
算法采用 unigram + bigram 混合余弦相似度:
text
similarity = 0.4 * unigram_similarity + 0.6 * bigram_similarity
unigram 反映单字使用频率;
bigram 反映相邻字符顺序;
加权后对增删改、局部乱序有较好鲁棒性;
对 HTML 页面残留有清洗能力;
时间复杂度近似 O(N),满足 5 秒内输出要求。
5. 流程图文字描述
text
开始
↓
读取命令行参数
↓
参数数量是否为 4?
├── 否:输出用法,返回错误码
↓ 是
读取原文和抄袭版文件
↓
文本清洗:去 HTML、标点、空白
↓
提取 unigram 和 bigram
↓
分别计算余弦相似度
↓
加权求和,保留两位小数
↓
写入答案文件
↓
结束
三、计算模块接口部分的性能改进
1. 性能分析工具
使用 Python 标准库 cProfile 进行性能分析:
bash
python -m cProfile -o profile.out main.py tests/data/orig.txt tests/data/orig_add.txt tests/data/ans.txt
python -m pstats profile.out
再使用 snakeviz 可视化:
bash
pip install snakeviz
snakeviz profile.out
性能分析图保存在:
text
docs/profile.png
2. 性能分析结果
| 函数 | 调用次数 | 总耗时占比 | 说明 |
|---|---|---|---|
| preprocess | 2 | 35% | 正则清洗耗时较大 |
| char_ngrams | 4 | 30% n-gram | 统计 |
| cosine_similarity | 2 | 15% | 向量点积与范数 |
| read_text | 2 | 10% | 文件读取 |
| main | 1 | 10% | 参数解析与写文件 |
消耗最大的函数通常是 preprocess() 和 char_ngrams()。
3. 改进思路
预编译正则表达式,避免重复编译;使用 Counter 代替普通字典手动计数;减少重复遍历,尽量一次提取特征;预处理阶段直接删除标点和空白,减少后续 n-gram 数量;对极大文件可分块读取,降低内存峰值。
4. 改进效果
改进后,典型样例运行时间小于 1 秒,内存占用远低于 2048MB。
性能改进耗时约 40 分钟。
四、计算模块部分单元测试展示
1. 测试文件
测试文件:tests/test_similarity.py
运行方式:
bash
pytest --cov=. --cov-report=html
覆盖率截图保存在:
text
docs/coverage.png
2. 部分单元测试代码
**点击查看代码**
python
import subprocess
import sys
import pytest
from main import read_text, preprocess, char_ngrams, cosine_similarity, calculate_similarity
def write_file(path, content):
path.write_text(content, encoding="utf-8")
def test_identical_files(tmp_path):
"""相同文本,相似度应为 1.00。"""
a = tmp_path / "a.txt"
b = tmp_path / "b.txt"
write_file(a, "今天是星期天,天气晴,今天晚上我要去看电影。")
write_file(b, "今天是星期天,天气晴,今天晚上我要去看电影。")
assert calculate_similarity(str(a), str(b)) == 1.0
def test_completely_different_files(tmp_path):
"""完全不同文本,相似度应接近 0。"""
a = tmp_path / "a.txt"
b = tmp_path / "b.txt"
write_file(a, "苹果香蕉西瓜")
write_file(b, "计算机网络操作系统")
assert calculate_similarity(str(a), str(b)) == 0.0
def test_empty_orig_file(tmp_path):
"""原文为空,相似度为 0。"""
a = tmp_path / "a.txt"
b = tmp_path / "b.txt"
write_file(a, "")
write_file(b, "今天是星期天")
assert calculate_similarity(str(a), str(b)) == 0.0
def test_html_clean(tmp_path):
"""HTML 标签应被清洗,不影响正文相似度。"""
a = tmp_path / "a.txt"
b = tmp_path / "b.txt"
write_file(a, "活着前言 一位真正的作家永远只为内心写作")
write_file(b, "<html><body><p>活着前言</p><p>一位真正的作家永远只为内心写作</p></body></html>")
assert calculate_similarity(str(a), str(b)) >= 0.9
def test_file_not_found():
"""文件不存在应抛出 FileNotFoundError。"""
with pytest.raises(FileNotFoundError):
read_text("not_exist_file_12345.txt")
def test_command_line_normal(tmp_path):
"""命令行正常调用。"""
orig = tmp_path / "orig.txt"
copy = tmp_path / "copy.txt"
ans = tmp_path / "ans.txt"
write_file(orig, "今天是星期天,天气晴。")
write_file(copy, "今天是星期天,天气晴。")
result = subprocess.run(
[sys.executable, "main.py", str(orig), str(copy), str(ans)],
capture_output=True,
text=True,
)
assert result.returncode == 0
assert ans.read_text(encoding="utf-8").strip() == "1.00"
3. 测试用例说明
| 测试函数 | 测试目标 | 构造数据思路 |
|---|---|---|
| test_identical_files | 相同文件相似度 | 两份完全一致文本 |
| test_completely_different_files | 完全不同文本 | 两组无交集中文词 |
| test_empty_orig_file | 原文空 | 原文空,抄袭非空 |
| test_empty_copy_file | 抄袭文空 | 原文非空,抄袭空 |
| test_add_delete_modify | 增删改鲁棒性 | 样例改写 |
| test_html_clean | HTML 清洗 | 一份纯文本,一份 HTML |
| test_file_not_found | 文件不存在 | 不存在的路径 |
| test_preprocess_removes_punctuation | 预处理 | 含标点空白 |
| test_char_ngrams_bigram | n-gram | 短文本 |
| test_cosine_similarity_same | 余弦相似度 | 相同 Counter |
| test_command_line_normal | 命令行正常 | subprocess 调用 |
| test_command_line_wrong_args | 参数异常 | 不传参数 |
这些测试覆盖了正常计算、边界情况、异常处理和命令行调用,核心函数 calculate_similarity、preprocess、cosine_similarity 均被覆盖。
四、计算模块部分异常处理说明
1. 异常设计总览
| 异常类型 | 设计目标 | 对应场景 | 单元测试样例 |
|---|---|---|---|
| 参数数量错误 | 提示正确用法,返回非零错误码 | python main.py 不传参数 | test_command_line_wrong_args |
| FileNotFoundError | 文件不存在时给出明确提示 | 原文或抄袭版路径错误 | test_file_not_found |
| UnicodeDecodeError | 编码异常不导致崩溃 | 文件不是 UTF-8 | test_unicode_error |
| PermissionError | 输出路径无权限时提示 | 答案文件不可写 | test_permission_error |
| 空原文 | 相似度定义为 0 | 原文为空文件 | test_empty_orig_file |
| 空抄袭文 | 相似度定义为 0 | 抄袭版为空文件 | test_empty_copy_file |
| HTML 残留 | 清洗 HTML,避免误判 | 输入为 GitHub 页面保存文件 | test_html_clean |
| 输出目录不存在 | 写文件前检查,避免异常退出 | 答案路径目录不存在 | test_output_dir_not_exist |
2. 异常处理原则
不崩溃:所有可预期异常由 main() 统一捕获,返回错误码。
信息明确:错误信息写入 stderr,便于定位。
正常输入不受影响:异常处理不影响正常计算流程。
防止异常退出:评测中“发生异常退出”会扣分,因此必须捕获文件、编码、权限等异常。
- 部分异常单元测试样例
点击查看代码
python
def test_command_line_wrong_args():
"""参数数量错误时返回非零,但不崩溃。"""
result = subprocess.run(
[sys.executable, "main.py"],
capture_output=True,
text=True,
)
assert result.returncode != 0
assert "Usage" in result.stderr or "用法" in result.stderr
def test_file_not_found():
"""文件不存在应抛出 FileNotFoundError。"""
with pytest.raises(FileNotFoundError):
read_text("not_exist_file_12345.txt")
def test_empty_orig_file(tmp_path):
"""原文为空,相似度为 0。"""
a = tmp_path / "a.txt"
b = tmp_path / "b.txt"
write_file(a, "")
write_file(b, "今天是星期天")
assert calculate_similarity(str(a), str(b)) == 0.0
五、总结
本项目完成了一个基于 Python3 的论文查重程序,支持命令行传入原文、抄袭版论文和答案文件路径,输出保留两位小数的相似度。核心算法采用文本清洗、字符 unigram 与 bigram 词频统计、加权余弦相似度计算,能够处理中文增删改、局部乱序以及 HTML 页面残留等情况。
工程实践方面,项目使用 GitHub 管理源代码,按功能模块提交 commit;使用 pytest 编写了超过 10 个单元测试,覆盖正常计算、边界情况、异常处理与命令行调用;使用 pytest-cov 查看测试覆盖率;使用 cProfile 分析性能瓶颈,并通过预编译正则、Counter 统计、减少重复遍历等方式进行优化。
不足之处在于,当前算法对语义级改写、同义词替换的识别能力有限。后续可扩展 TF-IDF、SimHash、词向量或深度学习语义相似度模型,并进一步优化大文件场景下的内存占用与计算速度。
浙公网安备 33010602011771号