第一次个人编程作业
我的第一次个人编程作业:论文查重
GitHub仓库链接:https://github.com/mosquito512/3124004051
一、PSP表格

二、计算模块设计与实现分析
2.1 需求分析
题目:论文查重
设计一个论文查重算法,给出一个原文件和一个在这份文件原文上经过增删改的抄袭版论文的文件,在答案文件中输出其重复率。
·输入(命令行参数,空格分割,路径不含空格):
a.原论文的文件绝对路径;
b.抄袭版论文的文件绝对路径;
c.输出的答案文件的绝对路径。
·输出:答案文件中写入重复率,浮点型,精确到小数点后两位
·调用约定:Python main.py [原文][抄袭版][答案]
示例:
·原文:今天是星期天,天气晴,今天晚上我要去看电影。
·抄袭版:今天是周天,天气晴朗,我晚上要去看电影。
2.2整体架构
程序分为两个核心类 + 一个主入口:
main()
├── TextProcessor.read_file() # 文件读取(多编码自动识别)
├── TextProcessor.tokenize() # 分词 + 词性过滤 + 停用词过滤
├── SimilarityCalculator.build_mixed_features() # unigram + bigram 混合特征
├── SimilarityCalculator.build_vector() # 词频向量(Counter)
└── SimilarityCalculator.cosine_similarity() # 余弦相似度
类职责划分:
·TextProcessor:隔离IO与文本处理,负责文件读取和分词过滤;
·SimilarityCalculator:负责特征工程与相似度计算,纯计算无IO;
·main():命令行解析、流程编排、异常处理。
2.3算法设计
查重算法:分词 + 混合特征 + 余弦相似度
1.分词:使用 jieba 词性标注分词(jieba.posseg.cut),仅保留名词、动词、形容词、副词、数词和英文,过滤代词、介词、连词、助词、标点等噪声;
2.停用词过滤:内置约 90 个中文停用词表,过滤 "的、了、在、是、我们" 等高频虚词;
3.混合特征:将分词结果构建为 unigram(单词)+ bigram(相邻词对)的混合特征列表。bigram 能捕捉短语级重复,对增删改写更高效;
4.余弦相似度:将混合特征转为词频向量,用余弦公式计算相似度。
余弦相似度公式:

结果范围 [0, 1],0 表示完全不同,1 表示完全相同。
关键函数流程图(tokenize):
输入文本
│
▼
jieba.posseg.cut 分词标注
│
▼
词性在白名单?(n/v/a/d/m/eng)
├── 否 → 丢弃
└── 是 → 停用词表中?
├── 是 → 丢弃
└── 否 → 含中文/字母/数字?
├── 否 → 丢弃
└── 是 → 保留
2.4 关键代码实现
分词与过滤(TextProcessor.tokenize):
@staticmethod
def tokenize(text: str) -> list:
text = (text or "").strip()
if not text:
return []
if _HAS_JIEBA:
words = []
for pair in pseg.cut(text):
w = pair.word.strip()
flag = pair.flag
if not w or w.isspace():
continue
# 词性白名单:实词首字母或英文
if not (flag[0] in _KEEP_POS_PREFIX or flag == "eng"):
continue
# 过滤停用词
if w.lower() in _STOP_WORDS:
continue
if any(ch.isalnum() or "\u4e00" <= ch <= "\u9fff" for ch in w):
words.append(w)
return words
# 回退:字符 bigram
if len(text) < 2:
return [text]
return [text[i:i + 2] for i in range(len(text) - 1)]
混合特征构建(build_mixed_features):
@staticmethod
def build_mixed_features(words: list) -> list:
if not words:
return []
features = list(words) # unigram
for i in range(len(words) - 1):
features.append(f"{words[i]}_{words[i + 1]}") # bigram
return features
余弦相似度(cosine_similarity):
@staticmethod
def cosine_similarity(vec1: Counter, vec2: Counter) -> float:
if not vec1 or not vec2:
return 0.0
common_keys = set(vec1.keys()) & set(vec2.keys())
dot_product = sum(vec1[k] * vec2[k] for k in common_keys)
norm1 = math.sqrt(sum(v * v for v in vec1.values()))
norm2 = math.sqrt(sum(v * v for v in vec2.values()))
if norm1 == 0.0 or norm2 == 0.0:
return 0.0
return dot_product / (norm1 * norm2)
2.5 独到之处
1.ieba 不可用时自动回退:若环境未安装 jieba,自动回退到字符 bigram 分词,保证程序在任何环境下可运行,不会因依赖缺失而崩溃;
2.多编码自动识别:依次尝试 utf-8、utf-8-sig、gbk、gb18030、latin-1,兼容不同来源的文本文件;
3.unigram + bigram 混合特征:既看单词重合也看短语重合,bigram 对 "增删改" 型抄袭更鲁棒,能捕捉被改写语序后的短语重合;
4.词性过滤 + 停用词双重降噪:先通过词性标注去掉虚词,再通过停用词表去掉高频无意义词,显著提升信号质量。
三、计算模块接口性能改进
3.1性能分析方法
使用 Python 标准库 cProfile 对核心查重流程进行性能分析。测试文本为模拟论文级长文本:
·原文:19,990 字符
·抄袭版:18,383 字符
·总函数调用:527 万次
·总耗时:1.942 秒
分析命令:
python profile_run.py
3.2 性能分析结果
性能分析图(Top 10 函数耗时):

