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

所属课程 软件工程1916|W(福州大学)
作业要求 结对第二次—文献摘要热词统计及进阶需求
结对学号 221600327221600329
基本需求Github项目地址 PairProject1-Java
进阶需求Github项目地址 PairProject2-Java
作业目标 运用结对编程完成作业,增强团队协作能力
参考文献 《构建之法》、《数据结构与算法分析 Java语言描述》

PSP表格:

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

目录

任务1、WordCount基本需求

1.1、PSP表格

1.2、需求分析

(一)成品:wordCount的命令行程序

(二)功能性需求:统计input.txt中的以下几个指标

  1. 统计文件的字符数
    • 只需要统计Ascii码,汉字不需考虑
    • 空格,水平制表符,换行符,均算字符
  2. 统计文件的单词总数,单词:至少以4个英文字母开头,跟上字母数字符号,单词以分隔符分割,不区分大小写
    • 英文字母: A-Z,a-z
    • 字母数字符号:A-Z, a-z,0-9
    • 分割符:空格,非字母数字符号
    • 例:file123是一个单词,123file不是一个单词。file,File和FILE是同一个单词
  3. 统计文件的有效行数:任何包含非空白字符的行,都需要统计。
  4. 统计文件中各单词的出现次数,最终只输出频率最高的10个。频率相同的单词,优先输出字典序靠前的单词。
  5. 按照字典序输出到文件result.txt:例如,windows95,windows98和windows2000同时出现时,则先输出windows2000
    • 输出的单词统一为小写格式
  6. 输出的格式为

characters: number
words: number
lines: number
: number
: number
...

(三)非功能性需求

1.接口封装

  • 统计字符数 ;
  • 统计单词数 ;
  • 统计最多的10个单词及其词频 ;

1.3、解题思路

1.3.1统计字符数:

我理解的input.txt的内容应该是,文件中只包含ascii码以及空格,水平制表符,换行符这些字符。所以想直接用String存储文件内容,用String.length()来得到字符数。但是这个会有一个问题,就是在windows平台下,换行符是用"\r\n",String.length()会统计为两个字符,用“\n”代替“\r\n”,即可解决问题。

1.3.2统计单词数:

文件由两种字符分割符(空格,非字母数字符号)和非分隔符构成,单词统计与非分割符无关,所以直接为了接下来split方便,将所有的分割符都替换为“|”,第二步分割字符串,第三步,使用正则表达式匹配题目所规定的单词,统计单词数。

1.3.3统计有效行数:

一开始我想的统计行数是使用readline();去读出每一行,然后逐行判断这行有没有ascii码值<32的字符存在,因为ascii码值小于32的是在文本中不显示的,所以我开始的想法就是这样的;不过试了一下之后发现对于"\r\n"之类的字符和空白行单靠ascii码是区分不出来的,然后在网上找了资料之后,发现String.trim().isEmpty()是可以去除空白行然后统计的,最后使用的是这个方法。

1.3.4统计各单词的出现次数,输出频率Top10的单词:

因为处理场景为单机,所以决定选用散列表,顺序扫描刚刚的到的单词数组,当扫描到某个关键词时,就去散列表里查询。如果存在,就将对应的次数加一;如果不存在,就将他插入到散列表,并记录为1,以此类推,遍历完后,散列表中就存储了不重复的单词以及其出现的次数。
然后就是求Top10,建立一个大小为10的小顶堆,遍历散列表,依次取出每个单词及对应出现的次数,然后与堆顶的单词比较。如果出现次数比堆顶单词的次数多,就删除堆顶单词,将这个次数更多的关键词加入到堆中,以此类推,遍历完散列表,堆中的单词就是出现次数top10的单词。然后排序输出。

1.4、设计过程

