第一次个人编程作业

软件工程 网工1934班级链接
作业要求 作业要求链接
作业目标 1. 尝试个人开发一个论文查重项目
2. 学会使用 PSP 表格预计和记录各模块开发时间
3. 学会使用单元测试对项目进行测试
4. 学习使用性能分析工具来找出代码中的性能瓶颈并进行改进
5. 学会使用 GitHub 来管理源代码和测试用例

作业 GitHub 链接

一、PSP 表格

PSP 2.1 Personal Software Process Stages 预估耗时(分钟) 实际耗时(分钟)
Planning 计划
· Estimate · 估计这个任务需要多少时间 240 360
Development 开发
· Analysis · 需求分析 (包括学习新技术) 240 300
· Design Spec · 生成设计文档 30 45
· Design Review · 设计复审 10 5
· Coding Standard · 代码规范 (为目前的开发制定合适的规范) 30 45
· Design · 具体设计 300 190
· Coding · 具体编码 150 160
· Code Review · 代码复审 45 30
· Test · 测试(自我测试,修改代码,提交修改) 120 300
Reporting 报告
· Test Report · 测试报告 30 25
· Size Measurement · 计算工作量 10 20
· Postmortem & Process Improvement Plan · 事后总结, 并提出过程改进计划 15 10
· 合计 1240 1690

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

模块设计

  • 主类
    • Main 类:main(),传入文件绝对路径,计算两文本内容重复率,并写入答案文件。
  • 工具类
    • WordFreq 类:记录分词后每个词的词频。
    • Article 类:对文件内容的封装,包含了文件名、原始字符串、分词结果以及词频集合。
    • TxtIOUtils 类:readTxt(),读取txt文件内容;writeTxt(),向txt文件写入内容。
    • TextTools 类:getSegmentList(),分词操作;getWordFrequency(),统计词频;getArticle(),封装成 Article.
    • TextException 类:自定义异常处理类,用于抛出文件文本内容为空的异常。
    • CompareTask 类:对两 Article 对象生成词频向量,TF-IDF 加权,计算 Jaccard 系数,即两文本内容重复率。
    • CompareReport 类:生成两文本重复率的报告。
  • 依赖库
    • hanlp:用于分词。
    • guava:用于统计词频时用到的 MultiSet 支持。
    • junit:用于单元测试。

项目结构

关键函数

getSegmentList()

getWordFrequency()

关键算法

前几天上数据挖掘课的时候,讲到相似性度量的时候,书中就提到“广义 Jaccard 系数”,于是萌生了用作论文查重算法的念头。

广义 Jaccard 系数

广义 Jaccard 系数,元素的取值可以是实数。又称为 Tanimoto 系数,用EJ来表示,计算方式如下:
EJ(A,B) = (A * B) / (|A|^2 + |B|^2 - A * B)
其中 A、B 分别表示为两个向量,集合中每个元素表示为向量中的一个维度,在每个维度上,取值通常是 [0, 1] 之间的。

TF-IDF

对于两篇文档,分词之后,形成两个“词语--词频向量”,词语可以做为 EJ 的维度,如何将词频转换为实数值。借鉴 tf/idf 的思路。对于每个词语,有两个频度:1.在当前文档中的频度;2. 在所有文档中的频度。其中 1 相当于 tf ,与权重正相关;2 相当于 df,与权重反相关。对于 1,权重就可以取词频本身 tf(w) = D(w),D(w)表示在当前文档中 w 出现的次数。对于2,计算权重为 idf (w) = log(TotalWC/C(w)). C(w)是词语 w 在所有文档中出现的次数,TotalWC 是所有文档中所有词的总词频。

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

最初采用 tika 来获取 txt 文件内容,发现大材小用了,并且要增加 apache 的许多外部依赖包,导致程序打包体积过大,性能也有所降低。后来就简单地使用 IO 来读写字符串。
Jprofiler 的性能分析图:

从上图来看,程序消耗最大的属于 HanLP 中的函数,及主要消耗来自分词操作。

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

测试用例在 3119005373 文件夹下。前面的模块测试用例使用作业要求里的例子,重复率容易看得出,最后的报告多个测试使用老师提供的样例。

public class ProcessTest {

	private static String root = "3119005373/";

	@Test
	/*
	  测试分词
	 */
	public void segmentTest() {
		String str = "今天是星期天,天气晴,今天晚上我要去看电影。";
		List<Term> segmentList = TextTools.getSegmentList(str);
		System.out.println(segmentList);
	}

	@Test
	/*
	  测试统计词频
	 */
	public void wordFreqTest() {
		String str = "今天是星期天,天气晴,今天晚上我要去看电影。";
		List<Term> segmentList = TextTools.getSegmentList(str);
		List<WordFreq> wordFreqList = TextTools.getWordFrequency(segmentList);
		System.out.println(wordFreqList);
	}

