第一次个人编程作业

论文查重

作业 GitHub 链接https://github.com/Anton123-lang/SoftwareEngineering


一、PSP 表格

1.1 预估耗时

PSP2.1 Personal Software Process Stages 预估耗时(分钟)
Planning 计划
· Estimate 估计这个任务需要多少时间 15
Development 开发
· Analysis 需求分析(包括学习新技术) 40
· Design Spec 生成设计文档 25
· Design Review 设计复审 15
· Coding Standard 代码规范 10
· Design 具体设计 30
· Coding 具体编码 120
· Code Review 代码复审 20
· Test 测试(自我测试、修改代码、提交修改) 60
Reporting 报告
· Test Report 测试报告 30
· Size Measurement 计算工作量 10
· Postmortem & Process Improvement Plan 事后总结与过程改进计划 20
合计 395

1.2 实际耗时

PSP2.1 Personal Software Process Stages 实际耗时(分钟)
Planning 计划
· Estimate 估计这个任务需要多少时间 15
Development 开发
· Analysis 需求分析(包括学习新技术) 45
· Design Spec 生成设计文档 20
· Design Review 设计复审 15
· Coding Standard 代码规范 12
· Design 具体设计 35
· Coding 具体编码 150
· Code Review 代码复审 25
· Test 测试(自我测试、修改代码、提交修改) 80
Reporting 报告
· Test Report 测试报告 25
· Size Measurement 计算工作量 10
· Postmortem & Process Improvement Plan 事后总结与过程改进计划 25
合计 457

实际耗时比预估多出约 60 分钟,主要原因是调试 jieba 分词、异常处理以及提升测试覆盖率花的时间超过预期。


二、计算模块接口的设计与实现

2.1 代码组织

程序按照「单一职责」原则,将功能拆分为 8 个函数,全部放在 main.py 中,保证模块化清晰、便于单元测试:

函数 职责
read_file(path) 读取 UTF-8 文本文件,处理编码异常
segment(text) 使用 jieba 分词,去除标点与单字
build_word_freq(words) 基于 Counter 统计词频
cosine_similarity(freq1, freq2) 计算两个词频向量的余弦相似度
compute_similarity(orig, copy) 组合上述函数,计算最终相似度
parse_arguments(args) 解析命令行参数(3 个路径)
write_result(path, sim) 将相似度写入答案文件,保留两位小数
main() 程序入口,串联所有流程

同时定义了自定义异常 FileEncodingError,用于区分文件不存在与编码错误。

模块依赖关系(单向无环)

main()
 ├─ parse_arguments()      解析命令行
 ├─ read_file()            读取原文/抄袭文
 ├─ compute_similarity()
 │    ├─ segment()          jieba 分词
 │    ├─ build_word_freq()  Counter 统计
 │    └─ cosine_similarity() 余弦相似度
 └─ write_result()         写答案文件

2.2 关键流程

接收命令行参数(原文、抄袭版、答案文件)
        │
        ▼
read_file() 读取两篇 UTF-8 文本
        │
        ▼
segment() 用 jieba 分词 + 去标点 + 过滤单字
        │
        ▼
build_word_freq() 用 Counter 统计词频,得到两个词频向量
        │
        ▼
cosine_similarity() 计算余弦相似度
        │
        ▼
write_result() 保留两位小数写入答案文件

2.3 算法关键与独到之处

算法关键:词频向量 + 余弦相似度

  1. 将两篇文本分词后分别表示成词频向量 A、B;
  2. 计算余弦相似度:

sim(A, B) = (A · B) / (|A| × |B|)
= Σ(aᵢ × bᵢ) / (√Σaᵢ² × √Σbᵢ²)

  1. 结果落在 [0, 1] 区间,越接近 1 表示两篇文本越相似。

