第二周作业——论文查重

这个作业属于哪个课程 计科24级56班
这个作业要求在哪里 个人工程
这个作业的目标 完成论文查重的程序设计

Github链接:https://github.com/Philzair/Philzair/tree/main/3124004089

一、PSP表格

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

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

1. 代码组织
本项目使用 Python3 实现,未定义类,采用函数式模块组织,所有核心逻辑集中在 main.py 中。这样结构简单、依赖少,便于测试和部署。

主要函数如下:

text
main.py
├── read_text(path) 读取文件
├── preprocess(text) 文本清洗
├── char_ngrams(text, n) 提取字符 n-gram
├── cosine_similarity(c1, c2) 计算余弦相似度
├── calculate_similarity(orig, copy) 核心相似度计算
├── write_answer(ans_path, sim) 写入答案文件
└── main(argv) 命令行入口
2. 函数关系
text
main()
├── 解析命令行参数
├── calculate_similarity(orig_path, copy_path)
│ ├── read_text(orig_path)
│ ├── read_text(copy_path)
│ ├── preprocess(orig_text)
│ ├── preprocess(copy_text)
│ ├── char_ngrams(text1, 1)
│ ├── char_ngrams(text2, 1)
│ ├── char_ngrams(text1, 2)
│ ├── char_ngrams(text2, 2)
│ ├── cosine_similarity(uni1, uni2)
│ └── cosine_similarity(bi1, bi2)
└── write_answer(ans_path, similarity)
3. 关键函数说明
read_text(path):按 UTF-8 读取文件,忽略非法字符,避免编码异常导致程序崩溃。

preprocess(text):去除 HTML 标签、script/style、标点、空白,只保留中文、英文、数字。

char_ngrams(text, n):使用 Counter 统计字符 n-gram 频率。

cosine_similarity(c1, c2):计算两个词频向量的余弦相似度,任一为空则返回 0。

calculate_similarity(orig, copy):核心接口,返回保留两位小数的相似度。

write_answer(ans_path, sim):将结果写入答案文件。

4. 算法关键与独到之处
算法采用 unigram + bigram 混合余弦相似度:

text
similarity = 0.4 * unigram_similarity + 0.6 * bigram_similarity
unigram 反映单字使用频率;

bigram 反映相邻字符顺序;

加权后对增删改、局部乱序有较好鲁棒性;

对 HTML 页面残留有清洗能力;

时间复杂度近似 O(N),满足 5 秒内输出要求。

5. 流程图文字描述
text
开始

读取命令行参数

参数数量是否为 4?
├── 否:输出用法,返回错误码
↓ 是
读取原文和抄袭版文件

文本清洗:去 HTML、标点、空白

提取 unigram 和 bigram

分别计算余弦相似度

加权求和,保留两位小数

写入答案文件

结束

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

1. 性能分析工具
使用 Python 标准库 cProfile 进行性能分析:

bash
python -m cProfile -o profile.out main.py tests/data/orig.txt tests/data/orig_add.txt tests/data/ans.txt
python -m pstats profile.out
再使用 snakeviz 可视化:

bash
pip install snakeviz
snakeviz profile.out
性能分析图保存在:

text
docs/profile.png

2. 性能分析结果

函数 调用次数 总耗时占比 说明
preprocess 2 35% 正则清洗耗时较大
char_ngrams 4 30% n-gram 统计
cosine_similarity 2 15% 向量点积与范数
read_text 2 10% 文件读取
main 1 10% 参数解析与写文件

消耗最大的函数通常是 preprocess() 和 char_ngrams()。

3. 改进思路
预编译正则表达式,避免重复编译;使用 Counter 代替普通字典手动计数;减少重复遍历,尽量一次提取特征;预处理阶段直接删除标点和空白,减少后续 n-gram 数量;对极大文件可分块读取,降低内存峰值。

4. 改进效果
改进后,典型样例运行时间小于 1 秒,内存占用远低于 2048MB。
性能改进耗时约 40 分钟。

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

1. 测试文件
测试文件:tests/test_similarity.py

运行方式:

bash
pytest --cov=. --cov-report=html
覆盖率截图保存在:

text
docs/coverage.png

2. 部分单元测试代码

**点击查看代码**
python
import subprocess
import sys
import pytest
from main import read_text, preprocess, char_ngrams, cosine_similarity, calculate_similarity


def write_file(path, content):
    path.write_text(content, encoding="utf-8")


def test_identical_files(tmp_path):
    """相同文本,相似度应为 1.00。"""
    a = tmp_path / "a.txt"
    b = tmp_path / "b.txt"
    write_file(a, "今天是星期天,天气晴,今天晚上我要去看电影。")
    write_file(b, "今天是星期天,天气晴,今天晚上我要去看电影。")
    assert calculate_similarity(str(a), str(b)) == 1.0


def test_completely_different_files(tmp_path):
    """完全不同文本,相似度应接近 0。"""
    a = tmp_path / "a.txt"
    b = tmp_path / "b.txt"
    write_file(a, "苹果香蕉西瓜")
    write_file(b, "计算机网络操作系统")
    assert calculate_similarity(str(a), str(b)) == 0.0


def test_empty_orig_file(tmp_path):
    """原文为空,相似度为 0。"""
    a = tmp_path / "a.txt"
    b = tmp_path / "b.txt"
    write_file(a, "")
    write_file(b, "今天是星期天")
    assert calculate_similarity(str(a), str(b)) == 0.0


