结对第二次—文献摘要热词统计及进阶需求

作业格式


课程名称:软件工程1916|W (福州大学)

作业要求:结对第二次—文献摘要热词统计及进阶需求

结对学号:221600415-傅德泉 & 221600416-黄海山

Github项目地址:基础篇
Github项目地址:进阶篇

代码签入记录

作业正文


一、分工情况

  • 221600415-傅德泉
    • 1、前期需求分析设计以及规划实现
    • 2、PSP表开发规划
    • 3、类图、流程图讨论设计
    • 4、博客文档主要撰写
    • 5、目标项目的用例测试
  • 221600416-黄海山
    • 1、前期需求分析设计以及规划实现
    • 2、类图、流程图讨论设计
    • 3、代码撰写,算法设计与程序功能的实现
    • 4、项目性能分析
    • 5、项目性能优化改进

二、PSP表

PSP2.1 Personal Software Process Stages 预估耗时(分钟) 实际耗时(分钟)
Planning 计划
· Estimate · 估计这个任务需要多少时间 30 40
Development 开发
· Analysis · 需求分析 (包括学习新技术) 150 200
· Design Spec · 生成设计文档 50 60
· Design Review · 设计复审 30 20
· Coding Standard · 代码规范 (为目前的开发制定合适的规范) 40 20
· Design · 具体设计 100 90
· Coding · 具体编码 360 400
· Code Review · 代码复审 100 150
· Test · 测试(自我测试,修改代码,提交修改) 200 260
Reporting 报告
· Test Repor · Test Repor 60 70
· Size Measurement · 计算工作量 30 20
· Postmortem & Process Improvement Plan · 事后总结, 并提出过程改进计划 90 95
* 合计 1240 1425

三、解题思路描述

  • 需求分析:刚拿到此次作业题目的时候,第一感觉是有点压力的,不过在和队友认真阅读,按点分析后,我们从宏观角度得到大概的需求框架:基础需求大概是实现一个命令行程序,将文本输入进行解析,得到文本里的字符数单词数量,有效行数以及top10高频单词等。将基本功能封装后用于进阶需求,对论文列表和指定文件进行统计操作。通过编程实现方法,将原来的大问题拆解成各个功能点,分块解决逐个击破。
  • 实现思路:主要是利用BufferedReader读出文件的内容,然后用readLine按行读出文件,按照特定的分隔字符利用split函数判断合法性,统计字符数。接着用string类型的List存储单词,最后用map按字典序存储。而在统计字符数功能中,直接调用BufferedReader的read函数,对读取的字符直接操作。
  • 存在的问题:在实现进阶需求时,爬虫功能之前一直没有接触过,但是通过上网学习,结合过去文档树相关知识点的认知,很快得到使用。

四、设计实现过程

1、代码组织

  • 基本需求
    • 类的组织

      |- src

        |- Main.java(主程序,可以从命令行接收参数)

        |- lib.java(包含多个其它自定义函数)
    • 函数组织
      • countLetters 统计字符数
      • splitStringIntoList 统计单词,调用转小写函数toLower
      • getContentAndLinesFromFilePath 统计行数,获取文件内容成字符串,全转为小写
      • getTopTenWords 统计前十字符,其中调用函数getOrderTopTenMap按频率和字典序排序,而在该函数中再次调用函数sortMapByKey和sortMapByValue,分别实现对Map按key和value进行排序
  • 进阶需求
    • 类的组织

      |- cvpr(爬取论文列表,输出到result.txt)

      |- src

        |- Main.java(主程序,可以从命令行接收参数)

        |- lib.java(包含多个其它自定义函数)
    • 函数组织
      • countLettersInPapers 封装基础功能,在论文里统计字符数,行数,单词数。其中调用函数toLower 实现转小写包含数字的字符串
      • getTopTenWeightWords 统计前十权重的单词,调用函数getOrderTopTenMap 使map按频率和字典序排序。该函数内部调用函数sortMapByKey和sortMapByValue,分别实现对Map按key和value进行排序
      • getTopTenWordarray 统计前十的词组。函数调用同getTopTenWeightWords

2、单元测试

  • 测试思路:设定十种不同的临界输入,提前判断得出测试结果,通过对不同input文件进行算法运算获得输出,判断与预想结果是否一致
    • 测试getTopTenWeightWords函数
    • 测试countLettersInPapers函数
    • 测试空白文件
    • 测试文件不存在
    • 测试单词数字开头的情况
    • 测试大小写单词是否能识别
    • 测试空白行是否识别
    • 测试对\r\n的特殊输入进行
    • 测试开头带有空格、tab的情形
    • 测试带有空格,非字幕数字符号的分隔符
  • 单元测试截图

