第一次个人编程作业

第一次个人编程作业

这份作业属于哪个课程 2024级计算机科学78班(Class78-Grade2024-CS)
这个作业要求在哪里 作业要求
这个作业的目标 通过实现一个命令行论文查重程序,完成从需求分析、算法设计、文件输入输出、性能优化到单元测试和工程报告的完整开发流程,掌握 Python 项目组织、GitHub 版本管理和软件质量验证方法。

项目仓库: https://github.com/Huahetai-cell/3224004345

学号: 3224004345

开发语言: Python 3

本次软件工程实践完成了一款命令行论文查重程序。程序读取原文文件和抄袭版文件,在内存中计算字符 n-gram 相似度,并将保留两位小数的重复率写入答案文件。整个开发过程包含需求分析、代码实现、Counter 性能优化、20个自动化测试、覆盖率验证、异常处理和 GitHub 版本管理。

一、PSP 表格(开发前的预估)

本项目按需求分析、接口设计、首版实现、性能改进、测试和报告六个阶段推进。预估值在开始编码前记录,实际值在完成本地验收和资料整理后补充。时间单位为分钟。为避免中文阶段名称被挤成窄列,下面使用宽度为 100% 的表格。

PSP2.1 阶段工作内容预估耗时(分钟)
Planning计划30
Estimate估计任务时间20
Development需求分析、设计、编码、复审和测试500
Reporting测试报告、工作量统计和事后总结140
合计690

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

2.1 需求与对外接口

程序按照作业规定从命令行接收三个文件路径:

python main.py <原文文件> <抄袭版论文文件> <答案文件>

程序读取前两个文件,计算重复率,再把固定两位小数写入第三个文件。例如:

python main.py C:\tests\orig.txt C:\tests\orig_add.txt C:\tests\ans.txt

答案文件输出范围为 0.001.00。成功退出码为 0,参数错误为 2,文件读取、解码或写入错误为 1。

2.2 代码组织:入口、计算、测试和证据四部分

文件或目录 作用 关键内容
main.py 程序入口 调用 main() 并返回进程退出码
paper_checker.py 计算与文件接口 normalize_text()calculate_similarity()run()main()
tests/ 自动化测试 20个 unittest 用例,覆盖正常、边界和异常分支
tools/ 工程辅助 性能证据和老师测试数据批量运行脚本
docs/ 过程材料 设计说明、PSP、测试报告和本文博客草稿

calculate_similarity() 不依赖文件系统,因此可以直接测试算法;run() 只负责读取、计算和写答案;main() 统一处理参数和异常。这种拆分让测试既能验证纯计算,也能验证真实命令行行为。

2.3 算法关键:字符一元组和二元组的多重集 Dice

原始论文和抄袭版可能出现增删改、标点变化、空格变化和局部重排。程序先对文本进行 Unicode NFKC 规范化、英文大小写归一,并移除空白、标点和符号。这样“全角A”和“半角A”、不同排版空格不会被误判成内容差异。

对规范化文本生成字符一元组和相邻字符二元组。两个多重集 A、B 的 Sørensen–Dice 系数为:

Dice(A, B) = 2 × |A ∩ B| / (|A| + |B|)

交集按每个片段出现次数的较小值累计,而不是简单判断“出现过/没出现过”。最终重复率为:

重复率 = 0.2 × 一元组 Dice + 0.8 × 二元组 Dice

一元组反映整体字符重合,二元组更重视相邻语序。文本短于两个字符时退化为一元组比较;两个空文本定义为 1.00,只有一边为空时定义为 0.00

2.4 关键实现:用 Counter 计算多重集交集

初版用列表逐项查找并删除匹配项:

for item in left:
    if item in unmatched:
        unmatched.remove(item)

长文本中 item in listlist.remove() 会反复扫描列表,最坏复杂度接近 O(n²)。最终版本改为:

left_counts = Counter(left)
right_counts = Counter(right)
overlap = sum((left_counts & right_counts).values())

计数器交集保持多重集语义,时间复杂度降为 O(n),空间复杂度为 O(n)。这是本项目性能改进的核心。

2.5 整体流程

flowchart LR A[三个命令行路径] --> B[读取原文和抄袭版] B --> C[NFKC 规范化] C --> D[移除空白标点符号] D --> E[Counter 统计一元组和二元组] E --> F[加权 Dice 重复率] F --> G[格式化两位小数] G --> H[写入答案文件]

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

3.1 两代实现

版本 多重集匹配方式 时间复杂度 结果一致性
第一版 列表查找并删除 最坏 O(n²) 基准版本
最终版 Counter 交集 O(n) 与第一版一致

3.2 实测耗时(Windows / Python 3.13)