def test_html_clean(tmp_path):
    """HTML 标签应被清洗,不影响正文相似度。"""
    a = tmp_path / "a.txt"
    b = tmp_path / "b.txt"
    write_file(a, "活着前言 一位真正的作家永远只为内心写作")
    write_file(b, "<html><body><p>活着前言</p><p>一位真正的作家永远只为内心写作</p></body></html>")
    assert calculate_similarity(str(a), str(b)) >= 0.9


def test_file_not_found():
    """文件不存在应抛出 FileNotFoundError。"""
    with pytest.raises(FileNotFoundError):
        read_text("not_exist_file_12345.txt")


def test_command_line_normal(tmp_path):
    """命令行正常调用。"""
    orig = tmp_path / "orig.txt"
    copy = tmp_path / "copy.txt"
    ans = tmp_path / "ans.txt"
    write_file(orig, "今天是星期天,天气晴。")
    write_file(copy, "今天是星期天,天气晴。")
    result = subprocess.run(
        [sys.executable, "main.py", str(orig), str(copy), str(ans)],
        capture_output=True,
        text=True,
    )
    assert result.returncode == 0
    assert ans.read_text(encoding="utf-8").strip() == "1.00"

3. 测试用例说明

测试函数 测试目标 构造数据思路
test_identical_files 相同文件相似度 两份完全一致文本
test_completely_different_files 完全不同文本 两组无交集中文词
test_empty_orig_file 原文空 原文空,抄袭非空
test_empty_copy_file 抄袭文空 原文非空,抄袭空
test_add_delete_modify 增删改鲁棒性 样例改写
test_html_clean HTML 清洗 一份纯文本,一份 HTML
test_file_not_found 文件不存在 不存在的路径
test_preprocess_removes_punctuation 预处理 含标点空白
test_char_ngrams_bigram n-gram 短文本
test_cosine_similarity_same 余弦相似度 相同 Counter
test_command_line_normal 命令行正常 subprocess 调用
test_command_line_wrong_args 参数异常 不传参数

这些测试覆盖了正常计算、边界情况、异常处理和命令行调用,核心函数 calculate_similarity、preprocess、cosine_similarity 均被覆盖。

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

1. 异常设计总览

异常类型 设计目标 对应场景 单元测试样例
参数数量错误 提示正确用法,返回非零错误码 python main.py 不传参数 test_command_line_wrong_args
FileNotFoundError 文件不存在时给出明确提示 原文或抄袭版路径错误 test_file_not_found
UnicodeDecodeError 编码异常不导致崩溃 文件不是 UTF-8 test_unicode_error
PermissionError 输出路径无权限时提示 答案文件不可写 test_permission_error
空原文 相似度定义为 0 原文为空文件 test_empty_orig_file
空抄袭文 相似度定义为 0 抄袭版为空文件 test_empty_copy_file
HTML 残留 清洗 HTML,避免误判 输入为 GitHub 页面保存文件 test_html_clean
输出目录不存在 写文件前检查,避免异常退出 答案路径目录不存在 test_output_dir_not_exist

2. 异常处理原则

不崩溃:所有可预期异常由 main() 统一捕获,返回错误码。

信息明确:错误信息写入 stderr,便于定位。

正常输入不受影响:异常处理不影响正常计算流程。

防止异常退出:评测中“发生异常退出”会扣分,因此必须捕获文件、编码、权限等异常。

  1. 部分异常单元测试样例
点击查看代码
python
def test_command_line_wrong_args():
    """参数数量错误时返回非零,但不崩溃。"""
    result = subprocess.run(
        [sys.executable, "main.py"],
        capture_output=True,
        text=True,
    )
    assert result.returncode != 0
    assert "Usage" in result.stderr or "用法" in result.stderr


def test_file_not_found():
    """文件不存在应抛出 FileNotFoundError。"""
    with pytest.raises(FileNotFoundError):
        read_text("not_exist_file_12345.txt")


def test_empty_orig_file(tmp_path):
    """原文为空,相似度为 0。"""
    a = tmp_path / "a.txt"
    b = tmp_path / "b.txt"
    write_file(a, "")
    write_file(b, "今天是星期天")
    assert calculate_similarity(str(a), str(b)) == 0.0

五、总结

本项目完成了一个基于 Python3 的论文查重程序,支持命令行传入原文、抄袭版论文和答案文件路径,输出保留两位小数的相似度。核心算法采用文本清洗、字符 unigram 与 bigram 词频统计、加权余弦相似度计算,能够处理中文增删改、局部乱序以及 HTML 页面残留等情况。

工程实践方面,项目使用 GitHub 管理源代码,按功能模块提交 commit;使用 pytest 编写了超过 10 个单元测试,覆盖正常计算、边界情况、异常处理与命令行调用;使用 pytest-cov 查看测试覆盖率;使用 cProfile 分析性能瓶颈,并通过预编译正则、Counter 统计、减少重复遍历等方式进行优化。

不足之处在于,当前算法对语义级改写、同义词替换的识别能力有限。后续可扩展 TF-IDF、SimHash、词向量或深度学习语义相似度模型,并进一步优化大文件场景下的内存占用与计算速度。

posted @ 2026-09-15 21:20  Philil  阅读(8)  评论(0)    收藏  举报