	@Test
	/*
	  测试getArticle
	 */
	public void getArticleTest() throws IOException, TextException {
		Article article = TextTools.getArticle(root + "test_orig.txt");
		System.out.println(article.getName());
		System.out.println(article.getText());
		System.out.println(article.getSegmentList());
		System.out.println(article.getWordFreqList());
	}

	@Test
	/*
	  测试CompareTask和CompareReport
	 */
	public void compareTest() throws IOException, TextException {
		// TODO 涉及读写可用多线程FutureTask优化。
		Article a1 = TextTools.getArticle(root + "test_orig.txt");
		Article a2 = TextTools.getArticle(root + "test_plag.txt");
		CompareTask task = new CompareTask(a1, a2);
		task.execute();
		CompareReport report = task.getCompareReport();
		System.out.println(report);
	}

	@Test
	/*
	  报告多个测试
	 */
	public void compareMultiTest() throws IOException, TextException {
		List<CompareTask> tasks = new ArrayList<>();

		Article a1 = TextTools.getArticle(root + "orig.txt");
		Article b1 = TextTools.getArticle(root + "orig_0.8_add.txt");
		tasks.add(new CompareTask(a1, b1));
		System.out.println("load 1");

		Article a2 = TextTools.getArticle(root + "orig.txt");
		Article b2 = TextTools.getArticle(root + "orig_0.8_del.txt");
		tasks.add(new CompareTask(a2, b2));
		System.out.println("load 2");

		Article a3 = TextTools.getArticle(root + "orig.txt");
		Article b3 = TextTools.getArticle(root + "orig_0.8_dis_1.txt");
		tasks.add(new CompareTask(a3, b3));
		System.out.println("load 3");

		Article a4 = TextTools.getArticle(root + "orig.txt");
		Article b4 = TextTools.getArticle(root + "orig_0.8_dis_10.txt");
		tasks.add(new CompareTask(a4, b4));
		System.out.println("load 4");

		Article a5 = TextTools.getArticle(root + "orig.txt");
		Article b5 = TextTools.getArticle(root + "orig_0.8_dis_15.txt");
		tasks.add(new CompareTask(a5, b5));
		System.out.println("load 5");

		for (CompareTask task : tasks) {
			task.execute();
			System.out.println(task.getCompareReport().toString());
			System.out.println();
		}
	}

}

测试覆盖率截图:

主方法因为会退出程序终止测试,所以没加入单元测试中。

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

文件不存在的异常

用 file.exists() 判断文件是否存在,放在 TxtIOUtils 类的 readTxt() 方法下:

if(!file.exists())
  throw new FileNotFoundException(file.getName()+"文件不存在!");

异常测试:

@Test
/*
测试文本文件不存在的异常
*/
public void fileNotExistTest() throws TextException {
	try {
		Article article = TextTools.getArticle(root + "test.txt");
	}catch (IOException e) {
		e.printStackTrace();
	}
}

测试场景:3119005373 文件夹中不存在 test.txt 文件。
测试截图:

文本为空的异常

用 str.isEmpty() 判断文本内容是否为空,放在 TextTools 类下的 getArticle() 方法下:

if(text.isEmpty())
  throw new TextException(file.getName()+"文本为空!");

异常测试:

@Test
/*
测试文本内容为空的异常
*/
public void emptyTextTest() throws IOException {
	try {
		Article article = TextTools.getArticle(root + "test_empty.txt");
	}catch (TextException e) {
		e.printStackTrace();
	}
}

测试场景:test_empty.txt 为空。
测试截图:

答案文件已存在且不可写的异常

用 file.canWrite() 判断文件是否可写,放在 TxtIOUtils 类的 writeTxt() 方法下:

if(!file.canWrite())
  throw new FileNotFoundException(file.getName()+"文件已存在且不可写!");

异常测试:

@Test
/*
测试答案文件已存在且不可写的异常
*/
public void fileReadOnlyTest() {
	try {
		String str = "写不进去啊";
		TxtIOUtils.writeTxt(str, root+"test_readonly.txt");
	}catch (IOException e) {
		e.printStackTrace();
	}
}

测试场景:test_readonly.txt 设置只读。
测试截图:

六、参考

  1. https://blog.csdn.net/xceman1997/article/details/8600277
  2. https://www.cnblogs.com/TtTiCk/archive/2007/08/04/842819.html
  3. https://gitee.com/nanbowang/HanLP-TextSimilarity
posted @ 2021-09-20 01:58  John-Wong  阅读(144)  评论(2编辑  收藏  举报