消耗最大的函数:TextProcessor.tokenize
|函数|累计耗时|占比|说明|
|main.py:tokenize|1.938s|99.8%|绝对瓶颈,自有函数中最大|
|jieba.posseg.cut|1.891s|97.4%|词性标注分词(内部viterbi算法)|
|jieba词典加载|0.524s|27.0%|一次性开销,首次调用时加载|
|build_mixed_features|<0.01s|<0.1%|可忽略|
|cosine_similarity|<0.01s|<0.1%|可忽略|
3.3改进思路与实施
瓶颈分析:
·99.8%的时间花在tokenize上,其中绝大部分是jieba词性标注的viterbi算法
·余弦相似度和向量构建耗时极短,无需优化
·jieba词典加载是一次性开销(0.524s),避免首次调用时的0.5s延迟
改进措施:
1.预加载词典:程序启动时调用 jieba.initialize(),避免首次调用时的 0.5s 延迟
if _HAS_JIEBA:
jieba.initialize() # 预加载词典
四、单元测试展示
4.1 测试设计
测试框架:pytest + pytest-cov(覆盖率)
测试方法:白盒测试,覆盖每个函数的正常分支与异常分支
等价类划分:
·正常文本 / 空文本 / 纯标点 / 单字符
·完全相同 / 完全不同 / 部分重叠
·文件存在 / 不存在 / 空文件 / 多编码
·参数正确 / 参数不足
测试用例清单(共 27 个):
|测试类|用例数|覆盖内容|
|TestReadFile|4|UTF-8正常读取/文本不存在/空文件/GBK编码|
|TestTokenize|5|返回类型/空文本/None防御/中文非空/单字符|
|TestBuildVector|3|词频统计/空列表/返回Counter|
|TestBuildMixedFeature|5|unigram+bigram/长度/空/单词/短语集合|
|TestCosineSimilarity|7|相同=1/不同=0/部分重叠/空向量*2/示例文本|
|TestMainEndToEnd|4|端到端写文件/参数不足/文件不存在/空文件|
4.2 关键测试代码
构造测试数据思路:
·使用 pytest 的 tmp_path fixture 创建临时文件,不污染真实环境
·使用 monkeypatch 模拟命令行参数 sys.argv
·使用 pytest.raises(SystemExit) 捕获 sys.exit() 并校验退出码
端到端测试(test_main_writes_answer_file):
def test_main_writes_answer_file(self, tmp_path, monkeypatch, sample_orig, sample_plag):
orig = tmp_path / "orig.txt"
plag = tmp_path / "plag.txt"
ans = tmp_path / "ans.txt"
orig.write_text(sample_orig, encoding="utf-8")
plag.write_text(sample_plag, encoding="utf-8")
模拟命令行参数
monkeypatch.setattr(sys, "argv", ["main.py", str(orig), str(plag), str(ans)])
dut.main()
校验答案文件存在且内容为两位小数浮点
assert ans.exists()
content = ans.read_text(encoding="utf-8").strip()
value = float(content)
assert 0.0 <= value <= 1.0
assert "." in content and len(content.split(".")[1]) == 2
混合特征测试(test_mixed_improves_overlap_detection):
def test_mixed_improves_overlap_detection(self):
两文本单词有差异,但共享短语"机器学习"
w1 = ["机器", "学习", "算法", "研究"]
w2 = ["机器", "学习", "模型", "应用"]
f1 = SimilarityCalculator.build_mixed_features(w1)
f2 = SimilarityCalculator.build_mixed_features(w2)
短语特征 "机器_学习" 同时出现在两边
assert "机器_学习" in f1 and "机器_学习" in f2
异常路径测试(test_main_insufficient_args):
def test_main_insufficient_args(self, monkeypatch, capsys):
monkeypatch.setattr(sys, "argv", ["main.py"])
with pytest.raises(SystemExit) as exc_info:
dut.main()
assert exc_info.value.code == 1
captured = capsys.readouterr()
assert "用法" in captured.err
4.3 测试覆盖率
运行命令:
pytest test_main.py -v --cov=main --cov-report=term-missing
覆盖率结果:
Name Stmts Miss Cover
-----------------------------
main.py 124 24 81%
-----------------------------
TOTAL 124 24 81%
未覆盖分支说明:
·jieba 不可用时的字符 bigram 回退分支(测试环境已装 jieba)
·部分异常兜底分支(PermissionError、UnicodeDecodeError 兜底、OSError)
·答案文件目录自动创建逻辑
五、计算模块异常处理说明
5.1 异常设计目标
程序采用 "明确报错 + 标准化退出码" 的异常处理策略:
·每种异常都打印明确的错误信息到 stderr,避免静默崩溃
·不同类型的异常对应不同的退出码,方便自动化测试和脚本调用
·兜底捕获所有未预期异常,保证程序不会因未知错误而崩溃
5.2 异常分类与处理
|异常类型|退出码|设计目标|对应场景|
|参数不足|1|校验命令行参数数量,提示正确用法|用户未传够3个文件路径|
|FileNotFoundError|2|输入文件不存在时明确报错|原文或抄袭版文件路径错误|
|PermissionError|3|无读写权限时明确报错|文件被占用或无权限|
|ValueError|4|内容为空或分词结果为空时报错|空文件、全标点文件|
|UnicodeDecodeError|5|所有编码都无法解码时报错|损坏的二进制文件|
|OSError|6|其他IO错误|写入答案文件失效|
|未知异常|99|兜底捕获,打印异常类型和信息|任何为预期错误|
5.3 异常处理代码
try:
... 核心流程 ...
except FileNotFoundError as e:
print(f"[文件错误] {e}", file=sys.stderr)
sys.exit(2)
except PermissionError:
print("[权限错误] 没有权限读取输入文件或写入答案文件", file=sys.stderr)
sys.exit(3)
except ValueError as e:
print(f"[内容错误] {e}", file=sys.stderr)
sys.exit(4)
except UnicodeDecodeError as e:
print(f"[编码错误] {e}", file=sys.stderr)
sys.exit(5)
except OSError as e:
print(f"[IO 错误] {e}", file=sys.stderr)
sys.exit(6)
except Exception as e:
print(f"[未知错误] {type(e).__name__}: {e}", file=sys.stderr)
sys.exit(99)
5.4 单元测试样例
样例 1:参数不足(退出码 1)
def test_main_insufficient_args(self, monkeypatch, capsys):
monkeypatch.setattr(sys, "argv", ["main.py"])
with pytest.raises(SystemExit) as exc_info:
dut.main()
assert exc_info.value.code == 1
错误场景:用户运行 python main.py 未传入任何参数。
样例 2:文件不存在(退出码 2)
def test_main_file_not_found(self, tmp_path, monkeypatch):
ans = tmp_path / "ans.txt"
monkeypatch.setattr(sys, "argv", [
"main.py",
str(tmp_path / "no_orig.txt"),
str(tmp_path / "no_plag.txt"),
str(ans)
])
with pytest.raises(SystemExit) as exc_info:
dut.main()
assert exc_info.value.code == 2
错误场景:传入的原文或抄袭版文件路径不存在。
样例 3:空文件(退出码 4)
def test_main_empty_input_file(self, tmp_path, monkeypatch):
orig = tmp_path / "orig.txt"
plag = tmp_path / "plag.txt"
ans = tmp_path / "ans.txt"
orig.write_text("", encoding="utf-8")
plag.write_text("有内容", encoding="utf-8")
monkeypatch.setattr(sys, "argv", ["main.py", str(orig), str(plag), str(ans)])
with pytest.raises(SystemExit) as exc_info:
dut.main()
assert exc_info.value.code == 4
错误场景:原文文件为空,分词后无有效内容。
事后总结与改进计划
·编码阶段实际耗时超过预估,主要因为 jieba 词性过滤的调试比预期复杂
·测试阶段耗时符合预估,pytest 的 tmp_path/monkeypatch 大幅提升了测试编写效率
·改进计划:下次提前调研第三方库的 API,减少编码阶段的试错时间
六、附录
7.1项目结构
plagiarism_checker/
├── main.py # 主程序(TextProcessor + SimilarityCalculator + main)
├── test_main.py # 单元测试(27 个用例)
├── requirements.txt # 依赖(jieba / pytest / pytest-cov / matplotlib)
├── profile_run.py # cProfile 性能分析脚本
├── performance_top10.png # 性能分析图
├── performance_report.txt # cProfile 文本报告
└── profile_result.prof # cProfile 原始数据
6.2 参考资料
·邹欣老师博客:现代软件工程讲义 2 工程师的能力评估和发展(PSP)
·邹欣老师博客:源代码管理
·邹欣老师博客:现代软件工程讲义 2 开发技术 - 单元测试 & 回归测试
·阮一峰博客:Commit message 和 Change log 编写指南
·jieba 中文分词文档
6.3运行方式
# 安装依赖
pip install -r requirements.txt
# 运行查重
python main.py 原文绝对路径 抄袭版绝对路径 答案绝对路径
# 运行单元测试
pytest test_main.py -v
# 运行带覆盖率的测试
pytest test_main.py --cov=main --cov-report=term-missing
# 性能分析
python profile_run.py
浙公网安备 33010602011771号