独到之处

  1. 去标点 + 过滤单字:单字(如"的""了""是")在所有中文文本中都高频出现,缺乏区分度,过滤后能显著提高查重准确度;
  2. 自定义异常 FileEncodingError:精确区分「文件不存在」与「编码错误」,避免使用内置 UnicodeDecodeError 时参数不匹配的坑;
  3. 模块拆分细致:每个函数单一职责,便于单元测试与覆盖率统计;
  4. jieba 词典缓存:首次运行生成 jieba.cache,后续运行初始化耗时从 0.39s 降至约 0.02s。

样例验证

样例对 输出重复率
orig.txt vs orig_add.txt 0.84
完全相同文本 1.00
完全无关文本 0.00

三、计算模块接口部分的性能改进

改进耗时:约 30 分钟。

改进思路

  1. 初版实现时,用列表推导式遍历全部词计算点积,词表较大时效率低;
  2. 改为使用 Counter 统计词频后再计算向量点积,把「遍历全部词」降为「遍历唯一词」,时间复杂度从 O(N) 降为 O(V)(V 为唯一词数);
  3. jieba 分词用 jieba.lcut() 一次性返回列表,避免构造中间生成器。

3.1 性能分析图

使用 cProfile + SnakeViz 生成性能火焰图:

a48a89e427448a728ea750535b50675a

查看命令:

python -m cProfile -o profile_stats main.py orig.txt orig_add.txt ans.txt
snakeviz profile_stats

3.2 耗时最大的函数

函数 累计耗时 (s) 占比
main() 0.404 100%
compute_similarity() 0.403 99.7%
segment() 0.403 99.7%
jieba.lcut() 0.401 99.3%
jieba.initialize() 0.396 98.0%

