第一次个人编程作业
第一次个人编程作业 - 论文查重
Github 仓库地址:
https://github.com/zzy685/ZZY685
(代码位于仓库中学号命名的文件夹下)
一、PSP 表格
| PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
|---|---|---|---|
| Planning | 计划 | 20 | 15 |
| · Estimate | ・估计这个任务需要多少时间 | 10 | 5 |
| Development | 开发 | 180 | 150 |
| · Analysis | ・需求分析 (包括学习新技术) | 30 | 25 |
| · Design Spec | ・生成设计文档 | 15 | 10 |
| · Design Review | ・设计复审 | 10 | 5 |
| · Coding Standard | ・代码规范 (为目前的开发制定合适的规范) | 10 | 5 |
| · Design | ・具体设计 | 20 | 15 |
| · Coding | ・具体编码 | 60 | 50 |
| · Code Review | ・代码复审 | 15 | 10 |
| · Test | ・测试(自我测试,修改代码,提交修改) | 20 | 25 |
| Reporting | 报告 | 60 | 50 |
| · Test Report | ・测试报告 | 20 | 15 |
| · Size Measurement | ・计算工作量 | 10 | 5 |
| · Postmortem & Process Improvement Plan | ・事后总结,并提出过程改进计划 | 30 | 30 |
| 合计 | 260 | 215 |
二、计算模块接口的设计与实现过程
2.1 功能划分与版本控制
本项目按照开发阶段划分为初始完整版本、性能优化版本和HTML 兼容版本三个阶段,每个阶段完成后分别 commit 到 Github,共三次 commit:
| 阶段 | 包含文件 | 功能说明 | Commit Message |
|---|---|---|---|
| 初始完整版本(Commit 1) | main.py、plagiarism_checker.py、requirements.txt、test_plagiarism_checker.py、test_data/ |
实现论文查重核心逻辑(文本预处理、jieba 分词、词频向量余弦相似度、字符级 bigram 相似度、加权融合、命令行参数解析、文件读写),并配套 24 个单元测试用例和测试样例数据 | Add files via upload |
| 性能优化版本(Commit 2) | plagiarism_checker.py(更新) |
使用 cProfile 分析性能瓶颈后,优化文本预处理(避免重复 clean_text 调用)、优化余弦相似度计算(单次遍历同时计算点积和模长),性能提升约 48% | refactor: 性能优化,避免重复文本预处理,优化余弦相似度计算 |
| HTML 兼容版本(Commit 3) | plagiarism_checker.py(更新) |
增加 HTML 纯文本提取功能,自动检测并解析从网页保存的测试文件(如 GitHub 页面),过滤 HTML 标签、CSS 和 JavaScript,提取正文内容进行查重 | feat: 增加HTML纯文本提取,支持从网页保存的测试文件 |
初始版本完成后程序已能正常运行并通过全部测试;优化版本在其基础上进行性能剖析和针对性改进;HTML 兼容版本进一步增强了对非纯文本输入格式的鲁棒性,体现了 "先实现、后优化、再完善" 的开发流程。
2.2 整体架构
main.py # 主程序入口,处理命令行参数和文件IO
plagiarism_checker.py # 核心查重算法模块
test_plagiarism_checker.py # 单元测试
requirements.txt # 依赖声明
2.3 核心函数
| 函数名 | 功能说明 |
|---|---|
extract_text_from_html(html) |
从 HTML 内容中提取纯文本,非 HTML 原样返回 |
clean_text(text) |
文本预处理,去除标点符号和空白字符 |
segment_words(text) |
中文分词,优先使用 jieba,不可用时退化为单字分词 |
get_char_ngrams(text, n) |
生成字符级 n-gram 列表 |
cosine_similarity(vec1, vec2) |
计算两个词频向量的余弦相似度 |
word_level_similarity(text1, text2) |
词级相似度计算 |
ngram_level_similarity(text1, text2, n) |
n-gram 级相似度计算 |
calculate_similarity(orig_text, plag_text) |
综合相似度计算(主接口) |
2.4 算法设计
采用词级余弦相似度 + 字符级 bigram 相似度的加权融合算法:
-
HTML 文本提取:检测输入是否为 HTML 格式(如从网页保存的测试文件),若是则使用
HTMLParser提取纯文本内容,过滤掉 HTML 标签、CSS 和 JavaScript。 -
文本预处理:去除所有标点符号、空白字符,统一转为小写。
-
词级相似度(权重 0.6):使用 jieba 精确模式分词,统计词频向量,计算余弦相似度。
-
字符级 bigram 相似度(权重 0.4):将预处理后的文本切分为相邻字符对,统计频率向量,计算余弦相似度。
-
加权融合:
最终重复率 = 0.6 × 词级相似度 + 0.4 × bigram 相似度。
2.5 算法独到之处
-
HTML 兼容:自动检测并提取 HTML 格式文件中的纯文本,支持从网页直接保存的测试样例,避免 HTML 标签干扰查重结果。
-
双维度融合:词级相似度捕捉语义重复,bigram 相似度捕捉字面重复,对增删改、语序调整等抄袭手段均有较好检测能力。
-
优雅降级:jieba 不可用时自动退化为单字分词,保证程序在任何环境下都能运行。
-
多编码兼容:文件读取支持 UTF-8、GBK、GB2312、UTF-16 等多种编码。
-
边界保护:对空文本、纯标点文本等边界情况完善处理,不会出现除零错误或异常退出。
2.6 关键流程图
开始
│
▼
读取原文文件和抄袭版文件
│
▼
HTML 文本提取(自动检测并过滤标签)
│
▼
文本预处理(去标点、去空白、转小写)
│
├─────────────────┐
▼ ▼
jieba 分词 字符级 bigram 切分
│ │
▼ ▼
统计词频向量 统计 bigram 频率向量
│ │
▼ ▼
计算余弦相似度 计算余弦相似度
│ │
└────────┬────────┘
▼
加权融合 (0.6/0.4)
│
▼
结果保留两位小数
│
▼
写入答案文件
│
▼
结束
2.7 程序运行效果
使用作业示例进行测试:

原文与抄袭版的重复率为 0.79,结果已正确写入答案文件。
三、计算模块接口部分的性能改进
3.1 性能分析工具
使用 Python 内置的 cProfile 性能分析工具,对长文本样例(约 300 字)重复运行 100 次进行分析。
3.2 性能分析结果(改进前)

3.3 性能瓶颈分析
clean_text(文本预处理):被调用 400 次,累计 0.020 秒。原因是word_level_similarity和ngram_level_similarity各自内部调用clean_text,导致同一文本被预处理两次。cosine_similarity(余弦相似度):累计 0.017 秒。原始实现分别计算点积、模 1、模 2,需要遍历向量多次,且创建了common_keys集合。get_char_ngrams(n-gram 生成):累计 0.013 秒。
3.4 改进思路与措施
改进一:避免重复预处理
- 在
calculate_similarity中统一调用clean_text一次,将预处理后的文本传入内部函数_segment_words_cleaned和_get_char_ngrams_cleaned,消除重复的预处理开销。
改进二:余弦相似度计算优化
- 原始实现需要遍历向量 3 次(点积、模 1、模 2),并创建交集集合。
- 改进后在一次遍历中同时累加点积和 vec1 的模长,选择较短的向量遍历以减少循环次数,消除了集合创建开销。
改进三:内部函数封装
- 新增
_segment_words_cleaned和_get_char_ngrams_cleaned两个内部函数,接受已预处理的文本,避免重复清洗。
3.5 改进前后对比
| 指标 | 改进前 | 改进后 |
|---|---|---|
| 100 次运行总耗时 | 0.046 秒 | 0.024 秒 |
| 单次平均耗时 | 0.00046 秒 | 0.00024 秒 |
| 函数调用次数 | 211,222 次 | 56,822 次 |
| 性能提升 | — | 约 48% |
改进后程序在 300 字文本上单次运行仅约 0.24 毫秒,远低于 5 秒的时间限制,内存占用也远低于 2048MB 限制。
四、计算模块部分单元测试展示
4.1 测试框架
使用 Python 标准库 unittest 编写单元测试,共设计 24 个测试用例,远超要求的 10 个。
4.2 测试用例分类
| 测试类 | 测试用例数 | 覆盖范围 |
|---|---|---|
TestCleanText |
4 | 标点去除、空白去除、空输入、纯标点输入 |
TestSegmentWords |
2 | 正常分词、空文本分词 |
TestCharNgrams |
3 | bigram 生成、短文本、空文本 |
TestCosineSimilarity |
3 | 相同向量、正交向量、空向量 |
TestCalculateSimilarity |
9 | 相同文本、不同文本、作业示例、空输入、空白文本、部分重叠、语序调整、长文本、范围校验 |
TestFileOperations |
2 | 结果写入读取、UTF-8 文件读取 |
TestMainIntegration |
1 | 完整文件输入输出流程 |
4.3 核心测试用例说明
测试 15:作业示例验证
def test_homework_example(self):
orig = "今天是星期天,天气晴,今天晚上我要去看电影。"
plagiarized = "今天是周天,天气晴朗,我晚上要去看电影。"
sim = calculate_similarity(orig, plagiarized)
self.assertGreater(sim, 0.5) # 应有较高相似度
self.assertLess(sim, 1.0) # 但不应完全相同
构造思路:直接使用作业题目给出的示例,验证算法对典型增删改抄袭的检测能力。
测试 13:完全相同文本
def test_identical_texts(self):
text = "今天是星期天,天气晴,今天晚上我要去看电影。"
sim = calculate_similarity(text, text)
self.assertAlmostEqual(sim, 1.0, places=2)
构造思路:验证自比较时重复率应为 1.0(100%)。
测试 19:语序调整
def test_word_order_change(self):
text1 = "我喜欢吃苹果和香蕉。"
text2 = "香蕉和苹果我喜欢吃。"
sim = calculate_similarity(text1, text2)
self.assertGreater(sim, 0.4)
构造思路:词语完全相同但语序颠倒,验证词级相似度对语序变化的鲁棒性。
测试 24:完整集成测试
def test_full_pipeline(self):
# 使用 tempfile 创建临时文件,模拟命令行调用 main()
# 验证输出文件存在且结果在合理范围内
...
构造思路:端到端验证文件读写、命令行参数解析和结果输出。
4.4 测试运行结果