1.4.1代码组织&接口设计:

  • bean包:Word类

  • unitl包:BasicWordCount类、IOUnitls类

    • BasicWordCount类:

      • 统计字符数函数 :

        /**
        * 计算文件字符数
        * @param fileName 文件名
        * @return long 字符数
        */
        public long characterCount(String fileName){}
        
      • 统计行数 :

        /**
        * 计算文件行数
        * @param fileName 文件名
        * @return long 行数
        */
        public long lineCount(String fileName){}
        
      • 统计单词数 :

        /**
        * 计算文件单词数
        * @param fileName 文件名
        * @return long 单词数
        */
        public long wordCount(String fileName){}
        
      • 统计词频top10的10个单词及其词频 :

        /**
        * 计算出现次数top10的单词及其词频
        * @param fileName 文件名
        * @return Word[] 单词数组
        */
        public Word[] topTenWord(String fileName) {}
        
    • IOUnitls类:

      • 读文件

        /**
        * 读入指定文件名的文件数据
        * @param fileName 文件名
        * @return BufferedReader
        * @throws IOException 
        */
        public static BufferedReader readFile(String fileName){}
        
      • 写文件

        /**
        	 * 将制定字符串输出到result.txt
        	 * @param fileContent 字符串
        	 */
        	public static void writeFile(String fileContent, String fileName) {}
        

1.4.2关键代码及其流程图(词频Top10单词统计):

解题思路
流程图:

代码:

//进行统计的散列表
public Word[] topTenWord(String fileName) {
    ...//省略读取文件、将文本处理成单词数组的过程
    	Map<String, Integer> countMap = new HashMap<>();
    	for(int i = 0; i < splitStrings.length; i++) {
    		if(Pattern.matches(regex, splitStrings[i])) {
    			Integer outValue = countMap.get(splitStrings[i]);
    			if (null == outValue) {
    			      outValue = 0;
    			}
    			outValue++;
    			countMap.put(splitStrings[i], outValue);
    		}
    	}
    	//求top10
    	PriorityQueue<Word> topN = new PriorityQueue<>(10, comp);
        Iterator<Map.Entry<String, Integer>> iter = countMap.entrySet().iterator();
        Map.Entry<String, Integer> entry;
        while (iter.hasNext()) {
          entry = iter.next();
          if (topN.size() < 10) {
            topN.offer(new Word(entry.getKey(), entry.getValue()));
          } else {
            // 如果当前数据比小顶堆的队头大,则加入,否则丢弃
            if (topN.peek().getCountNum() < entry.getValue()) {
              topN.poll();
              topN.offer(new Word(entry.getKey(), entry.getValue()));
            }
          }
        }
        //结果集
        Word[] result = null;
        int wordCount = countMap.size();
        if(wordCount < 10) {
        	result = new Word[(int) wordCount];
        }else {
        	result = new Word[10];
        }
        topN.toArray(result);
        //对top10单词排序
        Arrays.sort(result, comp);
    	return result;
}

复杂度分析:遍历散列表需要 O(n) 的时间复杂度,一次堆化操作需要 O(logK) 的时间复杂度,所以最坏情况下,n个元素都入堆一次,所以最坏情况下,求TopK的时间复杂度是O(nlogk)。

1.5、性能分析

从CPU Call Tree图可以看出,耗时主要是在对字符串的分割和单词的正则匹配。

任务2、WordCount进阶需求

2.1、PSP表格

2.2、需求分析

2.2.1 成品:

①程序一:爬取CVPR2018论文到本地result.txt的命令行程序

②程序二:支持命令行参数的wordCount命令行程序

2.2.2功能性需求

(1)使用工具爬取论文信息**输出到result.txt文件
(2)自定义输入输出文件**
        WordCount.exe -i [file] -o [file]
(3)加入权重的词频统计**
         WordCount.exe -w [0|1]
(4)新增词组词频统计功能**
        WordCount.exe -m [number]
(5)自定义词频统计输出**
        WordCount.exe -n [number]
(6)多参数的混合使用**
        WordCount.exe -i input.txt -m 3 -n 3 -w 1 -o output.txt

2.3、解题思路

2.3.1 爬取CVPR论文数据

要从网页上爬取数据,就得分析网页HTML源代码构成,从CVPR2018首页的HTML代码可以看出,在首页的论文列表中,每一篇的论文标题在HTML代码上体现为如下形式:

<dt class="ptitle"><a href="xxx.html">论文标题</a></dt>

其中<a></a>标签里的href属性为论文详情页的网页地址。

由此我们可以获取每篇论文详情页的url

