软件工程 wc.exe 代码统计作业

本项目介绍了一款基于Java的代码统计工具,支持字符数、词数、行数统计及更复杂的代码分析功能,如统计代码行、注释行和空行。

软件工程 wc.exe 代码统计作业分享

1. Github 项目地址

https://github.com/EdwardLiu-Aurora/WordCount

更好地阅读本文,可点击这里

  • 基本要求

    • [x] -c 统计文件字符数 (实现)

    • [x] -w 统计文件词数 (实现)

    • [x] -l 统计文件行数(实现)

  • 扩展功能

    • [x] -s 递归处理目录下符合条件得文件(实现)
    • [x] -a 返回文件代码行 / 空行 / 注释行(实现)
    • [x] 支持各种文件的通配符(*,?)(实现)
  • 高级功能

    • [ ] -x 图形化界面(未实现)
2. PSP 表格
PSP2.1Personal Software Process Stages预估耗时(分钟)实际耗时(分钟)
Planning计划55
· Estimate· 估计这个任务需要多少时间600730
Development开发480610
· Analysis· 需求分析 (包括学习新技术)6060
· Design Spec· 生成设计文档6060
· Design Review· 设计复审 (和同事审核设计文档)3030
· Coding Standard· 代码规范 (为目前的开发制定合适的规范)3010
· Design· 具体设计3060
· Coding· 具体编码120240
· Code Review· 代码复审3030
· Test· 测试(自我测试,修改代码,提交修改)120120
Reporting报告120120
· Test Report· 测试报告6060
· Size Measurement· 计算工作量3030
· Postmortem & Process Improvement Plan· 事后总结, 并提出过程改进计划3030
合计605735
3. 解题思路描述
(1) 返回文件的字符数

定义:返回文件中除去的字符总数(中文字符分离出来计算)

思路:使用 Java 按行读取文件,每个行就是一个 String 对象。
使用 String.length() 来统计该行的字符数,并且按照 Character 的值范围判断是否为中文字。使用两个 int 变量来计算总的字符数以及总的中文字符数。

(2) 返回文件的词汇数

定义:不包含中文字符,只包含 0-9,a-z,A-Z 和 _ 的连续字段称为词汇

思路:查看 Java 根据以上规则,编写符合的正则表达式,使用正则表达式进行按行累加单词数。

(3) 返回文件的行数

定义:返回文件中总行数(根据换行符决定)

思路:根据 Java 按行读取文件,设定计数器。

(4) 递归处理目录下符合条件的文件

定义:该目录及子目录下的文件全部分析,附带用户需要的数据

思路:使用一个函数,将该目录下的所有符合条件的文件路径转成一个 ArrayList 对象并且返回到 Main 函数,由 Main 函数继续处理。

(5) 返回更复杂的数据

代码行:除了格式控制符号(如 "{}" "()" ";" 等)之外,包含多余一个字符的代码;