在相同的 15,400 字符样本上使用 cProfiletracemalloc

实现 分析耗时 峰值内存 重复率
列表查找/删除 1.109023 秒 1.936 MiB 0.810514
Counter 交集 0.146865 秒 1.842 MiB 0.810514

结果完全一致,分析场景提速约 7.55 倍。对约 1 MiB 文本进行正常运行测试,耗时约 0.890186 秒,诊断峰值内存约 117.150 MiB,满足5秒和2048 MB限制。

3.3 性能分析图与热点函数

性能分析图

优化前的热点是 _dice_similarity() 中的列表查找和 list.remove();优化后热点转移到必要的文本规范化、n-gram 构造和 Counter 建立,说明二次匹配循环已经被消除。原始 cProfile 数据见 reports/evidence/cprofile-before.txtcprofile-after.txt

3.4 改进思路小结

先用基准版本确认功能,再用性能分析定位瓶颈,最后只替换多重集交集实现。每次优化都重新运行同一组测试,确认答案和边界行为不变,避免为了追求速度改变查重口径。

四、计算模块接口部分的单元测试

4.1 测试规模与运行方式

项目使用 Python 标准库 unittest,当前共有20个测试用例,超过需求规定的至少10个:

python -m unittest discover -v

本地实际结果:

Ran 20 tests
OK

测试覆盖规范化、相似度边界、多重集计数、UTF-8 BOM、GB18030、中文路径、命令行参数、文件异常、答案格式和独立进程调用。

4.2 覆盖率报告

python -m coverage run --branch -m unittest discover
python -m coverage report -m

核心模块结果:

Name               Stmts   Miss Branch BrPart  Cover
----------------------------------------------------
paper_checker.py      58      0     14      0   100%
----------------------------------------------------
TOTAL                 58      0     14      0   100%

覆盖率报告

4.3 部分测试代码与构造思路

完全相同、完全不同和文档示例用于验证基本正确性;重复字符用于验证多重集而非普通集合;空文本和单字符用于覆盖退化分支;错误编码、缺失路径和输出目录不存在用于覆盖异常分支。

def test_repeated_fragments_use_multiset_counts(self) -> None:
    self.assertAlmostEqual(calculate_similarity("aaaa", "aa"), 0.5333333333)

def test_output_may_overwrite_an_input_after_both_are_read(self) -> None:
    original, suspect, _ = self._write_inputs()
    run(original, suspect, original)
    self.assertRegex(original.read_text(encoding="utf-8"), r"^\d\.\d{2}\n$")

这两例分别验证计数交集语义和“先读完输入再写输出”的文件安全行为。

4.4 老师数据的补充验收

项目新增了 tools/run_teacher_samples.ps1,可以批量解压并运行老师提供的样例。当前压缩包中的 orig_0.8_add.txt 可正常作为文本读取;另外几个文件开头是 GitHub HTML 页面,不是论文正文,因此验收记录明确标注了数据完整性问题,没有把文件名中的 0.8 擅自当成标准答案。

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

异常类别 处理目标 退出码 对应测试
参数数量不是3个 显示规范用法,不访问文件 2 test_main_rejects_wrong_argument_count
输入路径不存在或不是普通文件 提示具体路径 1 test_main_reports_missing_input_without_traceback
UTF-8、GB18030均无法解码 提示支持的编码 1 test_main_reports_invalid_encoding
输出目录不存在或不可写 捕获系统写入错误 1 test_main_reports_unwritable_output_location
输出路径与输入相同 先完成两次读取再写答案 0 test_output_may_overwrite_an_input_after_both_are_read

预期异常统一转换成单行错误信息,不显示 traceback。程序不连接网络,不扫描指定路径之外的文件,也不使用数据库,符合题目对文件输入输出和评测红线的要求。

六、PSP 表格(实现之后的实际耗时)

PSP2.1 阶段工作内容预估(分钟)实际(分钟)
Planning计划3045
Estimate估计任务时间2030
Development需求分析、设计、编码、复审和测试500620
Reporting测试报告、工作量统计和事后总结140180
合计690875

实际耗时包含需求阅读、编码、性能定位、20个测试用例、覆盖率报告、老师数据验收、README 和博客材料整理。与只运行一次程序相比,这个时间更接近一次完整作业交付的自主开发过程。

七、总结与改进计划

本项目完成了命令行文件输入输出、字符 n-gram 查重、异常处理、自动化测试和性能改进。当前最重要的限制是题目没有公开唯一算法和隐藏测试期望值;如果老师提供干净的原始样例及标准答案,下一步会用这些标注结果校准算法权重,并继续保持测试和性能基准不回退。

posted @ 2026-09-14 02:06  化合态  阅读(23)  评论(0)    收藏  举报