第一次个人编程作业
第一次个人编程作业
| 这个作业属于哪个课程 | https://edu.cnblogs.com/campus/gdgy/Class78-Grade2024-CS |
|---|---|
| 这个作业要求在哪里 | https://edu.cnblogs.com/campus/gdgy/Class78-Grade2024-CS/homework/15702 |
| 这个作业的目标 | 实现一个基于命令行文件输入输出的论文查重程序,准确计算原文与改写文本的重复率。 |
项目仓库:3124004242/Plagiarism_Check at main · Masterfred06/3124004242
学号:3124004242
开发语言:Python3
项目版本:v1.0
一、项目简介
本次作业题目为“论文查重”。程序需要从命令行接收三个参数:原文文件绝对路径、抄袭版论文文件绝对路径、答案文件绝对路径。程序读取前两个文件后计算重复率,并将结果以浮点数形式写入答案文件,精确到小数点后两位。
我使用 Python 3 完成该项目。项目入口文件为 main.py,核心计算模块为 plagiarism_checker.py,测试文件位于 tests/test_plagiarism_checker.py。程序运行时只使用 Python 标准库,不依赖第三方库,因此 requirements.txt 中没有额外运行依赖。
运行方式如下:
python main.py <原文文件绝对路径> <抄袭版论文文件绝对路径> <答案文件绝对路径>
例如:
python main.py D:\tests\orig.txt D:\tests\orig_0.8_add.txt D:\tests\ans.txt
二、PSP 表格
| PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
|---|---|---|---|
| Planning | 计划 | 20 | 18 |
| · Estimate | · 估计这个任务需要多少时间 | 20 | 18 |
| Development | 开发 | 220 | 205 |
| · Analysis | · 需求分析(包括学习新技术) | 30 | 25 |
| · Design Spec | · 生成设计文档 | 25 | 22 |
| · Design Review | · 设计复审 | 15 | 12 |
| · Coding Standard | · 代码规范 | 10 | 8 |
| · Design | · 具体设计 | 30 | 28 |
| · Coding | · 具体编码 | 60 | 58 |
| · Code Review | · 代码复审 | 20 | 18 |
| · Test | · 测试(自我测试,修改代码,提交修改) | 30 | 34 |
| Reporting | 报告 | 75 | 68 |
| · Test Report | · 测试报告 | 25 | 22 |
| · Size Measurement | · 计算工作量 | 15 | 12 |
| · Postmortem & Process Improvement Plan | · 事后总结,并提出过程改进计划 | 35 | 34 |
| 合计 | 合计 | 315 | 291 |
三、计算模块接口的设计与实现过程
3.1 代码组织
项目主要分为入口层、计算层和测试层。
| 文件 | 作用 |
|---|---|
main.py |
命令行入口,接收参数并调用核心模块 |
plagiarism_checker.py |
核心计算模块,负责文件读取、HTML 正文抽取、文本归一化、相似度计算和答案写入 |
tests/test_plagiarism_checker.py |
单元测试文件,覆盖正常输入、异常输入、HTML 输入和命令行接口 |
requirements.txt |
Python 依赖说明,本项目无第三方运行依赖 |
README.md |
项目运行说明和开发说明 |
入口文件 main.py 尽量保持简单,只负责把命令行参数传给核心模块:
import sys
from plagiarism_checker import run_cli
if __name__ == "__main__":
raise SystemExit(run_cli(sys.argv[1:]))
这样的组织方式可以让命令行入口和计算逻辑分离,方便单元测试直接测试核心函数,而不需要每次都启动一个新进程。
3.2 主要接口
核心模块中的主要函数如下:
| 函数 | 功能 |
|---|---|
read_text_file(file_path) |
读取文本文件,兼容 utf-8-sig、utf-8、gb18030 等常见编码 |
looks_like_html(text) |
判断输入内容是否可能是 HTML |
extract_document_text(raw_text) |
从普通文本或 HTML 中抽取论文正文 |
normalize_text(text) |
文本归一化,统一全角半角和大小写,去掉标点、空白等无关字符 |
iter_ngrams(text, size) |
生成字符级 n-gram |
cosine_similarity(first_terms, second_terms) |
计算两个词频向量的余弦相似度 |
calculate_similarity(original_text, suspect_text) |
计算最终重复率 |
check_documents(original_path, suspect_path) |
读取两个文件并返回查重结果 |
write_answer(answer_path, score) |
将重复率按两位小数写入答案文件 |
run_cli(arguments) |
命令行流程控制和异常处理 |
3.3 HTML 文本处理
测试文本中存在 HTML 格式的查重文档,其中真实论文内容被包在 GitHub 页面代码行中。如果直接对整份 HTML 做相似度计算,会把导航栏、CSS、JavaScript、页脚等无关内容也算进去,导致结果失真。
因此我使用 Python 标准库 HTMLParser 实现了 DocumentHTMLParser:
- 遇到普通 HTML 时,提取可见文本。
- 遇到 GitHub blob 页面时,优先提取
id="LC..."或带有blob-code、js-file-line的代码行。 - 跳过
script、style、noscript标签中的内容。
这样可以让程序既支持普通纯文本,也能处理题目备注中提到的 HTML 格式目标文档。
3.4 查重算法
我没有使用深度学习或复杂语义模型,因为题目明确强调时间和空间限制。最终使用的是字符级 n-gram 频次向量加权余弦相似度。
算法流程如下:
文本归一化后,程序分别计算 1-gram、2-gram、3-gram 的余弦相似度,然后按如下权重加权:
NGRAM_WEIGHTS = ((1, 0.35), (2, 0.45), (3, 0.20))
选择混合 n-gram 的原因是:
- 1-gram 对插入、删除、换序更稳定,但区分度较低。
- 2-gram 能较好反映局部连续文本。
- 3-gram 对原文顺序更敏感,但在大量插字时会受到较大影响。
因此混合三者比单一 n-gram 更稳健。对于中文论文查重,这种方法不需要分词,也避免了分词词典差异带来的不稳定性。
3.5 核心代码片段
下面是最终相似度计算函数:
def calculate_similarity(original_text: str, suspect_text: str) -> float:
original = normalize_text(original_text)
suspect = normalize_text(suspect_text)
if not original and not suspect:
return 1.0
if not original or not suspect:
return 0.0
weighted_score = 0.0
for ngram_size, weight in NGRAM_WEIGHTS:
original_terms = Counter(iter_ngrams(original, ngram_size))
suspect_terms = Counter(iter_ngrams(suspect, ngram_size))
weighted_score += weight * cosine_similarity(original_terms, suspect_terms)
return min(max(weighted_score, 0.0), 1.0)
四、计算模块接口部分的性能改进
4.1 初始方案
最初考虑过两种方案:
- 使用编辑距离或最长公共子序列。
- 使用字符 n-gram 向量相似度。
编辑距离和最长公共子序列在短文本上效果直观,但对于较长论文文本,动态规划的时间和空间开销较大,不适合题目中 5 秒内给出答案的要求。因此我选择了 n-gram 词频向量方案。
4.2 性能瓶颈分析
我使用 Python 标准库 cProfile 进行性能分析:
python -m cProfile -s cumulative main.py 测试文本\orig.txt 测试文本\orig_0.8_add.txt ans.txt
一次性能分析结果如下:
179022 function calls in 0.111 seconds
Ordered by: cumulative time
ncalls tottime cumtime function
1 0.000 0.063 calculate_similarity
6 0.013 0.027 _collections._count_elements
2 0.000 0.018 normalize_text
3 0.000 0.017 cosine_similarity
56247 0.014 0.014 iter_ngrams
性能分析显示,耗时最大的部分是:
calculate_similarity:整体计算入口。Counter统计 n-gram 频次。normalize_text:遍历全文做 Unicode 归一化和字符过滤。cosine_similarity:计算向量点积和模长。
4.3 性能改进
根据性能分析结果,我做了以下改进:
- 使用
Counter统计 n-gram,避免手写双重循环比较文本片段。 - 只遍历文本生成 1/2/3-gram,不构造复杂中间对象。
- HTML 文档优先抽取正文,减少无关网页文本进入后续计算。
- 使用标准库,避免第三方分词库的加载开销。
- 对空文本做提前返回,避免无意义计算。
改进后,样例文本在本机上运行耗时约 0.1s,满足题目中 5 秒内给出答案的要求。
运行情况如下:

五、计算模块部分单元测试展示
5.1 测试方法
本项目使用 Python 标准库 unittest 编写单元测试。运行命令如下:
python -m unittest discover -s tests
测试结果:
Ran 15 tests in 0.125s
OK
5.2 测试用例设计
我设计的测试用例覆盖了以下场景:
| 编号 | 测试内容 | 目的 |
|---|---|---|
| 1 | 完全相同文本 | 检查重复率是否为 1 |
| 2 | 两个空文档 | 检查边界条件 |
| 3 | 一个空文档 | 检查空输入处理 |
| 4 | 标点、空格、全角字符归一化 | 检查文本预处理 |
| 5 | 题目示例中的局部改写 | 检查改写文本相似度 |
| 6 | 无关文本 | 检查低相似度判断 |
| 7 | 普通 HTML 正文抽取 | 检查 HTML 处理 |
| 8 | GitHub blob HTML 代码行抽取 | 检查样例 HTML 文件适配 |
| 9 | UTF-8 文件读取 | 检查文件读取 |
| 10 | 缺失文件异常 | 检查异常处理 |
| 11 | 答案文件两位小数输出 | 检查输出格式 |
| 12 | CLI 正常运行 | 检查命令行入口 |
| 13 | CLI 参数数量错误 | 检查参数校验 |
| 14 | 样例 orig_0.8_add.txt |
检查增添文本样例 |
| 15 | 样例 orig_0.8_dis_1.txt |
检查 HTML 样例 |
5.3 部分测试代码
下面展示几个有代表性的测试:
def test_identical_text_has_full_similarity(self) -> None:
self.assertAlmostEqual(calculate_similarity("今天天气很好", "今天天气很好"), 1.0)
def test_one_empty_document_has_zero_similarity(self) -> None:
self.assertAlmostEqual(calculate_similarity("今天天气很好", ""), 0.0)
def test_modified_example_keeps_medium_similarity(self) -> None:
original = "今天是星期天,天气晴,今天晚上我要去看电影。"
suspect = "今天是周天,天气晴朗,我晚上要去看电影。"
self.assertGreater(calculate_similarity(original, suspect), 0.60)
def test_github_blob_html_prefers_code_lines(self) -> None:
html = (
"<html><body><nav>GitHub</nav><table>"
'<td id="LC1" class="blob-code js-file-line">第一行</td>'
'<td id="LC2" class="blob-code js-file-line">第二行</td>'
"</table></body></html>"
)
self.assertEqual(extract_document_text(html).strip(), "第一行\n第二行")
5.4 覆盖率
覆盖率可用以下命令查看:
python -m coverage run -m unittest discover -s tests
python -m coverage report