第一步就是在网页的DOM树中提取所有的<dt class="ptitle">的节点。
第二步从每个<dt class="ptitle">的节点中提取出<a>标签的href属性。

接着分析论文详情页的HTML代码,

我们可以发现,在详情页源代码中,论文标题的html代码长这个样子:

<div id="papertitle">论文标题</div>

论文摘要的html代码长这个样子:

<div id="abstract">这是论文摘要</div>

由此我们可以获取每篇论文的标题和摘要

第一步从网页的DOM树中找到<div id="papertitle">的节点,获取他的文本即为paper title。
第二步从网页的DOM树中找到<div id="abstract">的节点,获取他的文本即为paper abstract。

2.3.2 自定义参数

自定义参数的实现是先设计一个CommandLine类,把作业中的参数作为类的私有成员,然后从String []args中读取命令行参数,存进CommandLine类中,再使用getxxxx()公有函数取出相应的参数就可以使用自定义参数了。

2.3.3 加入权重的词频统计:

从爬取的论文数据可知,title独占一行,abstract独占一行,所以可以按行处理数据。如果-w 参数为1,修改基本需求的文件,当一行的开头是title:,那么这一行的单词的权重为10。其他的一律权重为1。

2.3.4 词组词频统计:

因为词组不能跨越Title、Abstract,而且需求给定Title和Abstract都是独占一行的,所以按行读取。

然后构造一个队列,对读取的一行开始扫描,接下来的判断逻辑如下图:

扫描完后,对获得的散列表进行topK操作,即可。

2.3.4 自定义词频统计

这个很简单,基本需求规定的是top10,而这里我们只要从参数获取topK的值,传入参数,遍历单词列表,然后维护一个大小为K的小顶堆即可。

2.4、设计过程

2.4.1 代码组织和接口设计

  • bean包:Word类、CommandLine类

  • unitl包:AdvancedWordCount类、Command类、IOUnitls类

    • AdvancedWordCount类:

      • 统计字符数函数 : 同基本需求

      • 统计行数 : 同基本需求

      • 统计单词数 : 同基本需求

      • 统计单词词频topK的K个单词及其词频 :

        /**
        * 计算单词的(加权)词频和topK
        * @param fileName 文件名
        * @param topK 词频前K的单词
        * @param isWeight 是否启用加权计算
        * @return Word[] 有序单词数组
        */
        public Word[] topKWordWeighting (String fileName, int topK, boolean isWeight) {}
        
        
      • 统计词组词频topK的K个词组及其词频 :

        /**
        * 计算词组的(加权)词频和topK
        * @param fileName 文件名
        * @param groupNum 几个单词为一个组
        * @param topK 词频前K的词组
        * @param isWeight 是否启用加权计算
        * @return Word[] 有序单词数组
        */
        public Word[] topKWordsWeighting (String fileName, int groupNum,int topK, boolean isWeight) {}
      
      
    • Command类:

      • /**
        * 解析命令行参数
        * @param args 参数
        * @return CommandLine对象
        */
        public CommandLine ParseCommand(String[] args) {}
        
        
    • IOUnitls类(同基本需求)

2.4.2 关键代码(词组词频统计之词组划分):解题思路