3、代码组织与类图

  • 基本需求

  • 进阶需求

4、算法关键与关键实现部分流程图

(算法的关键与关键实现部分流程图--map函数的函数设计)

  • 关键函数:getOrderTopTenMap--按频率和字典序排序
  • 实现原理:传入文本的单词数组,用map里的getOrDefaul函数统计所有单词的频率,然后将map数组按值排序,再把频率相同的map分别存到不同的子数组中,在对这些子数组里的map按key排序,在把每个子数组内的map合并到总map数组中。
  • 算法思维图

五、性能分析与改进

1、改进花费时间:95min

2、改进的思路:

  • 由于一开始是在执行splitStringIntoList(用正则表达式匹配空白符和非字母数字,将整个文本内容字符串切割为一个个单词)过程中,顺便用map存储合法单词,由于题目要求是要三个独立的接口,因此在调用getTopTenWords函数时是先执行了一次splitStringIntoList函数。改进后splitStringIntoList函数只用于统计单词个数,getTopTenWords则是先自行遍历一次文件,查找合法单词后,进行后续操作。
  • 此外,刚开始对文件内容的读取也存在一定功能冗余:统计字符函数、统计单词总数函数和统计有效行三个功能彼此独立,导致在计算过程中需要对输入读取三次,致使时间资源的浪费。后来通过将文件读取到内存中,只进行一次输入操作,从而节省代码开销。

3、性能分析图

4、消耗最大的函数

  • map按频率和字典序排序函数-- getOrderTopTenMap

六、关键代码

1、基础需求

  • map按频率和字典序排序。传入文本的单词数组,用map里的getOrDefaul函数统计所有单词的频率,然后将map数组按值排序,再把频率相同的map分别存到不同的子数组中,在对这些子数组里的map按key排序,在把每个子数组内的map合并到总map数组中,从而实现题目需求。
public Map<String,Integer>getOrderTopTenMap(Map<String, Integer>m,Integer arrayNum){
		Map<String, Integer> map=sortMapByValue(m);//通过Value排序;
		int order=0;
		int numInten=0;
		for(Integer item:map.values()){
			++order;
			if(order==arrayNum){
				numInten=item;
			}
		}

		Iterator<Map.Entry<String, Integer>> it = map.entrySet().iterator();
		while(it.hasNext()){
			Map.Entry<String, Integer> entry = it.next();
			if(entry.getValue()<numInten)
				it.remove();//使用迭代器的remove()方法删除元素
		}

		Set<Integer> valueSet=new TreeSet<>();//有序
		for(String key:map.keySet()){//看看有几种值结果
			valueSet.add(map.get(key));
		}

		Map<Integer,Integer> sameMap=new HashMap<>();//每种结果有多少个
		for(Integer value:valueSet){
			for(String key:map.keySet()){
				if(map.get(key)==value){
					sameMap.put(value,sameMap.getOrDefault(value,0)+1);
				}
			}
		}
		//相同频率拆段
		List<Map<String,Integer> > mapList=new ArrayList<>();
		for(Integer item:valueSet){
			Map<String,Integer> mapItem=new HashMap<>();
			for(String s:map.keySet()){
				if(map.get(s)==item){
					mapItem.put(s,map.get(s));
				}
			}
			mapList.add(mapItem);
		}

		List<Map<String,Integer> > mapList2=new ArrayList<>();
		//频率相同排序
		for(int i=mapList.size()-1;i>=0;--i){//set升序,频率最高的段在后面
			mapList2.add(sortMapByKey(mapList.get(i)));
		}
		Map<String,Integer>result=new LinkedHashMap<>();
		int num=0;
		for(int i=0;i<mapList2.size();++i){
			for(String key: mapList2.get(i).keySet()){
				++num;
				if(num<=arrayNum)
					result.put(key,mapList2.get(i).get(key));
				else
					break;
			}
		}
		return result;
	}

2、进阶需求

  • 爬取论文信息主函数,通过对网页DOM树的操作,获取页面信息节点
