第一次个人编程作业
论文查重项目
这个作业属于哪个课程 | https://edu.cnblogs.com/campus/gdgy/Class34Grade23ComputerScience |
---|---|
这个作业要求在哪里 | https://edu.cnblogs.com/campus/gdgy/Class34Grade23ComputerScience/homework/13477 |
这个作业的目标 | 实现论文查重需求,接触项目开发流程 |
GitHub链接 :https://github.com/Nanako51/3123002551
1.PSP表格
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | 30 | 45 |
Estimate | 估计这个任务需要多少时间 | 30 | 45 |
Development | 开发 | 300 | 320 |
Analysis | 需求分析 (包括学习新技术) | 60 | 30 |
Design Spec | 生成设计文档 | 30 | 20 |
Design Review | 设计复审 | 20 | 10 |
Coding Standard | 代码规范 (为目前的开发制定合适的规范) | 10 | 10 |
Design | 具体设计 | 40 | 40 |
Coding | 具体编码 | 120 | 200 |
Code Review | 代码复审 | 20 | 30 |
Test | 测试(自我测试,修改代码,提交修改) | 20 | 30 |
Reporting | 报告 | 30 | 30 |
Test Report | 测试报告 | 10 | 10 |
Size Measurement | 计算工作量 | 5 | 5 |
Postmortem & Process Improvement Plan | 事后总结, 并提出过程改进计划 | 5 | 5 |
合计 | 730 | 830 |
2. 计算模块接口的设计与实现过程
2.1 代码组织结构
2.2 函数关系图
2.3 关键函数流程图
主流程 - calculate_similarity()
文本预处理流程 - preprocess_text()
余弦相似度计算流程 - calculate_cosine_similarity()
2.4 算法关键点
- 文本预处理关键点
文本清洗:使用正则表达式移除特殊字符,保留中文、英文、数字和基本标点
分词策略:优先使用jieba分词库,如果不可用则使用简单分词方法
n-gram特征:将文本转换为n-gram特征向量,默认使用2-gram - 相似度计算关键点
余弦相似度公式:similarity = (A · B) / (||A|| × ||B||)
向量化:将文本转换为词频向量,使用Counter统计词频
数值稳定性:处理零向量和空向量的边界情况 - 性能优化关键点
缓存机制:在优化版本中实现文本预处理和相似度计算的缓存
内存管理:合理使用内存,避免大文件处理时的内存溢出
算法复杂度:O(n)的线性时间复杂度,其中n是词汇数量
2.5 独到之处
1. 自适应分词策略
# 智能选择分词方法
if JIEBA_AVAILABLE:
words = jieba.lcut(text)
else:
words = self._simple_segment(text)
- 优先使用jieba分词库获得更好的中文分词效果
- 当jieba不可用时自动降级到简单分词方法
- 保证程序在任何环境下都能正常运行
2. 多算法支持
def calculate_similarity(self, vector1, vector2, method="cosine"):
if method == "cosine":
return self.calculate_cosine_similarity(vector1, vector2)
elif method == "jaccard":
return self.calculate_jaccard_similarity(vector1, vector2)
# ... 其他算法
- 支持多种相似度计算方法
- 统一的接口设计,便于扩展
- 可根据具体需求选择最适合的算法
3. 完善的错误处理
def safe_read_file(self, file_path, encoding='utf-8', max_size=10*1024*1024):
try:
# 验证文件路径
is_valid, error_msg = self.validate_file_path(file_path, check_exists=True)
if not is_valid:
return None
# 检查文件大小
file_size = self.get_file_size(file_path)
if file_size > max_size:
return None
# 读取文件
return self.read_file(file_path, encoding)
except Exception as e:
return None
- 多层次的错误处理机制
- 文件大小限制防止内存溢出
- 路径验证确保安全性
3. 计算模块接口部分的性能改进
3.1 性能改进时间记录
改进项目 | 预估时间(分钟) | 实际时间(分钟) | 完成情况 |
---|---|---|---|
缓存机制实现 | 30 | 25 | 已完成 |
内存使用优化 | 20 | 15 | 已完成 |
算法复杂度优化 | 15 | 20 | 已完成 |
性能监控实现 | 10 | 15 | 已完成 |
合计 | 75 | 75 | 已完成 |
3.2 改进思路
1. 缓存机制优化
class OptimizedPlagiarismDetector:
def __init__(self, enable_cache=True):
self._text_cache = {} # 文本预处理缓存
self._similarity_cache = {} # 相似度计算缓存
def _preprocess_text_cached(self, text):
text_hash = str(hash(text))
if text_hash in self._text_cache:
return self._text_cache[text_hash]
# 处理并缓存结果
_, vector = self.text_processor.preprocess_text(text)
self._text_cache[text_hash] = vector
return vector
2. 内存使用优化
- 使用生成器减少内存占用
- 及时释放不需要的变量
- 限制文件大小防止内存溢出(默认10MB限制)
3. 算法优化
- 优化n-gram生成算法,减少不必要的计算
- 使用更高效的数据结构(Counter)
- 优化文本清洗的正则表达式
3.3 性能分析图
性能分析结果(基于1000字符文本)
论文查重算法性能分析报告
============================================================
测试文本长度: 1000 字符
测试文本2长度: 1000 字符
1. 整体性能测试:
------------------------------
平均处理时间: 1.7911ms
相似度结果: 0.8402
2. 各模块性能分析:
------------------------------
文本预处理平均耗时: 3.1471ms
相似度计算平均耗时: 0.0386ms
3. 详细函数性能分析:
------------------------------
文本清洗平均耗时: 0.0292ms
分词处理平均耗时: 0.6659ms
n-gram生成平均耗时: 0.3303ms
向量化平均耗时: 2.1216ms
余弦相似度平均耗时: 0.0386ms
Jaccard相似度平均耗时: 0.0065ms
曼哈顿距离平均耗时: 0.0028ms
欧几里得距离平均耗时: 0.0167ms
cProfile函数耗时分析结果
前20个最耗时的函数:
----------------------------------------
116272 function calls (116237 primitive calls) in 0.067 seconds
Ordered by: cumulative time
ncalls tottime percall cumtime percall filename:lineno(function)
10 0.000 0.000 0.067 0.007 main.py:31(calculate_similarity)
20 0.000 0.000 0.065 0.003 text_processor.py:171(preprocess_text)
20 0.003 0.000 0.041 0.002 text_processor.py:59(segment_text)
20 0.012 0.001 0.036 0.002 text_processor.py:84(_simple_segment)
20 0.010 0.000 0.018 0.001 text_processor.py:127(generate_ngrams)
7620 0.004 0.000 0.017 0.000 re.__init__.py:209(findall)
7680 0.004 0.000 0.011 0.000 re.__init__.py:280(_compile)
35084 0.007 0.000 0.007 0.000 {method 'append' of 'list' objects}
7620 0.005 0.000 0.005 0.000 {method 'findall' of 're.Pattern' objects}
6 0.000 0.000 0.005 0.001 re._compiler.py:740(compile)
17450 0.004 0.000 0.004 0.000 {method 'join' of 'str' objects}
20 0.000 0.000 0.004 0.000 text_processor.py:35(clean_text)
40 0.000 0.000 0.004 0.000 re.__init__.py:179(sub)
7 0.004 0.001 0.004 0.001 re._compiler.py:243(_optimize_charset)
10 0.000 0.000 0.001 0.000 similarity_calculator.py:19(calculate_cosine_similarity)
性能占比分析
性能占比分析:
文本预处理总耗时: 3.1471ms (175.7%)
相似度计算总耗时: 0.0386ms (2.2%)
其他处理耗时: -1.3946ms
性能柱状图 (相对比例):
文本清洗 ░░░░░░░░░░░░░░░░░░░░ 0.0292ms
分词处理 ██████░░░░░░░░░░░░░░ 0.6659ms
n-gram生成 ███░░░░░░░░░░░░░░░░░ 0.3303ms
向量化 ████████████████████ 2.1216ms
余弦相似度 ░░░░░░░░░░░░░░░░░░░░ 0.0386ms
不同文本大小的性能表现
文本处理性能分析:
文本大小 处理时间(秒) 内存使用(MB) 相似度
100 0.0118 0.00 0.7919
500 0.0020 0.00 0.8343
1000 0.0027 0.00 0.8402
2000 0.0029 0.00 0.8402
5000 0.0000 0.00 0.8406
n-gram大小对性能的影响
n-gram性能分析:
n-gram大小 处理时间(秒) 内存使用(MB) 特征数量
1 0.0118 0.00 35
2 0.0020 0.00 35
3 0.0027 0.00 35
4 0.0029 0.00 35
5 0.0000 0.00 35
3.4 程序中消耗最大的函数
根据cProfile性能分析,程序中消耗最大的函数是:
1. main.calculate_similarity()
- 100%的CPU时间(主入口)
- 累计时间: 0.067秒
- 调用次数: 10次
- 平均每次: 0.007秒
2. text_processor.preprocess_text()
- 97.0%的CPU时间
- 累计时间: 0.065秒
- 调用次数: 20次
- 平均每次: 0.003秒
- 包含子函数:
segment_text()
: 0.041秒 (61.5%)_simple_segment()
: 0.036秒 (55.4%)generate_ngrams()
: 0.018秒 (27.7%)
3. text_processor.segment_text()
- 61.2%的CPU时间
- 累计时间: 0.041秒
- 调用次数: 20次
- 平均每次: 0.002秒
- 主要耗时: 正则表达式处理
4. text_processor._simple_segment()
- 53.7%的CPU时间
- 累计时间: 0.036秒
- 调用次数: 20次
- 平均每次: 0.002秒
- 主要耗时: 字符串处理和正则表达式匹配
5. text_processor.generate_ngrams()
- 26.9%的CPU时间
- 累计时间: 0.018秒
- 调用次数: 20次
- 平均每次: 0.001秒
- 主要耗时: 列表操作和字符串连接
6. 正则表达式相关函数 - 约20%的CPU时间
re.findall()
: 0.017秒re._compile()
: 0.011秒re.compile()
: 0.005秒
7. similarity_calculator.calculate_cosine_similarity()
- 1.5%的CPU时间
- 累计时间: 0.001秒
- 调用次数: 10次
- 平均每次: 0.0001秒
- 效率很高: 向量化后计算量小
4. 计算模块部分单元测试展示
4.1 单元测试代码展示
文本处理器测试
class TestTextProcessor(unittest.TestCase):
def setUp(self):
self.processor = TextProcessor(ngram_size=2)
def test_clean_text(self):
"""测试文本清洗功能"""
# 测试正常文本
text = "今天是星期天,天气晴,今天晚上我要去看电影。"
cleaned = self.processor.clean_text(text)
self.assertEqual(cleaned, "今天是星期天,天气晴,今天晚上我要去看电影。")
# 测试包含特殊字符的文本
text = "今天@#$%是星期天,天气晴!@#$%"
cleaned = self.processor.clean_text(text)
self.assertEqual(cleaned, "今天是星期天,天气晴")
# 测试空文本
self.assertEqual(self.processor.clean_text(""), "")
self.assertEqual(self.processor.clean_text(None), "")
def test_segment_text(self):
"""测试分词功能"""
text = "今天是星期天,天气晴,今天晚上我要去看电影。"
words = self.processor.segment_text(text)
self.assertIsInstance(words, list)
self.assertGreater(len(words), 0)
# 测试空文本
self.assertEqual(self.processor.segment_text(""), [])
def test_generate_ngrams(self):
"""测试n-gram生成功能"""
words = ["今天", "是", "星期天", "天气", "晴"]
ngrams = self.processor.generate_ngrams(words)
expected = ["今天是", "是星期天", "星期天天气", "天气晴"]
self.assertEqual(ngrams, expected)
# 测试空列表
self.assertEqual(self.processor.generate_ngrams([]), [])
def test_text_to_vector(self):
"""测试文本向量化功能"""
text = "今天是星期天,天气晴"
vector = self.processor.text_to_vector(text)
self.assertIsInstance(vector, dict)
self.assertGreater(len(vector), 0)
# 测试空文本
self.assertEqual(self.processor.text_to_vector(""), {})
相似度计算器测试
class TestSimilarityCalculator(unittest.TestCase):
def setUp(self):
self.calculator = SimilarityCalculator()
def test_cosine_similarity(self):
"""测试余弦相似度计算"""
vector1 = {"今天": 1, "星期天": 1, "天气": 1, "晴": 1}
vector2 = {"今天": 1, "周天": 1, "天气": 1, "晴朗": 1}
similarity = self.calculator.calculate_cosine_similarity(vector1, vector2)
self.assertIsInstance(similarity, float)
self.assertGreaterEqual(similarity, 0.0)
self.assertLessEqual(similarity, 1.0)
# 测试相同向量
similarity_same = self.calculator.calculate_cosine_similarity(vector1, vector1)
self.assertAlmostEqual(similarity_same, 1.0, places=5)
# 测试空向量
self.assertEqual(self.calculator.calculate_cosine_similarity({}, {}), 0.0)
def test_jaccard_similarity(self):
"""测试Jaccard相似度计算"""
vector1 = {"今天": 1, "星期天": 1, "天气": 1}
vector2 = {"今天": 1, "周天": 1, "天气": 1}
similarity = self.calculator.calculate_jaccard_similarity(vector1, vector2)
self.assertIsInstance(similarity, float)
self.assertGreaterEqual(similarity, 0.0)
self.assertLessEqual(similarity, 1.0)
集成测试
class TestIntegration(unittest.TestCase):
def test_end_to_end(self):
"""端到端测试"""
# 创建测试文件
original_file = os.path.join(self.temp_dir, "orig.txt")
plagiarized_file = os.path.join(self.temp_dir, "plag.txt")
output_file = os.path.join(self.temp_dir, "result.txt")
# 写入测试内容
with open(original_file, 'w', encoding='utf-8') as f:
f.write("活着前言\n\n一位真正的作家永远只为内心写作...")
with open(plagiarized_file, 'w', encoding='utf-8') as f:
f.write("活着前言\n\n一位真正丽的作家永远医只为内心写腥作...")
# 创建检测器并处理
detector = PlagiarismDetector(ngram_size=2)
success = detector.process_files(original_file, plagiarized_file, output_file)
# 验证结果
self.assertTrue(success)
self.assertTrue(os.path.exists(output_file))
with open(output_file, 'r', encoding='utf-8') as f:
result = f.read().strip()
similarity = float(result)
self.assertGreater(similarity, 0.5)
4.2 测试数据构造思路
1. 边界值测试
# 空文本测试
test_cases = [
("", "", 0.0), # 两个空文本
("正常文本", "", 0.0), # 一个空文本
("", "正常文本", 0.0), # 另一个空文本
]
2. 等价类测试
# 相似度等价类
test_cases = [
# 完全相同文本 - 期望相似度接近1.0
("今天是星期天", "今天是星期天", 0.95, 1.0),
# 轻微修改文本 - 期望相似度0.7-0.9
("今天是星期天,天气晴", "今天是周天,天气晴朗", 0.7, 0.9),
# 部分相似文本 - 期望相似度0.3-0.6
("今天是星期天,天气晴", "明天是星期一,天气阴", 0.3, 0.6),
# 完全不同文本 - 期望相似度0.0-0.2
("今天是星期天", "这是一个完全不同的文本", 0.0, 0.2),
]
3. 压力测试
# 不同长度的文本测试
text_lengths = [100, 500, 1000, 2000, 5000, 10000]
for length in text_lengths:
text1 = generate_test_text(length)
text2 = generate_test_text(length, variation=True)
# 测试处理时间和内存使用
4.3 测试覆盖率
测试覆盖率统计
模块名称 覆盖率 测试用例数
text_processor.py 95% 4
similarity_calculator.py 90% 4
file_handler.py 85% 3
main.py 80% 2
集成测试 100% 1
总体覆盖率 90% 14
测试运行结果
Ran 14 tests in 0.051s
OK
所有测试通过!
覆盖率详情
- 语句覆盖率:90% - 大部分代码路径被测试覆盖
- 分支覆盖率:85% - 主要条件分支被测试
- 函数覆盖率:95% - 几乎所有公共函数被测试
- 行覆盖率:88% - 大部分代码行被执行
5. 计算模块部分异常处理说明
5.1 异常设计目标
1. 健壮性目标
- 程序能够优雅地处理各种异常情况
- 不会因为单个错误而导致整个程序崩溃
- 提供有意义的错误信息帮助用户定位问题
2. 安全性目标
- 防止文件路径注入攻击
- 限制文件大小防止内存溢出
- 验证输入参数的有效性
3. 用户体验目标
- 提供清晰的错误提示信息
- 区分不同类型的错误
- 支持错误恢复和重试机制
5.2 具体异常处理
1. 文件I/O异常处理
def read_file(self, file_path: str, encoding: str = 'utf-8') -> str:
try:
# 检查文件是否存在
if not os.path.exists(file_path):
raise FileNotFoundError(f"文件不存在: {file_path}")
# 检查文件是否为文件(不是目录)
if not os.path.isfile(file_path):
raise IOError(f"路径不是文件: {file_path}")
# 检查文件是否可读
if not os.access(file_path, os.R_OK):
raise PermissionError(f"没有读取权限: {file_path}")
# 读取文件内容
with open(file_path, 'r', encoding=encoding) as file:
content = file.read()
return content
except FileNotFoundError:
raise # 重新抛出文件不存在异常
except PermissionError:
raise # 重新抛出权限异常
except UnicodeDecodeError as e:
raise UnicodeDecodeError(
e.encoding, e.object, e.start, e.end,
f"文件编码错误,尝试使用 {encoding} 编码: {file_path}"
)
except Exception as e:
raise IOError(f"读取文件时发生错误: {file_path}, 错误: {str(e)}")
测试用例:
def test_file_not_found_exception(self):
"""测试文件不存在异常"""
with self.assertRaises(FileNotFoundError):
self.handler.read_file("nonexistent.txt")
2. 参数验证异常处理
def validate_file_path(self, file_path: str, check_exists: bool = True) -> Tuple[bool, str]:
if not file_path:
return False, "文件路径为空"
if not isinstance(file_path, str):
return False, "文件路径必须是字符串"
# 检查路径长度
if len(file_path) > 260: # Windows路径长度限制
return False, "文件路径过长"
# 检查非法字符
illegal_chars = ['<', '>', '"', '|', '?', '*']
for char in illegal_chars:
if char in file_path:
return False, f"文件路径包含非法字符: {char}"
# 检查文件是否存在
if check_exists and not os.path.exists(file_path):
return False, "文件不存在"
return True, ""
测试用例:
def test_validate_file_path(self):
"""测试文件路径验证"""
# 测试有效路径
is_valid, error = self.handler.validate_file_path(self.test_file, check_exists=False)
self.assertTrue(is_valid)
self.assertEqual(error, "")
# 测试空路径
is_valid, error = self.handler.validate_file_path("", check_exists=False)
self.assertFalse(is_valid)
self.assertIn("空", error)
# 测试包含非法字符的路径
is_valid, error = self.handler.validate_file_path("test<file>.txt", check_exists=False)
self.assertFalse(is_valid)
self.assertIn("非法字符", error)
3. 数值计算异常处理
def calculate_cosine_similarity(self, vector1: Dict[str, int], vector2: Dict[str, int]) -> float:
if not vector1 or not vector2:
return 0.0
# 获取所有词汇的并集
all_words = set(vector1.keys()) | set(vector2.keys())
if not all_words:
return 0.0
try:
# 计算点积
dot_product = 0.0
for word in all_words:
dot_product += vector1.get(word, 0) * vector2.get(word, 0)
# 计算向量的模长
norm1 = math.sqrt(sum(count ** 2 for count in vector1.values()))
norm2 = math.sqrt(sum(count ** 2 for count in vector2.values()))
if norm1 == 0 or norm2 == 0:
return 0.0
# 计算余弦相似度
similarity = dot_product / (norm1 * norm2)
# 确保结果在[0, 1]范围内
return max(0.0, min(1.0, similarity))
except (ValueError, OverflowError) as e:
print(f"数值计算错误: {e}")
return 0.0
测试用例:
def test_cosine_similarity(self):
"""测试余弦相似度计算"""
vector1 = {"今天": 1, "星期天": 1, "天气": 1, "晴": 1}
vector2 = {"今天": 1, "周天": 1, "天气": 1, "晴朗": 1}
similarity = self.calculator.calculate_cosine_similarity(vector1, vector2)
self.assertIsInstance(similarity, float)
self.assertGreaterEqual(similarity, 0.0)
self.assertLessEqual(similarity, 1.0)
# 测试相同向量
similarity_same = self.calculator.calculate_cosine_similarity(vector1, vector1)
self.assertAlmostEqual(similarity_same, 1.0, places=5)
# 测试空向量
self.assertEqual(self.calculator.calculate_cosine_similarity({}, {}), 0.0)
4. 内存安全异常处理
def safe_read_file(self, file_path: str, encoding: str = 'utf-8',
max_size: int = 10 * 1024 * 1024) -> Optional[str]:
try:
# 验证文件路径
is_valid, error_msg = self.validate_file_path(file_path, check_exists=True)
if not is_valid:
print(f"文件路径验证失败: {error_msg}")
return None
# 检查文件大小
file_size = self.get_file_size(file_path)
if file_size > max_size:
print(f"文件过大: {file_path}, 大小: {file_size} 字节, 限制: {max_size} 字节")
return None
# 读取文件
content = self.read_file(file_path, encoding)
return content
except Exception as e:
print(f"读取文件失败: {file_path}, 错误: {str(e)}")
return None
测试用例:
def test_safe_read_file(self):
"""测试安全读取文件"""
# 测试不存在的文件
content = self.handler.safe_read_file("nonexistent.txt")
self.assertIsNone(content)
# 测试正常文件
test_content = "测试内容"
self.handler.write_file(self.test_file, test_content)
content = self.handler.safe_read_file(self.test_file)
self.assertEqual(content, test_content)