思路:设置一个 Set 里面存储了所有的格式控制字符,如果检测到字符不在 Set 内,则判断为代码行(要注意的跟注释行冲突的情况:

1. 当该行有 // 和 /* 时,观察哪一个在前面
2. 当该行在 /* */ 注释内时,则不属于代码行;
3. 当该行是 /* 注释行第一行或者末尾一行的时候,要注意检测 /* 前 或 */ 后 的字符;
4. 当该行仅包含 // 时,检查 // 前的符号

注释行:包括注释的行号,无论本行是不是代码行;即包含 // 或在 /* */ 范围内的行;

思路:按行读取,按照 // 或者 /* */ 区分情况

空行:全是空格或者格式控制字符的行;

思路:按照 正则表达式 和 String.indexOf() 函数进行匹配处理

(6) 文件通配符

定义:可以按照 * 代表任意 0 ~ 多个 字符以及 ? 代表 1 个任意字符进行匹配

思路:先将 ? 和 * 替换为特定的正则表达式表示,然后将 . 替换为正则表达式表示,然后进行每一个路径中的正则匹配。

4. 设计实现过程
包的说明
  • bean:存放将要返回的复合类型
  • service: 存放具体业务的函数实现
  • com.edwardliu_aurora:Main 函数的具体实现
类的说明
  • CharCount:
    • allCharCount 所有字符总数
    • chnCharCount 所有中字总数
  • LineCount:
    • blankLineCount 空白行数统计
    • codeLineCount 代码行数统计
    • commentLineCount 注释行数统计
  • BasicStatistic:
    • public CharCount getCharCount(String filePath) 返回字数统计
    • public long getWordCount(String filePath) 返回词数统计
    • public long getLineCount(String filePath) 返回行数统计
  • ExtraStatistic:
    • public LineCount getDetailLineCount(String filePath) 获取详细的行数信息
  • Utils:
    • public static Charset charsetRecognize(String filePath) 识别文本的编码类型(仅支持 GBK 和 UTF-8)
    • public static ArrayList getFilesPath(String folderPath,String filePattern) 返回某目录下的所有符合 filePattern 通配符的文件路径
  • Main:
    • 主要负责输入输出以及以上函数的合理调用
5. 代码说明
  • CharCount:
    • allCharCount 所有字符总数
    • chnCharCount 所有中字总数
      ```
      package bean;

/
记录总字符数和中文字符数的类
/
public class CharCount {
// 全体字符数目
long allCharCount = 0;
// 中文字符数目
long chnCharCount = 0;

public CharCount(long allCharCount, long chnCharCount) {
    this.allCharCount = allCharCount;
    this.chnCharCount = chnCharCount;
}

public long getAllCharCount() {
    return allCharCount;
}

public void setAllCharCount(long allCharCount) {
    this.allCharCount = allCharCount;
}

public long getChnCharCount() {
    return chnCharCount;
}

public void setChnCharCount(long chnCharCount) {
    this.chnCharCount = chnCharCount;
}

}

- LineCount:
    - blankLineCount    空白行数统计
    - codeLineCount     代码行数统计
    - commentLineCount  注释行数统计

package bean;

// 记录详细行数的类
public class LineCount {
// 空行
int blankLineCount = 0;
// 代码行
int codeLineCount = 0;
// 注释行
int commentLineCount = 0;

public LineCount(int blankLineCount, int codeLineCount, int commentLineCount) {
    this.blankLineCount = blankLineCount;
    this.codeLineCount = codeLineCount;
    this.commentLineCount = commentLineCount;
}

public int getBlankLineCount() {
    return blankLineCount;
}

public void setBlankLineCount(int blankLineCount) {
    this.blankLineCount = blankLineCount;
}

public int getCodeLineCount() {
    return codeLineCount;
}

public void setCodeLineCount(int codeLineCount) {
    this.codeLineCount = codeLineCount;
}

public int getCommentLineCount() {
    return commentLineCount;
}

public void setCommentLineCount(int commentLineCount) {
    this.commentLineCount = commentLineCount;
}

}

- BasicStatistic:
    - public CharCount getCharCount(String filePath)    返回字数统计

// 返回文件字符数的函数
public CharCount getCharCount(String filePath){
// 全体字符数变量和中文字符数变量
long allCharCount = 0, chnCharCount = 0;
// 新建 nio 文件路径对象
Path path = Paths.get(filePath);
// 为了避免文本太大,这里采用惰性的 Stream 对象
// 注意文件的编码只能为 UTF_8 类型,不然会出现不可知的错误
Stream lines = null;
try {
// 按行读取文件并转化成流
lines = Files.lines(path, Utils.charsetRecognize(filePath));
// 使用 lambda 表达式和 nio 进行归并统计字符数
allCharCount = lines.parallel().reduce(0,
(total, line) -> total + line.length(),
(total1, total2) -> total1 + total2);
lines.close();
// 按行读取文件并转化成流
lines = Files.lines(path, Utils.charsetRecognize(filePath));
// 使用 lambda 表达式和 nio 进行归并统计中文字数
chnCharCount = lines.parallel().reduce(0,
(total, line) ->
{
int res=0;
for(int i=0;i<line.length();i++)
if(line.charAt(i) >= 19968 && line.charAt(i) <= 40869)
res++;
return res + total;
},
(total1, total2) -> total1 + total2
);
lines.close();
}
catch (IOException e){
e.printStackTrace();
System.out.println("文件不存在或无法访问");
if(lines != null) lines.close();
return null;
}
return new CharCount(allCharCount, chnCharCount);
};

    - public long getWordCount(String filePath)         返回词数统计

// 返回文件的词汇数
public long getWordCount(String filePath){
long wordCount = 0;
// 为了避免文本太大,这里采用惰性的 Stream 对象
// 注意文件的编码只能为 UTF_8 类型,不然会出现不可知的错误
Stream lines = null;
// 新建 nio 文件路径对象
Path path = Paths.get(filePath);
try {
// 按行读取文件并转化成流
lines = Files.lines(path, Utils.charsetRecognize(filePath));
Pattern pattern = Pattern.compile("\w+");
// 使用 lambda 表达式和 nio 进行归并统计词汇数
wordCount = lines.parallel().reduce(0,
(total, line) ->
{
int count = 0;
Matcher matcher = pattern.matcher(line);
while(matcher.find()){
count++;
}
return total + count;
},
(total1, total2) -> total1 + total2
);
lines.close();
}
catch (IOException e){
e.printStackTrace();
System.out.println("文件不存在或无法访问");
if(lines != null) lines.close();
return 0;
}
return wordCount;
}

    - public long getLineCount(String filePath)         返回行数统计

// 返回文件的行数
public long getLineCount(String filePath){
long lineCount = 0;
// 为了避免文本太大,这里采用惰性的 Stream 对象
// 注意文件的编码只能为 UTF_8 类型,不然会出现不可知的错误
Stream lines = null;
// 新建 nio 文件路径对象
Path path = Paths.get(filePath);
try {
// 按行读取文件并转化成流
lines = Files.lines(path, Utils.charsetRecognize(filePath));
// 使用 lambda 表达式和 nio 进行归并统计行数
lineCount = lines.parallel().reduce(0,
(total, line) -> total + 1,
(total1, total2) -> total1 + total2);
lines.close();
}
catch (IOException e){
e.printStackTrace();
System.out.println("文件不存在或无法访问");
if(lines != null) lines.close();
return 0;
}
return lineCount;
}

- ExtraStatistic:
    - public LineCount getDetailLineCount(String filePath)  获取详细的行数信息

package service;

import bean.LineCount;

import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.InputStreamReader;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Stream;

// 高级统计功能
public class ExtraStatistic {

public LineCount getDetailLineCount(String filePath) {
    LineCount lineCount = new LineCount(0,0,0);
    // 正则表达式匹配任何非空字符
    Pattern pattern = Pattern.compile("\\S");
    // 统计空行数
    Path path = Paths.get(filePath);
    try(Stream<String> lines = Files.lines(path, Utils.charsetRecognize(filePath))){
        lineCount.setBlankLineCount(
                (int) lines.filter(line -> {
                    if(line.length() == 0) return true;
                    int i = 0;
                    Matcher matcher = pattern.matcher(line);
                    while(matcher.find()){
                        i++;
                        // 如果有超过一个非空白字符,则不为空行
                        if(i > 1) return false;
                    }
                    // 其余为空行
                    return true;
                }).count()
        );
    }
    catch(Exception e){
        e.printStackTrace();
        System.out.println("文件不存在或无法访问");
        return null;
    }
    // 统计注释行和代码行
    try(BufferedReader bufferedReader = new BufferedReader(
            new InputStreamReader(
                    new FileInputStream(filePath),
                    Utils.charsetRecognize(filePath)
            ))){
        int commentLineCount = 0;
        int codeLineCount = 0;
        // 按行读取文件
        for(String line = bufferedReader.readLine(); line != null; line = bufferedReader.readLine())
        {
            // 单行注释符号位置
            int oneLinePos = line.indexOf("//");
            // 多行注释符号位置
            int mulLinePos = line.indexOf("/*");
            // 如果该行有 //,且 // 在前,则将第一次匹配到的 // 后的内容删去,注释行 +1
            if(oneLinePos >= 0 && (mulLinePos < 0 || (mulLinePos >= 0 && oneLinePos < mulLinePos))){
                line = line.substring(0,oneLinePos);
                commentLineCount++;
                // 如果有 >1 个非空字符,则同时也为代码行
                Matcher matcher = pattern.matcher(line);
                int i = 0;
                while(matcher.find()) i++;
                if(i > 1) codeLineCount++;
            }
            // 如果该行只有 /* ,则检查是否同时为代码行。注释行 +1,连续读取直到遇到 */ 行
            else if(mulLinePos >= 0){
                line = line.substring(0, mulLinePos);
                commentLineCount++;
                // 如果有 >1 个非空字符,则同时也为代码行
                Matcher matcher = pattern.matcher(line);
                int i = 0;
                while(matcher.find()) i++;
                if(i > 1) codeLineCount++;
                line = bufferedReader.readLine();
                while(line.indexOf("*/") < 0) {
                    commentLineCount++;
                    line = bufferedReader.readLine();
                }
                commentLineCount++;
                line = line.substring(line.indexOf("*/")+2);
                // 如果有超过一个非空字符,则也为代码行
                i = 0;
                matcher = pattern.matcher(line);
                while(matcher.find()) i++;
                if(i > 1) codeLineCount++;
            }
            // 如果没有注释,则看是否能匹配到 >1 个非空字符
            else{
                int i = 0;
                Matcher matcher = pattern.matcher(line);
                while(matcher.find()) i++;
                if(i > 1) codeLineCount++;
            }
        }
        lineCount.setCodeLineCount(codeLineCount);
        lineCount.setCommentLineCount(commentLineCount);
    }
    catch(Exception e){
        e.printStackTrace();
        System.out.println("文件不存在或无法访问");
        return null;
    }
    return lineCount;
}

}

- Utils:
    - public static Charset charsetRecognize(String filePath)                               识别文本的编码类型(仅支持 GBK 和 UTF-8)

// 文件编码类型简单识别
public static Charset charsetRecognize(String filePath){
try{
File file = new File(filePath);
InputStream in = new java.io.FileInputStream(file);
byte[] b = new byte[3];
in.read(b);
in.close();
if (b[0] == -17 && b[1] == -69 && b[2] == -65)
return Charset.forName("UTF-8");
else{
try (Stream lines = Files.lines(Paths.get(filePath), Charset.forName("UTF-8"))){
lines.count();
return Charset.forName("UTF-8");
}
catch(Exception e){
return Charset.forName("GBK");
}
}
}
catch(Exception e){
e.printStackTrace();
return Charset.forName("UTF-8");
}
}

    - public static ArrayList<String> getFilesPath(String folderPath,String filePattern)    返回某目录下的所有符合 filePattern 通配符的文件路径

// 获取一个目录下及其子目录下的所有的文件路径
public static ArrayList getFilesPath(String folderPath,String filePattern){
Pattern pattern = Pattern.compile(filePattern);
ArrayList filePaths = new ArrayList<>();
// 获取路径
File file = new File(folderPath);
// 如果这个路径是文件夹
if(file.isDirectory()) {
// 获取路径下的所有文件
File[] files = file.listFiles();
for (int i = 0; i < files.length; i++) {
// 如果路径下的还是文件夹,则递归获取里面的文件和文件夹
if(files[i].isDirectory()) {
filePaths.addAll(getFilesPath(files[i].getPath(),filePattern));
} else {
Matcher matcher = pattern.matcher(files[i].getPath());
if(matcher.find()) filePaths.add(files[i].getPath());
}
}
} else {
Matcher matcher = pattern.matcher(file.getPath());
if(matcher.find()) filePaths.add(file.getPath());
}
return filePaths;
}

- Main:
    - 主要负责输入输出以及以上函数的合理调用

package com.edwardliu_aurora;

import bean.CharCount;
import bean.LineCount;
import service.BasicStatistic;
import service.ExtraStatistic;
import service.Utils;

import java.io.File;
import java.util.ArrayList;

public class Main {
public static void main(String[] args) {
boolean charCount = false;
boolean wordCount = false;
boolean lineCount = false;
boolean directory = false;
boolean detailLine = false;
for(int i=0;i<args.length-1;i++){
if(args[i].equals("-c")) charCount = true;
else if(args[i].equals("-w")) wordCount = true;
else if(args[i].equals("-l")) lineCount = true;
else if(args[i].equals("-s")) directory = true;
else if(args[i].equals("-a")) detailLine = true;
}
BasicStatistic basicStatistic = new BasicStatistic();
ExtraStatistic extraStatistic = new ExtraStatistic();
if(directory) {
String filePattern = args[args.length-1];
filePattern = filePattern.
replaceAll("\?","[^/\\\\:*?<>|]").
replaceAll("\","[^/\\\\:*?<>|]").
replaceAll("\.","\\.");
ArrayList filePaths = Utils.getFilesPath(new File("").getAbsolutePath(), filePattern);
for(String filePath:filePaths) {
System.out.println();
System.out.println("文件: " + filePath);
if(charCount) {
CharCount charCountRes = basicStatistic.getCharCount(filePath);
System.out.println("字符数: " + charCountRes.getAllCharCount() + "\t" + "中字数: " + charCountRes.getChnCharCount());
}
if(wordCount) System.out.println("单词数: " + basicStatistic.getWordCount(filePath));
if(lineCount) System.out.println("总行数: " + basicStatistic.getLineCount(filePath));
if(detailLine) {
LineCount lineCountRes = extraStatistic.getDetailLineCount(filePath);
System.out.println("空白行: " + lineCountRes.getBlankLineCount() + "\t" +
"代码行: " + lineCountRes.getCodeLineCount() + "\t" +
"注释行: " + lineCountRes.getCommentLineCount());
}
}
}
else {
String fileName = args[args.length-1];
File file = new File(fileName);
if(!file.exists()){
System.out.println("您要分析的文件不存在,请输入正确的文件名!");
return;
}
System.out.println("文件名: " + fileName);
if(charCount) {
CharCount charCountRes = basicStatistic.getCharCount(file.getAbsolutePath());
System.out.println("字符数: " + charCountRes.getAllCharCount());
System.out.println("中字数: " + charCountRes.getChnCharCount());
}
if(wordCount) System.out.println("单词数: " + basicStatistic.getWordCount(file.getAbsolutePath()));
if(lineCount) System.out.println("总行数: " + basicStatistic.getLineCount(file.getAbsolutePath()));
if(detailLine) {
LineCount lineCountRes = extraStatistic.getDetailLineCount(file.getAbsolutePath());
System.out.println("空白行: " + lineCountRes.getBlankLineCount());
System.out.println("代码行: " + lineCountRes.getCodeLineCount());
System.out.println("注释行: " + lineCountRes.getCommentLineCount());
}
}
}
}
```

6. 测试运行
单元测试 (已经在开发过程中进行,在这里就不展示了)
测试文件
  1. 空文件

1483624-20180909220731547-1044435807.jpg

  1. 只有一个字符的文件

1483624-20180909220750229-809357910.jpg

  1. 只有一个词的文件

1483624-20180909220850594-1529876977.jpg

  1. 只有一行的文件

1483624-20180909220855365-2057252506.jpg

  1. 一个典型的源文件

1483624-20180909220901877-429478320.jpg

基本功能
  1. 统计特定文件字符数、词汇数、行数

已经在上方图片展示

高级功能
  1. 统计目录下通配文件基本信息
  2. 统计目录下通配文件详细信息

1483624-20180909220910388-1325429814.jpg

代码覆盖率测试

1483624-20180909224826392-590261474.png

7. 实际花费时间 (见开头 PSP 表,已填入)
8. 项目小结
  1. 我在这个项目中使用了 Java 1.8 中才开始支持的 Stream API。好处是可以支持函数式编程,可以很方便地用并行运算对文件进行统计工作。而同时这也带来了一个问题——用户必须使用 JRE 1.8+ 的版本才能运行我的程序。

  2. 在这个项目中,我在读取文件时发现了一个文件编码的识别问题。文件编码的识别问题本身比较复杂,因为 Windows 下的文本文件,有的带有 BOM 头信息,而有的没有携带 BOM 头信息。 对于有携带 BOM 头信息的,我可以很方便地识别出该文件是否为 UTF-8 编码。然而,有很多文件是没有 BOM 编码的,我只能根据异常来猜测这个文件是什么编码的。所以目前我的编码识别函数只能支持 UTF-8 和 GBK 两种编码,并没有支持其他编码。一旦用户的文本文件是其他编码的,我的程序会出现不可预知的错误。

  3. 我没有对用户输入的参数进行判断和校验。我只对是否存在这个文件做了简单的校验。一旦用户把命令输入错误了,我的程序将发生无法预料的错误。

  4. 使用软件工程的方法来进行项目的设计,前期也许会花费很多时间,但是事后在编写的过程中会更加清晰有条理,让整体项目设计变得更加可控。

转载于:https://www.cnblogs.com/liufengcan/p/9615432.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值