...
			//进行统计的散列表
			Map<String, Integer> countMap = new HashMap<>();
			while((line = bufferedReader.readLine()) != null) {
				String regexTile = "Title: .*";
				int weight = 1;
				if(Pattern.matches(regexTile, line)) {
					line = line.replaceAll("Title: ", "");
					weight = 10;
				}else {
					line = line.replaceAll("Abstract: ", "");
					weight = 1;
				}
				line = line.toLowerCase();
				//按字符分析
				int groupnumber = 0;//词组单词个数
				int wordStratPosition = 0;//单词开始下标
				int wordEndPosition = 0;//单词开始下标
				int flagPosition = 0;//标记词组第一个单词的结束位置
				Queue<String> wordReadyQueue = new LinkedList<String>();
				for(int i = 0; i < line.length()-3;) {
					boolean flag = false;
					
					if(Character.isLetter(line.charAt(i))) {
						if(Character.isLetter(line.charAt(i+1))) {
							if(Character.isLetter(line.charAt(i+2))) {
								if(Character.isLetter(line.charAt(i+3))) {
									wordStratPosition = i;
									for(int j=i+4; j < line.length(); ++j) {
										if(!Character.isLetterOrDigit(line.charAt(j))) {
											wordEndPosition = j;
											//得到一个单词,加入到队列
											wordReadyQueue.add(line.substring(wordStratPosition, wordEndPosition));										
											groupnumber++;//词组单词数+1
											if(groupnumber == 1) {
												flagPosition = j;
											}
											i=j;
											flag = true;
											break;
										}else if((j+1)==line.length()) {
											wordEndPosition = j+1;
											//得到一个单词,加入到队列
											wordReadyQueue.add(line.substring(wordStratPosition));
											groupnumber++;//词组单词数+1
											if(groupnumber == 1) {
												flagPosition = j;
											}
											i=j;
											flag = true;
											break;
										}
									}
								}else {
									if(!Character.isLetterOrDigit(line.charAt(i))) {
										wordReadyQueue.add(line.charAt(i)+"");
									}else {
										while(wordReadyQueue.poll()!=null) {}//清空队列

										while(Character.isLetterOrDigit(line.charAt(i++))) {
											if(i >= line.length()){
												break;
											}
										}
										groupnumber = 0;
										flag = true;
									}
								}
							}else {
								if(!Character.isLetterOrDigit(line.charAt(i))) {
									wordReadyQueue.add(line.charAt(i)+"");
								}else {
									while(wordReadyQueue.poll()!=null) {}//清空队列

									while(Character.isLetterOrDigit(line.charAt(i++))) {
										if(i >= line.length()){
											break;
										}
									}
									groupnumber = 0;
									flag = true;
								}
							}
						}else {
							if(!Character.isLetterOrDigit(line.charAt(i))) {
								wordReadyQueue.add(line.charAt(i)+"");
							}else {
								while(wordReadyQueue.poll()!=null) {}//清空队列

								while(Character.isLetterOrDigit(line.charAt(i++))) {
									if(i >= line.length()){
										break;
									}
								}
								groupnumber = 0;
								flag = true;
							}
	
						}
					}else {
						if(!Character.isLetterOrDigit(line.charAt(i))) {
							if(groupnumber!=0) {
								wordReadyQueue.add(line.charAt(i)+"");
							}
						}else {
							while(wordReadyQueue.poll()!=null) {}//清空队列
							while(Character.isLetterOrDigit(line.charAt(i++))) {
								if(i >= line.length()){
									break;
								}
							}
							groupnumber = 0;
							flag = true;
						}
					}
					if(groupnumber == groupNum) {//达到要求个数,加入散列表
						String wordGroup = "";
						String wordOfQueue = wordReadyQueue.poll();
						while(wordOfQueue != null) {
							wordGroup = wordGroup + wordOfQueue;
							wordOfQueue = wordReadyQueue.poll();
						}
						groupnumber = 0;
                        //加权
						Integer outValue = countMap.get(wordGroup.toString());
						if (null == outValue) {
						      outValue = 0;
						}
						if(isWeight) {
							outValue += weight;
						}else {
							outValue++;
						}
						countMap.put(wordGroup.toString(), outValue);		
						i = flagPosition;
					}
					if(!flag) {
						++i;
					}
				}
			}
			...

2.4.3 关键代码(爬取论文标题和摘要):解题思路