public static void main(String args[]) throws IOException {
        Document doc=Jsoup.connect(URL).maxBodySize(0).get();
        Elements elements=doc.getElementsByClass("ptitle").select("a");
        BufferedWriter bw=new BufferedWriter(new OutputStreamWriter(new FileOutputStream(new File(STORE_NAME))));
        int index=0;
        System.out.println("start");
        for(Element e : elements){
            String href=e.attr("href");
            System.out.println(index+":"+href);
            String title=e.text();
            String text=Jsoup.connect(URL_PRE+href).get().getElementById("abstract").text();
            bw.write(String.valueOf(index));
            bw.write("\r\n");
            bw.write("Title: "+title+"\r\n");
            bw.write("Abstract: "+text+"\r\n\r\n");
            ++index;
        }
        bw.flush();
        bw.close();
        System.out.println("end");
    }
  • 统计前十权重单词,传入boolean类型的isWeight参数判断是否需要对单词进行权重统计,之后按照基础篇对单词进行排序的思想处理输入。
public Map<String,Integer> getTopTenWeightWords(String inputPath,int arrayNum,boolean isWeight)throws Exception{
		Map<String,Integer> map=new HashMap<>();
		FileInputStream fileInputStream=new FileInputStream(new File(inputPath));
		InputStreamReader inputStreamReader=new InputStreamReader(fileInputStream);
		BufferedReader br = new BufferedReader(inputStreamReader);
		String itemStr;
		while((itemStr=br.readLine())!=null){
			if(itemStr.startsWith("Title:")||itemStr.startsWith("Abstract:")){
				String[] strList=itemStr.split(regex);
				int weight=1;
				if(strList[0].equals("Title")){
					if(isWeight){
						weight=10;
					}
				}
				else if(strList[0].equals("Abstract"))
					weight=1;
				for(int i=1;i<strList.length;++i){
					String key=strList[i];
					if(key.matches(wordRegex)){//是一个单词
						key=toLower(key);
						map.put(key,map.getOrDefault(key,0)+weight);
					}
				}
			}
		}
		br.close();
		return getOrderTopTenMap(map,arrayNum);
	}
  • 统计前十的数组。将数组里的元素拼接为一个字符串,之后按照基础篇中对单词进行排序的思路排序数组。
public Map<String,Integer> getTopTenWordarray(String inputPath,int num,int lines,boolean isWeight)throws Exception{
		Map<String,Integer> map=new HashMap<>();
		FileInputStream fileInputStream=new FileInputStream(new File(inputPath));
		InputStreamReader inputStreamReader=new InputStreamReader(fileInputStream);
		BufferedReader br = new BufferedReader(inputStreamReader);
		String itemStr;
		while((itemStr=br.readLine())!=null){
			if(itemStr.startsWith("Title:")||itemStr.startsWith("Abstract:")){
				String[] strList=itemStr.split(regex);
				int weight=1;
				if(strList[0].equals("Title")){
					if(isWeight){
						weight=10;
					}
					for(int i=1;i<strList.length;++i){
						boolean isArray=true;
						StringBuilder sb=new StringBuilder();
						int j;
						for(j=i;j<i+num&&j<strList.length;++j){
							String key=strList[j];
							key=toLower(key);
							if(!key.matches(wordRegex)){//遇到无效单词
								i=j;
								isArray=false;
								break;
							}
							else{
								if(sb.length()>0){
									sb.append(" "+key);
								}
								else{
									sb.append(key);
								}
							}
						}
						if(isArray&&j==num+i) {
							map.put(sb.toString(), map.getOrDefault(sb.toString(), 0) + weight);
						}
					}
				}
				else if(strList[0].equals("Abstract")){
					weight=1;
					for(int i=1;i<strList.length;++i){
						boolean isArray=true;
						StringBuilder sb=new StringBuilder();
						int j;
						for(j=i;j<num+i&&j<strList.length;++j){
							String key=strList[j];
							key=toLower(key);
							if(!key.matches(wordRegex)){//遇到无效单词
								i=j;
								isArray=false;
								break;
							}
							else{
								if(sb.length()>0){
									sb.append(" "+key);
								}
								else{
									sb.append(key);
								}
							}
						}
						if(isArray&&j==num+i){
							map.put(sb.toString(), map.getOrDefault(sb.toString(), 0) + weight);
						}
					}
				}
			}
		}
		br.close();
		return getOrderTopTenMap(map,lines);
	}

七、单元测试

  • 测试程序实例
