软件工程—WC功能实现 (JAVA)
软件工程—WC功能实现(JAVA)
Github项目地址:https://github.com/Ousyoung/wc
项目要求
wc.exe 是一个常见的工具,它能统计文本文件的字符数、单词数和行数。这个项目要求写一个命令行程序,模仿已有wc.exe 的功能,并加以扩充,给出某程序设计语言源文件的字符数、单词数和行数。实现一个统计程序,它能正确统计程序文件中的字符数、单词数、行数,以及还具备其他扩展功能,并能够快速地处理多个文件。
基本要求
-c [filename]统计文件字符数
-w [filename]统计文件词数
-l [filename]统计文件行数
扩展功能
-s [filename]递归处理目录下符合条件得文件
-a [filename]返回文件代码行 / 空行 / 注释行
支持各种文件的通配符(*,?)
高级功能
-x 图形化界面(未实现)
PSP
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | 40 | 60 |
· Estimate | · 估计这个任务需要多少时间 | 40 | 60 |
Development | 开发 | 850 | 1400 |
· Analysis | · 需求分析 (包括学习新技术) | 60 | 120 |
· Design Spec | · 生成设计文档 | 40 | 120 |
· Design Review | · 设计复审 (和同事审核设计文档) | 40 | 60 |
· Coding Standard | · 代码规范 (为目前的开发制定合适的规范) | 20 | 60 |
· Design | · 具体设计 | 80 | 120 |
· Coding | · 具体编码 | 500 | 800 |
· Code Review | · 代码复审 | 40 | 30 |
· Test | · 测试(自我测试,修改代码,提交修改) | 70 | 90 |
Reporting | 报告 | 80 | 90 |
· Test Report | · 测试报告 | 40 | 50 |
· Size Measurement | · 计算工作量 | 10 | 20 |
· Postmortem & Process Improvement Plan | · 事后总结, 并提出过程改进计划 | 30 | 20 |
合计 | 970 | 1550 |
解题思路
此次项目是模拟WC统计文件字,词,行信息,很容易想到用JAVA中的I/O流来读取文件信息,对于读取文件字符数以及行数,这些都比较简单,字符数只需读出字符匹配不是换行符即可,行数只需没读到换行符就加一,基本功能当中就属词的统计较为不易,词的界定相对来说要模糊的多,所以想到需要用正则表达式来匹配字符串,替换掉一些分隔符号,继而使词的匹配简单化。
扩展功能方面,无非是在以上基础上再多加细分和应用。代码行/空行/注释行同样需要使用正则表达式来进行匹配,其界定一开始自己也很苦恼,但上网查阅资料,弄明白后也就迎刃而解了;至于文件的递归查找,只需要设计一个递归方法,递归查找指定目录下所有文件名,再通过用户键入的关键字作为方法参数,用contains(string)方法检索出符合条件的文件即可。
设计实现
此次项目中只写了两个类,一个是存放各种统计方法的WC类,另一个是运行程序的主类Main类,用于启动程序,里面还包括了命令获取,命令匹配的代码。
运行程序,main方法启动,系统显使用说明,再通过用户键入得到命令,考虑到项目要求的命令显示,需要把命令拆分成两部分:功能选择function以及文件路径path,继而需要调用split(" ")方法,用空格分割命令得到子命令,再用多个if语句的嵌套,匹配调用WC类中不同的静态方法,将path作为参数输入,找到指定文件。
设计主类Main类中的静态方法:词统计wordsCount,代码行,空行统计complexCount,文件递归处理fileHandle 等(只列举了部分,详见代码说明)
类,方法调用关系图如下
代码说明
main方法
数据的输入输出处理,命令的提取,匹配都在这一部分,下面主要展示命令的提取,匹配部分。这里使用nextLIne方法获取命令,再用split分割命令,得到子命令,再进行匹配;外层使用whlie循环访问,直至用户键入退出命令 -esc 退出
while (true) {
Scanner getIn = new Scanner(System.in);
if (getIn.hasNext())
command = getIn.nextLine(); // 获取指令
if (command.equals("-esc")) {
System.out.println("***WC.exe程序已关闭");
System.exit(0); // 关闭程序
} else {
String[] commands = new String[2]; // 字符串数组用于存放指令
commands = command.split(" ", 2); // 拆分用户输入的指令
function = commands[0]; // command前半部分为功能选择
path = commands[1]; // 后半部分为文件路径
// Mark:修改代码时出现了数组下标越界的问题*****************
if (function.equals("-c"))
WC.charsCount(path);
else if (function.equals("-w"))
WC.wordsCount(path);
else if (function.equals("-l"))
WC.rowsCount(path);
else if (function.equals("-a"))
WC.complexCount(path);
else if (function.equals("-all"))
WC.allCount(path);
else if (function.equals("-s")) {
System.out.print("请输入文件迭代查找的关键字:\n");
Scanner getIn2 = new Scanner(System.in);
// 需要拿到指定字符串,这里的方法需要两个参数
WC.fileHandle(path, getIn2.nextLine());
} else
System.out.println("用户输入不合法,请重新输入!");
}
}
WC类
统计字符 charsCount
比较简单,所以只展示关键代码
try {
int tempchar;
readFile = new InputStreamReader(new FileInputStream(file));
while ((tempchar = readFile.read()) != -1) {
if ((char) tempchar != '\r' && (char) tempchar != '\n') {
// 只要匹配的不是换行符,字符数就加一
charsNum++;
}
}
readFile.close();
System.out.println("文件路径:" + fileName + " 文件的字符数为: " + charsNum);
}
统计行 rowsCount
同样比较简单,所以只展示关键代码
try {
readFile = new InputStreamReader(new FileInputStream(file));
int tempchar;
while ((tempchar = readFile.read()) != -1) {
if ((char) tempchar == '\n') {
// 匹配到换行符,行数就加一
lineNum++;
}
}
System.out.println("文件路径:" + fileName + " 文件的行数为: " + lineNum);
readFile.close();
}
统计词 wordsCount
使用正则表达式,匹配字符,将文件字符串中的 . * " " / 等符号用reaplaceAll方法替换成空格,再用split方法用空格分割字符串,得到词
static void wordsCount(String fileName) throws IOException {
// 返回该文件的词的数目
File file = new File(fileName);
BufferedReader bur = null;
String string = "";
String line;
int wordNum = 0;
String words[] = null;
try {
bur = new BufferedReader(new FileReader(file));
while ((line = bur.readLine()) != null) {
String s = line.replaceAll("[\\p{Punct}\\s\\p{Nd}\\uffe5\\u4e00-]", " ");
// 得到除字符外全是空格的文本
string = string + s + " ";
// 这里要加空格,否则string每次增长,行末的词会和下一行首词相连
}
words = string.split(" ");
// 去除空格,得到词的字符串数组
wordNum = words.length;
System.out.println("文件路径:" + fileName + " 文件的词数为: " + wordNum);
bur.close();
} catch (FileNotFoundException e) {
System.out.println("***系统提示:找不到指定的文件!请重新输入:");
}
}
统计代码行,注释行等 complexCount
使用正则表达式匹配相应行:“((//)|(/+)|((\s))|((\s)+/))+” 注释行,"^\s$"空白行,"(?!import|package).+;\s(((//)|(/+)).)" 代码行
static void complexCount(String fileName) throws IOException {
// 返回该文件的代码行 / 空行/注释行
File file = new File(fileName);
BufferedReader bufr = null;
String line = null;
int codeLine = 0;
int blankLine = 0;
int noteLine = 0;
Pattern codePattern = Pattern.compile("(?!import|package).+;\\s*(((//)|(/\\*+)).*)*",
Pattern.MULTILINE + Pattern.DOTALL);
// 匹配代码行 // Mark:匹配代码曾行时出现了错误,还有优化空间*********
Pattern blankPattern = Pattern.compile("^\\s*$");
// 匹配空白行
Pattern notePattern = Pattern.compile("((//)|(/\\*+)|((^\\s)*\\*)|((^\\s)*\\*+/))+",
Pattern.MULTILINE + Pattern.DOTALL);
// 匹配注释行
try {
bufr = new BufferedReader(new InputStreamReader(new FileInputStream(file)));
while ((line = bufr.readLine()) != null) {
if (codePattern.matcher(line).matches())
codeLine++;
if (blankPattern.matcher(line).find())
blankLine++;
if (notePattern.matcher(line).find())
noteLine++;
}
System.out.println(
"文件路径:" + fileName + " 文件的代码行 / 空行 / 注释行数为: " + codeLine + "/" + blankLine + "/" + noteLine);
bufr.close();
} catch (FileNotFoundException e) {
System.out.println("***系统提示:找不到指定的文件!请重新输入:");
}
}
}
文件递归处理 fileHandle 及 allCount
递归返回目录下所有文件名,再调用String类的contains()方法模糊匹配所有文件名含指定字符串str的文件,再调用allCount方法返回各个文件的词,字符,行信息
static void fileHandle(String fileName, String str) throws IOException {
// 文件迭代处理
List<File> fileList = new ArrayList<File>();
File file = new File(fileName);
File[] files = file.listFiles();
// 获取目录下的所有文件或文件夹
if (files == null) {
// 如果目录为空,直接退出
System.out.println("***系统提示:找不到指定路径!请重新输入:");
}
// 遍历,目录下的所有文件
for (File f : files) {
if (f.isFile()) {
fileList.add(f);
} else if (f.isDirectory()) {
System.out.println(f.getAbsolutePath());
fileHandle(f.getAbsolutePath(), str);
}
}
for (File f1 : fileList) {
if (f1.getName().contains(str) == true) {
// 文件名包含指定字符串,contains()方法值为true
WC.allCount(fileName + "/" + f1.getName());
// f1.getName()只是文件名,这里要在f1.getName()前加入作为参数传入的fileName路径,否则会找不到文件
}
}
// System.out.println("测试代码"); 左侧为测试用代码
}
static void allCount(String fileName) throws IOException {
// 返回详细信息
WC.charsCount(fileName);
WC.wordsCount(fileName);
WC.rowsCount(fileName);
WC.complexCount(fileName);
System.out.println("\n");
}
运行测试
-c -w -l -a 测试
由于我写了一个allCount方法(命令 -all ),相当于一次执行-w -c -l -a ,所以对于以上命令只做部分测试
测试 oneline.txt
测试readme.txt
-all 测试 readme.txt文件
-s 测试 检索test目录下文件名含one的文件(onechar, oneword ,oneline,one)
递归处理 .java源代码文件测试
注:(上面截图第二行文字中“迭代查找”应为“递归查找”,截图的时候没注意,写错了......代码中已更改)
代码覆盖率
项目小结
此次项目是软件工程的第一次项目,总的来说感觉自己做的有些仓促,许多细节的地方还有优化的空间,比如正则表达式匹配词那里,对于一些中文词没有考虑在内;还有main方法里完全可以写一个匹配方法来处理用户命令,是代码结构看起来更加合理;部分代码(I/O流的创建)有些繁琐,可以再简洁化一点,达到更低的时间复杂度。不过,这次项目的收获也不少,首先是对正则表达式的掌握有了提升,以前自己都没怎么仔细了解正则表达式,做项目的时候,边做边学,现在自己也能写一些正则表达式了。再者,通过软件工程理论的学习,开始了解编程的设计步骤,从无到有,一步一步来,效率有所提高,正如老师所说的,前面的准备工作做足,考虑更全面,后面的代码编写就会轻松高效很多。