因为之前写安卓应用曾经用户jsoup抓取教务系统上的成绩等数据,这次自然而然使用了jsoup来对论文进行爬取。代码如下:

        /**
		 * 从cvpr网站爬取论文数据,并写入result.txt
		 * @param URL
		 */
	    public static void getFile(String URL) {
	    	BufferedWriter bufferedWriter = null;
	        try {
	            File outputFile = new File("result.txt");
	            bufferedWriter = new BufferedWriter(new FileWriter(outputFile));
	            //get方式得到HTML数据
	            //默认设置下,jsoup超时时间为3秒,鉴于当前网络环境,修改为10秒
	            //默认设置下,jsoup最大获取的长度只有1024K,设置maxBodySize(0),可不限长度
	            Document doc = Jsoup.connect(URL).timeout(10000).maxBodySize(0).get();
	            //从HTML中选择所有class=ptitle的节点
	            Elements paperList = doc.select("[class=ptitle]");
	            //从ptitle节点中选择a标签的href属性值
	            Elements links = paperList.select("a[href]");
	            int count = 0;
	            //分别当问每篇论文的详情页
	            for (Element link : links) {
	                //论文详情页URL
	                String url = link.attr("abs:href");
	                Document paperDoc = Jsoup.connect(url).timeout(10000).maxBodySize(0).get();
	                //获取论文title
	                Elements paperTitle = paperDoc.select("[id=papertitle]");
	                String title = paperTitle.text();
	                //获取论文Abstract
	                Elements paperAbstract = paperDoc.select("[id=abstract]");
	                String abstracts = paperAbstract.text();
	                //数据写入文件
	                bufferedWriter.write(count++ + "\r\n");
	                bufferedWriter.write("Title: " + title + "\r\n");
	                bufferedWriter.write("Abstract: " + abstracts + "\r\n\r\n\r\n");
	            }
	        } catch (Exception e) {
	        	System.out.println("获取论文数据失败");
	            e.printStackTrace();
	        }finally {
	        	try {
				if(bufferedWriter != null) {
					bufferedWriter.close();
				}
	        	}catch (Exception e) {
		        	System.out.println("获取论文数据失败");
					e.printStackTrace();
				}
			}
	    }

2.5、性能分析及改进

2.5.1、性能分析

考虑到进阶的数据量可能比较大,在压力测试时,我们用了近60M的txt(近6000万的字符)文件进行测试。内存占用在1G左右,由于复用基本需求的接口,在性能测试时,依然是对字符串的分割(split)耗时最多占用内存很大,原因是因为原来的代码是把这6000万字符存放到字符数组里,在进行合法单词的判断。


2.5.2、改进方法

在统计单词时,直接遍历一遍文本数据,识别单词的start和end的下标,直接截取单词进行识别。代码如下:

	for(int i = 0; i<updateString.length();) {
		startPosition = i;
		endPosition = i;
		while(Character.isLetterOrDigit(updateString.charAt(i++))) {
			endPosition++;
		}
		if(Pattern.matches(regex, updateString.substring(startPosition, endPosition))) {
			countOfWord++;
		}
	}

2.6、单元测试

我们用的是Eclipse中的JUnit4进行的测试,我们总共设计了16个测试单元,其中字符计数,词计数,行计数各三个,进阶单词和词频各两个。测试用例都是根据作业要求设计的各种字符混杂,有空白行,数字字母混杂的形式进行测试,测试结果都显示我们的程序满足了题目的要求。

单元测试 测试覆盖 测试代码块 测试个数
BasicWordCountTest.testCharacterCount() 普通字符、空格、各种符号 BasicWordCountTest.CharacterCount() 3
BasicWordCountTest.testWordCount() 普通字符、空格、各种符号,字母大小写,数字与字母各种组合 BasicWordCountTest.WordCount() 3
BasicWordCountTest.testLineCount() 空白行、非空白行 BasicWordCountTest.LineCount() 3
BasicWordCountTest.testTopTenWord() 普通字符、空格、各种符号,字母大小写,数字与字母各种组合 BasicWordCountTest.TopTenWord() 3
AdvancedWord.TesttopWordWeighting() 混合单词 AdvancedWord.topWordWeighting() 2
AdvancedWord.TesttopWordsWeighting() 混合词组 AdvancedWord.topWordsWeighting() 2

部分测试代码

package Untils;

import static org.junit.Assert.*;

import org.junit.Test;

public class BasicWordCountTest {
	public static BasicWordCount basic=new BasicWordCount();
	static String fileName="testinput2.txt";
	String []testTopWord= {"abcd123","here","your","aaaa","abss","bbbb","cccc","ddda","dera","esds"};
	int []testTopWordCount= {7,2,2,1,1,1,1,1,1,1};
	@Test
    //测试字符统计
	public void testCharacterCount() {
		System.out.println(basic.characterCount(fileName));
		assertEquals(189,basic.characterCount(fileName));
	}
	//测试单词统计
	@Test
	public void testWordCount() {
		basic.wordCount(fileName);
		assertEquals(21,basic.wordCount(fileName));
	}

