第一次个人编程作业
论文查重项目报告
一、作业基本信息
| 这个作业属于哪个课程 | https://edu.cnblogs.com/campus/gdgy/Class34Grade23ComputerScience/ |
|---|---|
| 这个作业要求在哪里 | https://edu.cnblogs.com/campus/gdgy/Class34Grade23ComputerScience/homework/13477 |
| 这个作业的目标 | 实现一个论文查重程序,并且熟悉项目开发的流程 |
| github仓库链接 | https://github.com/yeah-a/3123004804 |
二、PSP表格
| PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
|---|---|---|---|
| Planning | 计划 | 70 | 75 |
| · Estimate | · 估计这个任务需要多少时间 | 70 | 75 |
| Development | 开发 | 500 | 540 |
| · Analysis | · 需求分析 (包括学习新技术) | 55 | 60 |
| · Design Spec | · 生成设计文档 | 45 | 50 |
| · Design Review | · 设计复审 | 35 | 38 |
| · Coding Standard | · 代码规范 (为目前的开发制定合适的规范) | 25 | 28 |
| · Design | · 具体设计 | 65 | 70 |
| · Coding | · 具体编码 | 140 | 150 |
| · Code Review | · 代码复审 | 45 | 48 |
| · Test | · 测试(自我测试,修改代码,提交修改) | 90 | 96 |
| Reporting | 报告 | 130 | 145 |
| · Test Report | · 测试报告 | 50 | 55 |
| · Size Measurement | · 计算工作量 | 30 | 33 |
| · Postmortem & Process Improvement Plan | · 事后总结, 并提出过程改进计划 | 50 | 57 |
| 合计 | - | 700 | 760 |
三、需求分析
1 功能需求
核心功能:计算两篇论文(原文与抄袭版)的重复率,输出结果精确到小数点后两位。
输入输出:通过命令行参数接收3个文件路径(原文绝对路径、抄袭版绝对路径、结果输出绝对路径),程序读取输入文件内容,计算后将结果写入输出文件。
算法要求:需设计文本相似度计算算法,能处理中文文本的增删改(如示例中“星期天”改为“周天”、“天气晴”改为“天气晴朗”等场景)。
2 非功能需求
性能约束:单个测试点需在5秒内完成计算,内存占用≤2048MB,无严重内存泄漏。
安全性:禁止连接网络、读写非指定文件、执行系统命令(如shutdown)等危险操作。
鲁棒性:需处理文件不存在、空文件、格式错误等异常,避免程序崩溃或异常退出。
兼容性:Python程序入口为main.py,需通过命令行参数(python main.py [原文] [抄袭版] [结果])运行。
3 测试标准
至少10个测试点,覆盖不同文本修改场景(增、删、改、同义词替换等)。
程序需可正常编译/运行,逻辑与提交代码一致。
四、计算模块接口的设计与实现流程
1 需求细化
1.1 输入输出规范
命令行参数:3个绝对路径,格式为python main.py [原文路径] [抄袭版路径] [结果路径],例如:python src/main.py C:\test\orig.txt C:\test\orig_add.txt C:\test\ans.txt。
文件读写:支持.txt格式,编码为utf-8,处理空文件、超大文件(≤1GB)。
结果精度:输出浮点型,保留两位小数(如0.85而非0.850)。
1.2 核心算法设计
流程:文本预处理→分词→TF-IDF向量化→余弦相似度计算。
预处理步骤:
- 去除标点符号(正则表达式:re.sub(r'[^\w\s\u4e00-\u9fa5]', '', text));
- 分词(使用jieba.lcut,精确模式);
- 去停用词(自定义停用词表,包含“的”“是”等通用词及领域无关词)。
2 模块设计与关键函数流程图
2.1 模块划分
| 模块文件 | 核心函数/类 | 功能描述 |
|---|---|---|
| file_utils.py | read_file(path) | 读取文件内容,处理编码错误 |
| write_result(path, score) | 写入结果到文件,确保两位小数 | |
| preprocess.py | clean_text(text) | 去除标点、特殊字符 |
| segment_text(text) | 分词并过滤停用词 | |
| preprocess(text) | 整合清洗+分词,返回处理后文本 | |
| similarity.py | tfidf_vectorize(texts) | 生成TF-IDF向量 |
| cosine_similarity(vec1, vec2) | 计算两向量余弦相似度 | |
| main.py | main() | 解析命令行参数,串联模块执行流程 |
3 模块接口实现步骤
步骤1:文件处理模块(file_utils.py)
核心需求:安全读取文本、精确写入结果,处理文件异常(不存在、权限不足、编码错误)。
- read_file(path)实现:
检查文件是否存在(os.path.exists),不存在则抛出FileNotFoundError;
检查是否为文件(非目录),否则抛出IsADirectoryError;
尝试以utf-8编码读取,失败则捕获UnicodeDecodeError(提示“文件编码需为utf-8”);
返回读取的文本内容(空文件返回空字符串)。 - write_result(path, score)实现:
格式化得分(f"{score:.2f}"),确保两位小数(如0.8→0.80);
尝试写入文件,捕获PermissionError(提示“无写入权限”);
无返回值,直接写入结果到指定路径。
步骤2:文本预处理模块(preprocess.py)
核心需求:将原始文本转换为可用于特征提取的“干净”token序列,适配中文场景。
- clean_text(text)实现:
若输入文本为空,直接返回空字符串;
用正则表达式r'[^\u4e00-\u9fa5a-zA-Z0-9\s]'去除所有非中文字符、字母、数字及空格;
用re.sub(r'\s+', ' ', text)合并连续空格为单个空格,返回清洗后文本。 - segment_text(text)实现:
加载自定义停用词表(从样例数据统计论文领域高频无意义词,如“摘要”“关键词”“引言”);
调用jieba.lcut(text, cut_all=False)(精确模式)分词,返回词语列表;
过滤列表中的停用词和空字符串(如“的”“是”),返回核心token列表。 - preprocess(text)实现:
串联调用clean_text(text)→segment_text(text);
将token列表用空格连接为字符串(如["今天", "天气"]→"今天 天气"),适配TF-IDF向量器输入格式。
步骤3:相似度计算模块(similarity.py)
核心需求:将预处理后的文本转换为数学向量,计算相似度得分。
- tfidf_vectorize(texts)实现:
输入为列表[orig_token_str, copy_token_str](原文与抄袭版的token字符串);
初始化TfidfVectorizer(max_features=10000, min_df=1):限制最大特征数为10000(保留高频词),过滤出现次数<1的低频词;
调用fit_transform(texts)生成TF-IDF向量矩阵(2行10000列,每行对应一篇文本的向量)。 - calculate_similarity(orig_token_str, copy_token_str)实现:
若两篇文本预处理后均为空(如均为标点符号),返回1.0(完全重复);
若一篇为空,返回0.0(无重复);
调用tfidf_vectorize([orig_token_str, copy_token_str])生成向量矩阵;
计算向量矩阵的余弦相似度(调用sklearn.metrics.pairwise.cosine_similarity),返回0~1之间的得分。
步骤4:主流程模块(main.py)
核心需求:解析命令行参数,串联各模块执行流程,处理异常并输出结果。
- main()实现:
解析命令行参数(sys.argv),检查参数数量是否为4(程序名+3个文件路径),否则抛出参数错误异常;
调用file_utils.read_file(orig_path)和read_file(copy_path)读取原文与抄袭版文本;
调用preprocess.preprocess()分别预处理两篇文本,得到token字符串;
调用similarity.calculate_similarity()计算得分;
调用file_utils.write_result(result_path, score)写入结果;
捕获所有异常(如文件不存在、编码错误),输出友好提示并退出(sys.exit(1))。
五、函数关系与调用流程
1 各模块通过函数输入输出实现接口交互,核心调用流程如下:
模块内函数协作:
- preprocess.py:preprocess(text)依赖clean_text(text)和segment_text(text),形成“清洗→分词→过滤”的流水线;
- similarity.py:calculate_similarity()依赖tfidf_vectorize(texts),先向量化再计算相似度;
- main.py:作为入口,仅依赖其他模块的顶层函数(如read_file而非clean_text),降低耦合。
2 关键函数流程图
2.1 preprocess(text)函数流程