全部 24 个测试用例通过,覆盖了正常场景、边界条件和异常输入。
4.5 测试覆盖率

测试设计评价:24 个测试用例从单元测试到集成测试层层递进,覆盖文本预处理、分词、相似度计算、文件 IO 等所有核心功能,包含空输入、纯标点、短文本、长文本等边界情况,能够满足该程序的测试要求。
五、计算模块部分异常处理说明
5.1 命令行参数不足
异常场景:用户运行程序时未提供足够的命令行参数(少于 3 个)。
处理方式:在 main.py 中检查 sys.argv 长度,若不足则打印使用说明并以非零状态码退出,提示用户正确的调用格式。
if len(sys.argv) != 4:
print("用法: python main.py [原文文件路径] [抄袭版论文文件路径] [答案文件路径]")
sys.exit(1)
测试样例:运行 python main.py(无参数),程序输出用法说明并退出,不会抛出未捕获异常。
5.2 文件不存在
异常场景:用户指定的原文或抄袭版文件路径不存在。
处理方式:在 read_file 函数中捕获 FileNotFoundError,输出清晰的错误信息指明哪个文件不存在,然后退出程序。
try:
with open(file_path, 'r', encoding=encoding) as f:
return f.read()
except FileNotFoundError:
print(f"错误: 文件不存在 - {file_path}")
sys.exit(1)
测试样例:运行 python main.py nonexistent.txt orig.txt ans.txt,程序提示"错误: 文件不存在 - nonexistent.txt"。
5.3 文件编码不支持
异常场景:输入文件使用了程序不支持的编码格式。
处理方式:read_file 函数依次尝试 UTF-8、GBK、GB2312、UTF-16 等常见编码,全部失败时捕获 UnicodeDecodeError 并提示用户文件编码可能不支持。
for encoding in ['utf-8', 'gbk', 'gb2312', 'utf-16']:
try:
with open(file_path, 'r', encoding=encoding) as f:
return f.read()
except UnicodeDecodeError:
continue
print(f"错误: 无法识别文件编码 - {file_path}")
sys.exit(1)
测试样例:使用 Latin-1 编码的文件作为输入,程序尝试所有支持的编码后输出编码错误提示。
5.4 空文本输入
异常场景:输入文件为空或仅包含空白字符。
处理方式:在 calculate_similarity 入口处检查文本是否为空(去除空白后),若为空则直接返回 0.0,避免后续分词和向量计算出现除零或空集合错误。
if not orig_text.strip() or not plagiarized_text.strip():
return 0.0
测试样例:单元测试 test_empty_inputs 验证空字符串输入返回 0.0,不抛出异常。
5.5 除零保护
异常场景:两篇文本预处理后均为空(如纯标点文本),导致余弦相似度计算中模长为零。
处理方式:在 cosine_similarity 函数中检查向量模长,若任一模长为零则返回 0.0,避免除零错误。
if norm1 == 0 or norm2 == 0:
return 0.0
return dot / (norm1 * norm2)
测试样例:单元测试 test_punctuation_only 验证纯标点文本输入返回 0.0,不抛出 ZeroDivisionError。
六、附录
6.1 项目文件结构
学号/
├── main.py # 主程序入口
├── plagiarism_checker.py # 查重算法核心模块
├── test_plagiarism_checker.py # 单元测试
├── requirements.txt # 依赖(jieba)
└── test_data/ # 测试样例
├── orig.txt
├── orig_add.txt
├── long_orig.txt
└── long_plag.txt
6.2 运行方式
# 安装依赖
pip install -r requirements.txt
# 运行查重
python main.py [原文文件路径] [抄袭版论文文件路径] [答案文件路径]
# 运行单元测试
python -m unittest test_plagiarism_checker -v
6.3 事后总结与改进计划
本次作业通过实现论文查重算法,加深了对文本相似度计算、模块化设计和单元测试的理解。未来可以从以下方面改进:
- 算法优化:引入 TF-IDF 权重,降低常见词的影响;尝试 SimHash 算法支持大规模文档查重。
- 性能提升:对超大规模文本,使用哈希技巧和稀疏向量进一步降低内存和计算开销。
- 测试完善:引入 coverage.py 生成精确覆盖率报告,补充更多真实论文样例测试。

浙公网安备 33010602011771号