结对第二次——文献摘要热词统计及进阶需求
-
这个作业属于哪个课程:软件工程 1916 | W
-
这个作业要求在哪里:结对第二次——文献摘要热词统计及进阶需求
-
结对学号:021600823 余秉鸿, 221600126 刘忠燏
-
这个作业的目标:完成作业要求中的基本需求和进阶需求;熟悉 Git 和 GitHub 的使用;学习并掌握单元测试技巧;借助单元测试,适当重构部分代码
-
Fork 的 GitHub 项目地址:PairProject1-Java
-
GitHub 的签入记录
一些备注
本次结对作业中,我和队友之间的分工是这样的:
- 余秉鸿:完成基础需求,留下方便队友完成进阶需求的API
- 刘忠燏:在队友代码的基础上,完成进阶需求
由于这种分工,普通需求和进阶需求的仓库实际上是分别提交的,所以我只Fork了基本仓。
而且由于对Git基本操作的不太熟练,所以提交记录可能会有些许凌乱。。。
PSP 表格
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | 20 | 20 |
· Estimate | · 估计这个任务需要多少时间 | 20 | 20 |
Development | 开发 | 750 | 1335 |
· Analysis | · 需求分析(包括学习新技术) | 180 | 240 |
· Design Spec | · 生成设计文档 | 30 | 45 |
· Design Review | · 设计复审 | - | |
· Coding Standard | · 代码规范(为目前的开发制定合适的规范 | 30 | 90 |
· Design | · 具体设计 | 120 | 240 |
· Coding | · 具体编码 | 360 | 570 |
· Code Review | · 代码复用 | - | |
· Test | · 测试(自我测试、修改代码、提交修改) | 30 | 150 |
Reporting | 报告 | 40 | 40 |
· Test Report | · 测试报告 | 10 | 10 |
· Size Measurement | · 计算工作量 | 10 | 10 |
· Postmortem & Process Improvement | · 事后总结,并提出过程改进计划 | 20 | 20 |
合计 | 810 | 1395 |
WordCount 基本需求——思路
作业发布之后,我稍微看了一下需求,并且大概思考了一下如何完成这次作业才能更好的学到东西,因为自认为半路出家,基础可能稍微薄弱。最后和队友商量,他完成进阶需求,我来完成基础需求,并且给出接口,但是我们商量的时候交代没有特别清楚(进阶需求需要调用wordCount的地方我都实现了,队友需要改成进阶模式的时候调用就好了)。
因为当时没有沟通清楚,所以在前两天队友过来讨论进度的时候可能让他有点小崩溃,我当晚虽然加急重构了一下代码,但内心实在还是感到有些过意不去。
我的基本思路就是用BufferedReader()和BufferedWriter()来进行文件读写。并且使用readLine()方法逐行读取并分别传入统计字符,统计行数,统计单词三个方法。然后返回统计结果,类图有空补上。
Wordcount 基本功能——实现
因为wordCount在基础和进阶的需求上有不一样的地方,所以我需要添加一个判断事件,根据进阶和基础需求的不一致来返回结果。但三个统计方法都是一样的。
统计字符和统计行数较为简单,思路也不用多说,其中统计行数的方法本来可以省略,但是考虑到可能有包含空字符如'\t'的非空行,所以需要对行内字符进行判断。
主要难点在统计单词上,我们首先将不含有分隔符的字母数字数组称为单词,并且按照本次作业的要求,将其分为合法单词和不合法单词。
对于输入的String,我们认为除了最后一个单词,任何两个单词之间都有分隔符的存在,所以我写了一个cutWords方法,返回每次读取String遇到的分隔符的下标,如果其中不存在分隔符,就返回当前String的长度。我们就认为该String是一个单词。
public int cutWords(String input) { // 分隔单词 char[] charArray = input.toCharArray(); for (int index = 0; index < charArray.length; index++) { if (isNotDigitOrAlpha(charArray[index])) // 出现非字母数字字符则返回下标 return index; } return charArray.length; }
针对是否是一个合格的单词,我写了一个isWord()方法来判断
public boolean isWord(String input) { // 判断是否是符合标准的单词 if(input.length() < 4) return false; for(int i = 0; i < 4; i++) { if(isNotDigitOrAlpha(input.charAt(i)) || !Character.isLowerCase(input.charAt(i))) return false; } for(int i = 4; i < input.length(); i++) { if(isNotDigitOrAlpha(input.charAt(i))) return false; } return true; }
在计数时,我使用了HashMap,方便存取,针对进阶需求,则设置了权值,可以通过addWeight()方法设置的权值队列并从中获取,这里涉及了另一块代码WordHeap。
int countWords(String input, HashMap<String, Integer> wordMap, String type) { // 统计单词数,并存入哈希表 int result = 0; String word; int cut; int weight = 1; // 初始权重为一 if (!type.equals("")) { int i = weightTable.isExist(type); // 判断权值表中是否含有此类,有则替换权值。 if (i != -1) { weight = weightTable.heap.get(i).value; } } input = input.toLowerCase(); // 转化为小写 cut = cutWords(input); while (cut != input.length()) { word = input.substring(0, cut); input = input.substring(cut); if (isWord(word)) { addMap(word, weight, wordMap); result++; } cut = cutSign(input); input = input.substring(cut); cut = cutWords(input); } word = input; if (isWord(word)) // 防止最后一个单词漏读 { addMap(word, weight, wordMap); result++; } return result; } }
void addWeight(String type, Integer value) {// 添加类型权重 type = type.toLowerCase(); // 全部替换成小写 int index = weightTable.isExist(type); // 判断原先是否有权重记录,如果有则更新,没有则添加。 if (index == -1) weightTable.add(type, value); else weightTable.heap.get(index).value = value; }
统计完所有的单词后,会进行依照单词的权值进行排序,这里我选择了最大堆,并依照了键值对的思想,构建了一个wordValue类,来存储键值,至于堆的实现,大致就是造轮子。。。
private void wordClassify(HashMap<String, Integer> wordMap, wordHeap wh) // 单词归类,将哈希表中的单词归入词堆(最大堆) { String word; java.util.Iterator<Entry<String, Integer>> iter = wordMap.entrySet().iterator(); // 键值对遍历哈希表 Integer value; while (iter.hasNext()) { Entry<String, Integer> entry = iter.next(); word = entry.getKey(); // 获取key value = entry.getValue(); // 获取value wh.insert(word, value); } }
static class wordValue // 键值对 { public String word; public Integer value; wordValue(String word, Integer value) { this.word = word; this.value = value; } }
public boolean compare(int a, int b) // 比较两个键值对的顺序,返回ture则a在前 { if(heap.get(a).value > heap.get(b).value) // 比较值 return true; else if(heap.get(a).value.equals(heap.get(b).value)) { int result = heap.get(a).word.compareTo(heap.get(b).word); //比较字典顺序 return result <= 0; }
public void insert(String word, Integer value) // 插入键值对 { //在数组尾部添加,且注意下标为0的位置不放元素 wordValue wv = new wordValue(word, value); if(heap.size()==0) add("", -1); heap.add(wv); heapUp(heap.size() - 1); } private void heapUp(int index) //上浮操作 { if(index > 1) { // 求出其父亲节点 int parent = index / 2; // 如果父亲节点的值小于index节点的值,交换两者的位置 if(compare(index, parent)) { Collections.swap(heap, parent, index); heapUp(parent); } } } void delete() // 删除键值对 { heap.set(1, heap.get(heap.size() - 1)); //把最后的一个叶子的数值赋值给index位置 heapDown(1); heap.remove(heap.size() - 1); // 移除 } private void heapDown(int index) // 下沉操作 { // 因为第一个位置不存放数据,不考虑在内,最后一个也要删除,不考虑在内 int n = heap.size()-2; //记录较大的儿子的位置 int child = -1; if(2 * index>n) { //2*index>n 说明该节点没有左右儿子节点了,则返回 return; } else if (2 * index < n) { //两个儿子都在 child = 2*index; if(!compare(child, child + 1)) { child++; } } else if(2 * index == n) { //只有左儿子 child = 2 * index; } //交换和递归 if(compare(child, index)) { Collections.swap(heap, child, index); heapDown(child); } }
这块部分是根据进阶需求进行部分修改(值得一提的提交的代码这块地方是有点问题的,在符合规范爬取的文件中不会报错,但是如果调用某些不符合规范的文件将出现越界的情况————现告知搭档并已修改)
修改之前
String content; // 利用readline()函数读取每一行的值 while ((content = br.readLine()) != null) { String type = ""; if (!all) { int index = counter.cutWords(content); // 调用count类的单词分割,将第一个单词分割出来 if (index == content.length())//修改为index + 2 >= content.length() continue; type = content.toLowerCase().substring(0, counter.cutWords(content)); content = content.substring(index + 2); // 冒号与空格不计(若不修改此处在不符合规范的文件输入中可能越界,符合规范的文件输入则不受影响) if (!isWord(type))//已删除,原因:规范文件输入情况下的多余判断 continue; } characters += counter.countCharacters(content, true); // 字符计数 lines += counter.countLines(content); // 有效行计数 words += counter.countWords(content, wordMap, type); // 单词计数 }
修改之后
String content; // 利用readline()函数读取每一行的值 while ((content = br.readLine()) != null) { String type = ""; if (!all) { int index = counter.cutWords(content); // 调用count类的单词分割,将第一个单词分割出来 if ((index + 2) >= content.length()) continue; type = content.toLowerCase().substring(0, counter.cutWords(content)); content = content.substring(index + 2); // 冒号与空格不计 } characters += counter.countCharacters(content, true); // 字符计数 lines += counter.countLines(content); // 有效行计数 words += counter.countWords(content, wordMap, type); // 单词计数 }
接口
public void countWords(String fileName, int top, boolean all) throws IOException // top为前几的词频 all为是否为基础需求
WordCount 基本需求——单元测试
我对单元测试其实是小白。。。所以这次的单元测试基本都是跟在队友屁股后面学东西,自己的代码也都是他帮忙测试的,我基本都是看他测试并提出自己的预期,就比较臭不要脸的从他那里拿测试代码了。。。
@Test public void testIsNotDigitOrAlpha() { for(int i = 0; i < 26; i++) { assertFalse(String.format("%c: ", 'A' + i), c.isNotDigitOrAlpha((char)('A' + i))); assertFalse(String.format("%c: ", 'a' + i), c.isNotDigitOrAlpha((char)('a' + i))); } for(int i = 0; i < 9; i++) { assertFalse(String.format("%c: ", '0' + i), c.isNotDigitOrAlpha((char)('0' + i))); } assertTrue("â: ", c.isNotDigitOrAlpha('â')); assertTrue("é: ", c.isNotDigitOrAlpha('é')); }
上述方法是判断一个字符是否是非字母数字字符。于是测试用例就覆盖了所有的英文字母和数字,至于最后两个法文字母,是从爬虫的结果里找到了,将其作为一个非英文字母的用例。
@Test public void testIsWord() { assertTrue(c.isWord("apple")); assertFalse(c.isWord("foo")); assertFalse(c.isWord("123foo")); assertFalse(c.isWord("Café")); assertTrue(c.isWord("file123")); assertFalse(c.isWord("Mâché")); }
刚才那个例子,里面的情况是可以列举的,而上面的被测方法是判断一个字符串(仅由小写字母和数字构成)是否是合法的单词,传入这个方法的字符串是已经被分割符分割并全部转成小写后的单词,所以以上的样例大概能覆盖到各种情况。
评价
对自己的评价
我吧,一开始接到任务的时候,也没有和队友做好沟通和交流,虽然自己大致完成了自己的工作,并实现了进阶的一部分接口,但是很多地方其实有些不够,包括一些地方没有考虑周全,导致如果出现拓展需求的话可能需要重构一部分代码。。。还是自己经验太欠缺。需要多学习。
对队友的评价
我非常感谢我的队友,因为我自己的基础其实是有问题的,对于项目的理解很多都还不够,比如Git的使用,比如单元测试,还有对于方法的构建的合理性,这些很多都是我的队友跟在我身后教我的,而且并没有嫌弃我拖后腿。。。他对于库的了解要比我多很多,比如我想得到一个方法,我第一反应是自己写,而他更倾向从现有的库中去找,这无疑减少了bug的可能性。