	@Test
	//测试top10单词
	public void testTopTenWord() {
		basic.topTenWord(fileName);
		for(int i=9;i>0;i--) {
			System.out.println(testTopWord[9-i]+"=="+i+"=="+basic.topTenWord(fileName)[i].getKey());
			assertEquals(testTopWord[9-i],basic.topTenWord(fileName)[i].getKey());
			assertEquals(testTopWordCount[9-i],basic.topTenWord(fileName)[i].getCountNum());
		}
	}

	@Test
	//测试行数
	public void testLineCount() {
		basic.lineCount(fileName);
		assertEquals(4,basic.lineCount(fileName));
	}
}


其中一个测试结果截图

在JUnit4的测试下,我们的总体代码覆盖率在80%左右,其中Main.java类是用来输出结果的类,所以没有加入测试,还有一些原因是很多的异常处理是没有办法触发的,这些代码没有覆盖住。不过我们主要代码的覆盖率都在90%左右,甚至以上的,所以说我们的测试结果还是对程序正确性做了有力验证的。

三、其他

3.1、代码规范:

​ 码出高效 :阿里巴巴Java开发手册终极版v1.3.0

3.2、分工情况

221600327:

​ 行数统计、命令行参数解析、输入输出模块代码编写、单元测试、文档编写

221600329:

​ 字符统计、单词统计、词频统计模块代码编写、爬虫设计和实现、性能测试、文档编写

3.3、Github的代码签入记录

3.4、遇到的代码模块异常或结对困难及解决方法

本来我们认为的词组词频统计会过滤掉单词间的非法字母数字字符,而后来助教重申了需求,要求输出这些字符。因为原先的做法是先把非法字母数字字符都替换掉,然后再进行操作。这需求一改,不就全凉了。后面只能一个字符一个字符的遍历,再截取单词。但是最后发现在数据量大的时候,不采用split去分割字符串,直接遍历反而速度更快,而且更省内存。

3.5、评价队友

3.5.1 221600329 评价 221600327

队友给够按时完成分配的任务,有耐心,可能基础不是特别扎实,但是肯学肯做,这点事值得肯定的。

需要改进的地方:编码能力需要加强。

3.5.2 221600327评价221600329

我的队友在我们结对中大部分时间是驾驶员的角色,他对于需求的分析能很快划分成若干个字模块,在编程方面他数据结构和算法方面的基础知识相当扎实,编码实现能力也很强。我最欣赏的是我的队友认真和仔细的态度,他对作业近乎追求完美,在满足正确性之后还尽量去优化性能;还有他的坚定的信念,就算他在这周身体不舒服,每天狂咳嗽,但是他还是没有因此落下过一点进度,这是我很敬佩的。

需要改进的地方:在任务分配方面可以均衡一些,不然会累着他自己。

3.6、 感想

这次作业碰上了我重感冒的时候,一直咳嗽不止(不敢熬夜了),再加上自己编程能力不突出,所以时间上安排的不好,对于附加部分的需求,没有时间去考虑和设计。这两次都是结对任务,我和队友之间的协作能力有了进步,但是目前还是没有很高的效率,可能还没度过学习阶段,依然处于磨合阶段。

3.7、学习进度

这两周的任务都是结对任务,所以以此为契机我们阅读了《构建之法》第四章两人合作,了解到结对编程是极限编程这一思想的具体体现,结对编程有三种形式:

①键盘鼠标式②Ping-pong式③领航员-驾驶员式。

了解到结对编程,因为在结对编程过程中有随时的复审和交流,可以减少犯错,提高解决问题的效率,形成知识传递。

了解到要做好结对编程,需要遵守相同的代码规范,在不同的阶段,不同的人之间要有不同的方式,而且要养成个人良好的生活习惯。

但是我觉得虽然结对编程有很多好处,但是结对编程的两个队友,编程水平不能相差过大,不然可能会造成交流变成了教学,浪费更多的时间,影响效率。

posted @ 2019-03-15 22:39  oirving  阅读(187)  评论(2编辑  收藏  举报