2.2 main()函数流程

3 算法关键与独到之处
3.1 TF-IDF+余弦相似度原理
TF-IDF:词频(TF)× 逆文档频率(IDF),降低通用词权重(如“今天”),提升稀有词权重(如“电影”)。
余弦相似度:通过向量夹角余弦值衡量相似度,公式:
3.2 独到优化
使用哈工大停用词表:结合论文领域特点,使用最常用的停用词表;
分块处理超大文件:对>100MB文件,按段落分块读取预处理,避免一次性加载占用内存>2GB;
分词缓存:对测试用例中重复的原文,缓存分词结果(使用functools.lru_cache),减少重复计算。
六、计算模块接口部分的性能改进
1 工具准备与环境配置
| 工具用途 | 工具名称 | 安装命令 | 特点 |
|---|---|---|---|
| 函数耗时分析 | cProfile | Python自带 | 精确统计函数调用耗时、调用次数 |
| 内存占用分析 | memory-profiler | pip install memory-profiler | 逐行分析内存使用,定位内存泄漏 |
| 可视化耗时火焰图 | py-spy | pip install py-spy | 生成SVG火焰图,直观展示函数调用链耗时 |
2 第一步:用 cProfile 定位耗时瓶颈(函数级分析)
核心目标:找出程序中 耗时最长的函数,替代手动计时的粗糙分析。
2.1 基础使用:生成性能报告
操作步骤:
- 在命令行中运行程序,并使用 cProfile 分析:
![image]()
- 打开 profile_report.txt,重点关注 前5行函数(累计耗时最长):
![image]()
累计耗时最高的用户自定义函数是preprocess.py:34(segment_text)(0.547秒),其核心耗时来自调用jieba.lcut进行分词。因此,当前程序的性能瓶颈是文本预处理阶段的分词步骤,具体表现为用户自定义的segment_text函数耗时过高。
3 第二步:segment_text函数拆解与计时逻辑添加
目标:通过手动添加时间戳,分离“分词(jieba.lcut)”和“停用词过滤”的耗时占比。
- 步骤1:修改preprocess.py,添加内部计时
在segment_text函数中,对“分词”和“过滤停用词”两个核心步骤分别计时,代码如下:
![image]()
- 步骤2:运行程序,收集耗时数据
在命令行中运行程序,得到结果:
![image]()
- 第一次调用(含jieba初始化开销)
总耗时:0.4696s
分词耗时:0.4696s(占比100%)
过滤耗时:0.0000s(占比0%)
关键说明:首次调用时,jieba需加载词典和模型(控制台输出Building prefix dict... Loading model cost 0.440 seconds),初始化耗时(0.440s)被计入分词耗时,导致分词占比100%,属于一次性启动开销,非常态性能瓶颈。 - 第二次调用(常态情况,无初始化开销)
总耗时:0.0493s
分词耗时:0.0483s(占比97.92%)
过滤耗时:0.0010s(占比2.08%)
关键说明:模型已加载到内存,分词耗时为纯算法耗时(无初始化),分词占比近98%,过滤占比极低(2.08%),是常态下的真实耗时分布。
4 第三步:基于分词步骤的优化方案
目标:通过加载论文领域自定义词典,减少jieba.lcut对专业术语的拆分歧义(如避免“余弦相似度”拆分为“余弦/相似度”),降低分词算法(DAG图构建、动态规划)的计算量,从而优化segment_text函数的核心瓶颈——分词步骤耗时,同时提升论文场景分词精度。
- 步骤1:创建论文领域自定义词典文件
在程序根目录下新建文本文件,命名为paper_dict.txt,按“词语 词频 词性”格式(词频和词性可选,核心为“词语”)写入论文高频术语,覆盖查重场景常见词汇。词典内容示例: - 步骤2:修改preprocess.py,加载自定义词典
在preprocess.py中,于jieba.lcut调用前(模块加载阶段)添加加载自定义词典的代码,确保分词时优先使用论文术语。
在命令行中运行程序,得到结果:
![image]()
- 分词耗时与总耗时优化效果
分词耗时下降8.28%:
优化前常态分词耗时0.0483s(第二次调用,无初始化),优化后降至0.0443s,减少0.004s。核心原因是加载自定义词典后,论文术语(如“论文查重”“余弦相似度”)被正确分词,减少了jieba.lcut底层DAG图构建和动态规划的计算量(避免术语拆分歧义导致的重复计算)。
总耗时下降8.11%:
优化前总耗时0.0493s,优化后降至0.0453s,减少0.004s,与分词耗时下降幅度基本一致,说明总耗时优化完全来自分词步骤的提升,验证了“分词是核心瓶颈”的历史判断。 - 分词耗时占比稳定,过滤耗时可忽略
分词占比维持高位(97.78%):
优化后分词耗时占比仅比优化前(97.92%)下降0.14%,说明分词仍是segment_text函数的绝对核心耗时步骤,过滤耗时占比(2.22%)极低且稳定,进一步验证了“停用词过滤不是性能瓶颈”的结论。
过滤耗时稳定(0.0010s):
两次调用过滤耗时均为0.0010s,说明STOPWORDS集合查找效率(O(1))未受优化影响,保持高效。 - 首次调用耗时异常降低的原因
现象:优化后首次调用总耗时0.0291s(分词耗时0.0281s),显著低于第二次调用(0.0453s)。
解释:结合优化目标(“预加载jieba模型,分离初始化开销”),首次调用前的“Loading model cost 0.471 seconds”为模型初始化耗时(一次性),已被剥离到程序启动阶段,因此首次segment_text耗时为纯分词耗时,且可能因orig.txt文本长度短于orig_0.8_add.txt(第二次调用文件),导致分词耗时更低,符合“初始化开销不影响业务逻辑”的优化目标。
5 综合以上改进,我们得到最终的性能分析图如下:


七、计算模块部分单元测试展示
1 测试目标
基于测试方法,覆盖核心模块(preprocess.py、similarity.py)的所有逻辑分支与边界条件,验证文本清洗、分词、相似度计算的正确性,确保程序在各种输入下的健壮性。
2 测试用例设计




3 最终覆盖率如下

八、计算模块异常处理说明
1 preprocess.py 中自定义词典加载异常
当论文领域自定义词典 paper_dict.txt 不存在时,程序不崩溃,自动降级使用jieba默认词典,并通过警告提示用户,确保分词功能基础可用。

2 preprocess.py 中停用词表加载异常
当停用词表 stopwords.txt 不存在时,使用内置默认停用词集合,避免因文件缺失导致分词时无法过滤通用无意义词汇。

3 similarity.py 中空文本输入的逻辑处理
通过逻辑判断处理空文本边界情况,避免因输入文本为空导致TF-IDF向量化失败。

九、最终查重结果及部分说明

存放地点:

项目目录说明:
3123004804/ # 项目根目录
├─ docs/ # 文档目录
├─ result/ # 结果输出目录(存放各类生成结果文件)
│ ├─ ans_add.txt # 查重结果文件
│ ├─ ans_del.txt # 查重结果文件
│ ├─ ans_dis_1.txt # 查重结果文件
│ ├─ ans_dis_10.txt # 查重结果文件
│ ├─ ans_dis_15.txt # 查重结果文件
│ └─ profile_report.txt # 性能概要报告文件
├─ src/ # 源代码目录
│ ├─ file_utils.py # 文件工具模块(处理文件读写、路径等操作)
│ ├─ preprocess.py # 数据预处理模块(对数据进行清洗、转换等预处理操作)
│ └─ similarity.py # 相似度计算模块(用于计算数据间的相似度)
├─ tests/ # 测试目录
│ ├─ orig.txt # 原始测试数据文件
│ ├─ orig_0.8_add.txt #
│ ├─ orig_0.8_del.txt #
│ ├─ orig_0.8_dis_1.txt #
│ ├─ orig_0.8_dis_10.txt #
│ ├─ orig_0.8_dis_15.txt #
│ ├─ test_calculate.py # 计算相关测试
│ └─ .coverage # 测试覆盖率文件(记录代码测试覆盖情况)
├─ main.py # 项目主程序入口文件
├─ paper_dict.txt # 论文相关字典/术语文件
└─ requirements.txt # 项目依赖包列表文件(记录所需Python库及版本)






浙公网安备 33010602011771号