如果本机没有安装 coverage,可以先安装:
pip install coverage
六、计算模块部分异常处理说明
6.1 参数数量错误
设计目标:评测系统会按固定格式传入三个参数。如果参数数量不正确,程序不应继续运行,而应提示正确用法并返回错误码。
处理方式:
if len(arguments) != 3:
print(
"Usage: python main.py <original_file> <suspect_file> <answer_file>",
file=sys.stderr,
)
return 2
对应测试:
def test_run_cli_rejects_wrong_argument_count(self) -> None:
with redirect_stderr(StringIO()):
self.assertEqual(run_cli([]), 2)
6.2 输入文件不存在
设计目标:当传入的原文路径或抄袭版路径不存在时,程序应给出清晰错误,而不是异常退出。
处理方式:
if not path.is_file():
raise DocumentError(f"Input file does not exist: {path}")
对应测试:
def test_read_text_file_reports_missing_file(self) -> None:
with self.assertRaises(DocumentError):
read_text_file("not_exists.txt")
6.3 输入文件无法读取或无法解码
设计目标:不同系统上的中文文本可能使用不同编码。程序依次尝试 utf-8-sig、utf-8、gb18030。如果仍无法读取,统一转换为 DocumentError,由命令行入口捕获并返回错误码。
处理方式:
for encoding in ENCODINGS:
try:
return path.read_text(encoding=encoding)
except UnicodeDecodeError as exc:
last_error = exc
except OSError as exc:
raise DocumentError(f"Cannot read input file: {path}") from exc
6.4 答案文件无法写入
设计目标:如果答案路径所在目录不存在,或没有权限写入,程序应输出错误信息并返回错误码。
处理方式:
try:
path.write_text(f"{score:.2f}", encoding="utf-8")
except OSError as exc:
raise DocumentError(f"Cannot write answer file: {path}") from exc
对应测试:
def test_write_answer_uses_two_decimal_places(self) -> None:
with tempfile.TemporaryDirectory() as temp_dir:
answer = Path(temp_dir) / "answer.txt"
write_answer(str(answer), 0.836)
self.assertEqual(answer.read_text(encoding="utf-8"), "0.84")
6.5 空文本处理
设计目标:空文本是边界情况。两个空文本可以认为完全相同;如果只有一个为空,则重复率应为 0。
处理方式:
if not original and not suspect:
return 1.0
if not original or not suspect:
return 0.0
对应测试:
def test_empty_documents_are_equal(self) -> None:
self.assertAlmostEqual(calculate_similarity("", ""), 1.0)
def test_one_empty_document_has_zero_similarity(self) -> None:
self.assertAlmostEqual(calculate_similarity("今天天气很好", ""), 0.0)
七、样例运行结果
在项目已有测试文本上运行,结果如下:
| 查重文件 | 输出重复率 |
|---|---|
orig_0.8_add.txt |
0.88 |
orig_0.8_del.txt |
0.88 |
orig_0.8_dis_1.txt |
0.97 |
orig_0.8_dis_10.txt |
0.90 |
orig_0.8_dis_15.txt |
0.76 |
其中 orig_0.8_dis_1.txt 等文件是 HTML 格式,程序会自动提取其中的正文代码行再参与计算。
八、代码质量分析
本项目从以下几个方面保证代码质量:
- 函数职责单一,入口、计算、文件读写分离。
- 核心函数均添加类型标注。
- 对文件读取、文件写入、参数数量等异常情况做了统一处理。
- 不尝试连接网络。
- 不读取原文、抄袭版文件以外的其他业务文件。
- 不写入答案路径以外的业务文件。
- 使用
unittest编写自动化测试。
基础检查命令如下:
python -m compileall .
python -m unittest discover -s tests
检查结果:
compileall 通过
Ran 15 tests in 0.125s
OK
九、总结与改进方向
本项目实现了一个轻量级论文查重程序。程序使用字符级 n-gram 频次余弦相似度,能够处理普通文本、HTML 文本、插入、删除和局部改写等情况。相比使用复杂语义模型,该方法更符合题目的时间和空间限制。
本次开发中比较关键的点是 HTML 正文抽取。如果直接把 HTML 页面整体参与计算,结果会受到网页结构和脚本内容干扰。通过识别 GitHub blob 的代码行,可以让程序更准确地提取真正论文内容。
后续可改进方向:
- 根据更多样例微调 1/2/3-gram 权重。
- 增加更多编码格式兼容。
- 在保证性能的前提下加入简单的句子级相似度辅助判断。
浙公网安备 33010602011771号