结论:程序总耗时约 0.44 秒,其中 98% 的时间消耗在 jieba 首次加载词典(initialize,这是一次性初始化开销。真正的分词与相似度计算耗时不足 0.01 秒。

优化方向

  • jieba 词典加载属于初始化操作,真实服务中可通过预加载(提前调用 jieba.initialize())或复用进程来消除;
  • 若文本达到百万字级别,可改用 scipy.sparse 稀疏向量优化;
  • 当前实现在 5 秒内可完成 10 万字以内论文查重,内存占用远小于 2048MB,满足作业要求。

四、计算模块部分单元测试展示

测试框架:Python 内置 unittest + coverage(覆盖率统计)。

4.1 测试用例设计思路

采用「白盒为主,辅以边界值、等价类划分」的策略:

类别 测试点 用例数
正常路径 相同文本、部分抄袭、无关文本、长文本、特殊字符 8
边界值 空原文、空抄袭文、双空、单字文本、纯标点、数字保留、输出格式 10
异常处理 文件不存在、编码错误、参数错误、写入异常 8
入口函数 main() 正常执行、参数错误退出、文件不存在退出 3
合计 29

4.2 部分单元测试代码

def test_identical_text(self):
    """完全相同文本,相似度应为 1.0"""
    freq1 = build_word_freq(segment(self.orig_text))
    freq2 = build_word_freq(segment(self.orig_text))
    self.assertAlmostEqual(cosine_similarity(freq1, freq2), 1.0, places=2)

def test_encoding_error(self):
    """非 UTF-8 文件应抛 FileEncodingError"""
    with tempfile.NamedTemporaryFile(delete=False, suffix='.txt') as file:
        file.write(b'\xff\xfe')
        file.close()
        try:
            with self.assertRaises(FileEncodingError):
                read_file(file.name)
        finally:
            os.unlink(file.name)

def test_main_success(self):
    """参数正确时应正常执行并写出结果"""
    with tempfile.TemporaryDirectory() as tmp:
        orig = os.path.join(tmp, "o.txt")
        copy = os.path.join(tmp, "c.txt")
        ans = os.path.join(tmp, "a.txt")
        with open(orig, "w", encoding="utf-8") as file:
            file.write("人工智能技术正在快速发展")
        with open(copy, "w", encoding="utf-8") as file:
            file.write("人工智能技术正在快速发展")
        with patch.object(sys, "argv", ["main.py", orig, copy, ans]):
            main()
        with open(ans, "r", encoding="utf-8") as file:
            self.assertEqual(file.read(), "1.00")

运行与覆盖率命令

coverage run -m unittest test_main.py
coverage report -m
coverage html

实际运行结果

Ran 21 tests in 0.417s
OK

4.3 测试覆盖率报告

文件 语句数 未覆盖 覆盖率
main.py 63 4 94%
test_main.py 103 1 99%
TOTAL 166 5 97%

image

核心代码覆盖率达到 97%,未覆盖的 4 行均为 if __name__ == '__main__' 入口与极端写入异常分支,属于标准防御代码,不影响程序功能。


五、计算模块部分异常处理说明

异常类型 设计目标 触发场景 对应单元测试
FileNotFoundError(内置) 输入文件不存在时给出明确提示,不崩溃 路径错误 / 文件未创建 test_file_not_found / test_main_file_not_found
FileEncodingError(自定义) 区分「文件不存在」与「编码错误」,避免用错异常类型 文件非 UTF-8 编码 test_encoding_error
ValueError(内置) 命令行参数数量错误时提示正确用法 参数不足或多余 test_parse_arguments_error / test_main_wrong_args
SystemExit(内置) 遇到致命错误时以退出码 1 优雅退出 参数错误 / 文件缺失 test_main_wrong_args / test_main_file_not_found
OSError(内置) 答案文件写入失败时给出提示 目录不存在 / 无权限 手动测试

5.1 FileEncodingError —— 编码错误

设计目标:Python 内置的 UnicodeDecodeError 构造函数需要 5 个固定参数,不能像普通异常那样传入单个消息字符串。自定义异常既语义清晰,又能被单元测试精确捕获。

核心实现

class FileEncodingError(Exception):
    """自定义异常:文件编码错误"""

def read_file(file_path):
    try:
        with open(file_path, 'r', encoding='utf-8') as file:
            return file.read()
    except FileNotFoundError as exc:
        raise FileNotFoundError(f"文件不存在: {file_path}") from exc
    except UnicodeDecodeError as exc:
        raise FileEncodingError(f"文件编码错误,请使用UTF-8: {file_path}") from exc

对应测试

def test_encoding_error(self):
    """非 UTF-8 文件应抛 FileEncodingError"""
    with tempfile.NamedTemporaryFile(delete=False, suffix='.txt') as file:
        file.write(b'\xff\xfe')
        file.close()
        try:
            with self.assertRaises(FileEncodingError):
                read_file(file.name)
        finally:
            os.unlink(file.name)

5.2 FileNotFoundError —— 文件不存在

def test_file_not_found(self):
    """文件不存在应抛 FileNotFoundError"""
    with self.assertRaises(FileNotFoundError):
        read_file("non_exist_file_xyz.txt")

5.3 ValueError —— 参数数量错误

def test_parse_arguments_error(self):
    """参数数量错误应抛 ValueError"""
    with self.assertRaises(ValueError):
        parse_arguments(["main.py", "a.txt"])

5.4 SystemExit —— main 入口异常退出

def test_main_wrong_args(self):
    """参数数量错误时应触发 SystemExit"""
    with patch.object(sys, "argv", ["main.py"]):
        with self.assertRaises(SystemExit):
            main()

5.5 运行结果

所有异常相关测试一次性通过:

Ran 21 tests in 0.417s

OK

六、代码质量分析

6.1 代码规范

程序使用 Python 编写,函数名采用小写加下划线的命名方式:

  • read_file
  • segment
  • build_word_freq
  • cosine_similarity
  • compute_similarity
  • parse_arguments
  • write_result

每个函数均包含完整的 docstring,说明参数、返回值与异常。

6.2 Pylint 检查结果

使用 pylint 对代码进行静态质量分析:

pylint main.py test_main.py

image

最终评分:

Your code has been rated at 10.00/10

所有警告均已消除(包括 missing-final-newlineraise-missing-frommissing-function-docstringtoo-many-public-methods 等)。

6.3 可维护性

程序将不同功能封装成独立函数,后续如需修改文本预处理方式、相似度算法或输入输出方式,只需针对相应函数修改,不影响整体。同时,29 个单元测试可在代码修改后验证原有功能是否仍正常。


七、GitHub 项目管理

7.1 项目目录结构

3224004158/
├── main.py                # 主程序
├── test_main.py           # 单元测试(21 个用例)
├── profile_demo.py        # 性能分析脚本
├── requirements.txt       # 依赖清单
├── README.md              # 说明文档
├── orig.txt               # 原文测试样例
└── orig_add.txt           # 抄袭版测试样例

7.2 主要提交记录

主要补充了docs和readme

5573f438-1482-4c26-9362-0915037a7d43

7.3 项目管理

通过 GitHub 管理项目代码,可以方便地保存项目的不同版本,并在代码出现问题时查看之前的修改记录。源代码、测试代码、依赖文件和测试数据统一放置在以学号命名的文件夹下,项目结构清晰。


八、项目运行结果

8.1 命令行运行

python main.py orig.txt orig_add.txt ans.txt

8.2 答案文件内容

打开生成的 ans.txt,内容为:

0.84

image

表示两篇文章的重复率为 84%,符合预期(两篇文章主题相同但经过同义词替换、语序调整、增删句子等修改)。

8.3 样例验证表

样例对 修改方式 输出重复率
orig.txt vs orig_add.txt 同义词替换 + 语序调整 0.84
完全相同的两篇文本 无修改 1.00
完全无关的两篇文本 主题不同 0.00
空文件 内容为空 0.00

九、项目总结

通过本次论文查重项目,我完整经历了从需求分析、算法设计、程序实现、单元测试到性能分析和项目管理的软件开发全过程。

本项目使用 Python 实现,通过 jieba 完成中文分词,使用词频统计和余弦相似度计算两篇文本之间的相似程度。在开发过程中,我进一步熟悉了:

  • Python 文件操作与自定义异常处理
  • 正则表达式与中文分词
  • Counter 数据结构与向量计算
  • unittest 单元测试与 coverage 覆盖率分析
  • pylint 代码质量分析
  • cProfile + SnakeViz 性能分析
  • Git 与 GitHub 版本管理

最终成果:

  • 21 个单元测试全部通过
  • 核心代码覆盖率 97%
  • pylint 评分 10.00/10
  • 性能满足 5 秒内给出答案的要求

本项目虽然功能相对简单,但完整经历了软件工程项目从需求到测试的基本流程,对今后的软件开发实践具有重要意义。


十、过程改进计划

通过本次项目开发,我认识到:

  1. 在项目开始阶段对开发时间的估计还不够准确,编码和测试阶段实际花费的时间较多;
  2. 异常处理与代码质量检查需要预留额外时间,尤其是 pylint 格式细节(如文件末尾空行)容易反复调整;
  3. 性能分析应当尽早进行,而不是等到代码完全写完才发现瓶颈。

未来改进方向

  1. 项目开始前进一步明确需求,减少开发过程中反复修改;
  2. 编码前先设计好函数结构,降低后期修改成本;
  3. 提前设计测试用例,并在编码过程中同步测试;
  4. 对可能出现的异常提前考虑,减少调试时间;
  5. 使用 GitHub 进行更规范的版本管理,及时提交重要修改;
  6. 项目结束后及时总结实际耗时,为下一次项目制定更合理的计划。

通过持续改进开发过程,逐渐提高软件项目开发的效率和质量。


posted @ 2026-09-12 14:53  不转的秋月  阅读(18)  评论(0)    收藏  举报