@Test
    public void test1(){
        String input_path="src\\input1.txt";
        try{
            //基本需求
            Lib lib=new Lib();
            String contentStr=lib.getContentAndLinesFromFilePath(input_path);
            List<String> wordList=lib.splitStringIntoList(contentStr);
            Integer lines=lib.getLineCountFromFilePath(input_path); //有效行数
            Integer characters=lib.countLetters(input_path);    //字符数
            Integer words=wordList.size();  //单词总数
            Map<String,Integer> map=lib.getTopTenWords(wordList,10);
            String topWords ;//频率最高单词
            for(String key:map.keySet()){
                topWords="<"+key+">: "+map.get(key)+"\r\n";
            }
            String testChar="characters: 102";
            String testWords="words: 2";
            String testLines="lines: 2";
            String testTopWords="<abcdefghijklmnopqrstuvwxyz>: 2";
            assertEquals(testChar,characters);
            assertEquals(testWords,words);
            assertEquals(testLines,lines);
            assertEquals(testTopWords,topWords);
        }
        catch (Exception e){
            e.printStackTrace();
        }
    }
  • 部分测试数据
 !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~
	
\t\n


测试结果:
characters: 102
words: 2
lines: 2
<abcdefghijklmnopqrstuvwxyz>: 2
abcdefghijklmnopqrstuvwxyz
1234567890
,./;'[]\<>?:"{}|`-=~!@#$%^&*()_+

测试结果:
characters: 76
words: 1
lines: 3
<abcdefghijklmnopqrstuvwxyz>: 1
  • 测试思路:设定不同的临界输入,如:空白文件、文件不存在、以单词数字开头等情况,提前判断得出测试结果,通过对不同input文件进行算法运算获得输出,判断与预想结果是否一致。具体测试内容回见“四-2”。

八、困难与解决方法

  • 在最初的需求分析上花费了较长的时间,一些需求没有分析到位导致后来在实现过程中出现遗漏,再补充起来就需要对已实现的代码进行修改。下次在动手前还是需要和队员更加深入的进行讨论。
  • 一些以前没接触过的知识在此次编程中需要及时学习,如爬虫的使用和算法的性能分析。在及时上网,交流掌握技能的同学,渐渐学到新的实用技能。

九、项目小结

1、队友评价

  • 221600416-黄海山

在和德泉队友合作的过程中,我觉得我的队友是一个性格温和,擅于沟通交流的人,不管是阅读文档还是沟通理解的能力都是极强,最让我感动的是,我的队友责任心极强,在自己还有许多学生工作要完成的同时,还能够熬夜写文档,及时并且高质量地完成作业任务,这实在是一种难能可贵的品质。总的来说,这是一次愉快的合作经历。

  • 221600415- 傅德泉

我的队友黄海山,编程能力极强,完成了此次作业主要程序的撰写。在需求分析阶段,能较好的相互交流,尊重彼此的看法,使得后边的工作进展顺利。最后在文档撰写环节,也能认真交流算法的实现过程,为后期测试打好基础,总而言之合作过程十分顺利。

2、个人心得

  • 221600416-黄海山

在这次作业开始之前,我曾被这次作业如此多的需求和所要完成的巨大工作量所困惑,但是困难摆在眼前,我顾不得去抱怨什么,更加不能因为有困难而轻易放弃,于是我不得不开始认真思考如何着眼去解决这个问题,经过对作业需求认真地阅读和分析后,我发现这次的作业看似繁多的需求其实是由一个个小问题组成的,而有一部分问题的解答是有重复的,比如作业中对单词频率的统计,单词权重的统计,数组频率的统计,数组权重的统计都可以归类为一个map排序的问题,而其他诸如单词,字符的统计等都是熟悉的文件读写问题,经过如此分析后,就大大减少了原来的工作量。经过一天多的认真编程后,我和队友完成了对作业需求的实现和初步测试,又经过一段时间的修改和测试,认真比对作业需求,终于完成了这次作业。在编写这次作业的过程中,我最大的收获就是加强了阅读文档的能力,拆分复杂问题的能力。当然,在和队友的讨论过程中,也加强了沟通交流的能力。

  • 221600415-傅德泉

此次作业需求量大,锻炼到了自身需求分析的能力。在于队友的沟通交流中,发现自己对于代码的理解能力也有所提升,同时还接触了代码测试的较专业的方式。此外,回顾了类图和流程图的绘制方法,更加深入的掌握了该项能力。在撰写文档的过程中,体会到了一个程序的全面性,应该从多个地方进行考评。

posted @ 2019-03-15 22:46  固态咀嚼性胶基糖果  阅读(233)  评论(2编辑  收藏  举报