GitHub项目地址:https://github.com/JackyLin18/word-count

Word Count 项目要求:

wc.exe 是一个常见的工具,它能统计文本文件的字符数、单词数和行数。这个项目要求写一个命令行程序,模仿已有wc.exe 的功能,并加以扩充,给出某程序设计语言源文件的字符数、单词数和行数。

实现一个统计程序,它能正确统计程序文件中的字符数、单词数、行数,以及还具备其他扩展功能,并能够快速地处理多个文件。

具体功能要求:
程序处理用户需求的模式为:

wc.exe [parameter] [file_name]

 

基本功能列表:

wc.exe -c file.c     //返回文件 file.c 的字符数

wc.exe -w file.c    //返回文件 file.c 的词的数目  

wc.exe -l file.c      //返回文件 file.c 的行数

扩展功能:
    -s   递归处理目录下符合条件的文件。
    -a   返回更复杂的数据(代码行 / 空行 / 注释行)。

空行:本行全部是空格或格式控制字符,如果包括代码,则只有不超过一个可显示的字符,例如“{”。

代码行:本行包括多于一个字符的代码。

注释行:本行不是代码行,并且本行包括注释。一个有趣的例子是有些程序员会在单字符后面加注释:

    } //注释
在这种情况下,这一行属于注释行。

[file_name]: 文件或目录名,可以处理一般通配符。

高级功能:

 -x 参数。这个参数单独使用。如果命令行有这个参数,则程序会显示图形界面,用户可以通过界面选取单个文件,程序就会显示文件的字符数、行数等全部统计信息。

需求举例:
  wc.exe -s -a *.c ===> 返回当前目录及子目录中所有*.c 文件的代码行数、空行数、注释行数。

解题思路:

分析项目要求,核心内容是需要读取指定文件,使用jdk提供的FileReader类和BufferedReader类,可以便捷地实现读取文件和记录字符数、行数的功能。

设计实现过程:

  1. 项目的代码构成:一共需要编写四个类,分别为:WordCountMain、FileUtil、ParamsUtil、FileFrame。
  2. WordCountMain类为程序入口类,其中包括main()方法和show()方法。main()方法的功能:调用相关方法对用户输入的参数进行判断和操作,根据用户的输入调用不同的功能方法,输出操作结果。show()方法的功能:打印用户菜单。
  3. FileUtil类为文件操作类,是项目的核心类,其中包括getFileList()方法,getCharsetsCount()方法,getWordsCount()方法,getLinesCount()方法和getCBNLinesCount()方法共五个静态方法。①getFileList()方法的功能:根据传入的文件是否为文件夹:是文件夹,遍历文件夹中的每一个文件。如果文件名符合通配符,将文件放入List中返回。②getCharsetsCount()方法的功能是:返回指定文件的字符数。③getWordsCount()方法的功能是:返回指定文件的单词数。④getLinesCount()方法的功能是:返回指定文件的行数目。⑤getCBNLinesCount()方法的功能是:返回指定文件的代码行数、空白行数、注释行数。
  4. ParamsUtil类为参数检查类,用于检查用户输入的参数是否合法,其中包括isExe()方法、isOperation()方法、isFileExist()方法这三个private方法和checkInputParams()方法这一个public方法。
  5. FileFrame类为用户提供图形界面,便于用户进行文件选择操作。
  6. 程序运行流程:程序启动,提示用户输入参数,程序通过调用ParamsUtil的checkInputParams()方法判断输入的参数是否合法,如果不合法,要求用户重新输入;如果合法,根据用户输入的参数调用对应的FileUtil类的相关方法,获得文件的相关数据后进行输出反馈给用户。
  7. 用户图形界面(GUI)说明:用户输入参数 “-frame” 后,调出图形界面。用户在图形界面可选择文件或者文件夹,根据用户选择的文件或者文件夹,程序自动识别出所有匹配的后缀供用户选择。同时,用户可在操作选项中选择需要进行的操作(-c, -w, -l, -a),用户选择完毕后点击确定,程序弹出窗口返回文件信息。

代码说明:

  • FileUtil.getFileList(String filePath, String consistent)
1 // 根据传入的路径,如果表示的是一个文件夹,返回该文件夹下的所有符合通配符的子文件
 2     public static List<File> getFileList(String filePath, String consistent) {
 3         File file = new File(filePath);
 4         // 存放遍历过程中的文件夹
 5         LinkedList<File> temp_fileList = new LinkedList<>();
 6         // 存放需要返回的符合条件的子文件
 7         List<File> fileList = new ArrayList<>();
 8         // 先将传入的指定文件放入temp_fileList中
 9         temp_fileList.add(file);
10         // 如果该文件不是文件夹,将其放置fileList中并返回
11         if (!file.isDirectory()) {
12             // 如果通配符为默认的 ".*" 将所有文件加入fileList
13             if (consistent.equals(".*")) {
14                 fileList.add(file);
15             } else {
16                 // 如果文件与指定的格式相符,将其加入返回的fileList
17                 if (filePath.endsWith(consistent)) {
18                     fileList.add(file);
19                 }
20             }
21             return fileList;
22         }
23         /*
24             如果temp_fileList中的每一个元素,如果该元素为文件夹,将整个元素放至temp_fileList中,
25             如果该元素为文件,但不符合通配符,不做任何处理
26             如果改文件为文件,且符合通配符,将其放至fileList中
27          */
28         while (!temp_fileList.isEmpty()) {
29             // 遍历temp_fileList
30             for (File f : Objects.requireNonNull(temp_fileList.removeFirst().listFiles())) {
31                 // 如果该元素为文件夹,将其放至temp_fileList中
32                 if (f.isDirectory()) {
33                     temp_fileList.add(f);
34                 } else {
35                     // 如果通配符为默认的 ".*" 将所有文件加入fileList
36                     if (consistent.equals(".*")) {
37                         fileList.add(f);
38                     } else {
39                         // 如果文件与指定的格式相符,将其加入返回的fileList
40                         if (f.getName().endsWith(consistent)) {
41                             fileList.add(f);
42                         }
43                     }
44                 }
45             }
46         }
47         return fileList;
48     }
  • FileUtil.getCharsetsCount(String filePath)
1 // "-c"操作:传入一个文件路径,返回该文件的字符数
 2     public static String getCharsetsCount(String filePath) throws IOException {
 3         File file = new File(filePath);
 4         int charsetsCount = 0;
 5         String str = null;
 6         // 装饰模式,使其获得多功能
 7         FileReader fileReader = new FileReader(file);
 8         BufferedReader reader = new BufferedReader(fileReader);
 9         while ((str = reader.readLine()) != null) {
10             // 将所有的空格去除
11             str = str.replaceAll(" ", "");
12             charsetsCount += str.length();
13         }
14         fileReader.close();
15         // 返回指定文件文件名及其字符数
16         return "指定文件" + filePath + "的字符数:" + charsetsCount;
17     }
  • FileUtil.getWordsCount(String filePath)
1 // "-w"操作:传入一个文件路径,返回该文件的词数
 2     public static String getWordsCount(String filePath) throws IOException {
 3         File file = new File(filePath);
 4         int wordsCount = 0;
 5         String str = null;
 6         // 装饰模式,使其获得多功能
 7         FileReader fileReader = new FileReader(file);
 8         BufferedReader reader = new BufferedReader(fileReader);
 9         while ((str = reader.readLine()) != null) {
10             // 两个单词之间使用空格分开
11             String[] strArray = str.split(" ");
12             for (String s : strArray) {
13                 if (!s.equals("")) {
14                     wordsCount++;
15                 }
16             }
17         }
18         fileReader.close();
19         // 返回指定文件文件名及其单词数
20         return "指定文件" + filePath + "的单词数:" + wordsCount;
21     }
  • FileUtil.getLinesCount(String filePath)
1  // "-l"操作:传入一个文件路径,返回该文件的行数
 2     public static String getLinesCount(String filePath) throws IOException {
 3         File file = new File(filePath);
 4         int linesCount = 0;
 5         // 装饰模式,使其获得多功能
 6         FileReader fileReader = new FileReader(file);
 7         BufferedReader reader = new BufferedReader(fileReader);
 8         while ((reader.readLine()) != null) {
 9             // 每读取一行,行数加一
10             linesCount++;
11         }
12         fileReader.close();
13         // 返回指定文件文件名及其行数目
14         return "指定文件" + filePath + "的行数目:" + linesCount;
15     }
  • FileUtil.getCBNLinesCount(String filePath)
1 // "-a"操作:传入一个文件路径,输出该文件的代码行数、空行数、注释行数
 2 
 3     /**
 4      * 代码行:本行包括多于一个字符的代码
 5      * 空行:本行全部是空格或格式控制字符,如果包括代码,则只有不超过一个可显示的字符,例如 "{"
 6      * 注释行:本行不是代码行,并且本行包括注释。一个有趣的例子是有些程序员会在单字符后面加注释:
 7      * } //注释
 8      * 在这种情况下,这一行属于注释行
 9      */
10     public static List<String> getCBNLinesCount(String filePath) throws IOException {
11         int codeLineCounts = 0; // 代码行数目
12         int blankLineCounts = 0; // 空白行数目
13         int noteLineCounts = 0; // 注释行数目
14         File file = new File(filePath);
15         // 装饰模式,使其获得多功能
16         FileReader fileReader = new FileReader(file);
17         BufferedReader reader = new BufferedReader(fileReader);
18         String str = null;
19         while ((str = reader.readLine()) != null) {
20             // 去掉空格
21             str = str.replaceAll(" ", "");
22             /*
23                 如果包含 "//" 但是注释外的字符多于一个,为代码行
24                 如果包含 "//" 而且注释外的字符少于或等于一个,为注释行
25              */
26             if (!str.contains("//")) {
27                 if (str.length() > 1) {
28                     codeLineCounts++;
29                 } else {
30                     blankLineCounts++;
31                 }
32             } else {
33                 if (str.substring(0, str.indexOf("//")).length() > 1) {
34                     codeLineCounts++;
35                 } else {
36                     noteLineCounts++;
37                 }
38             }
39         }
40         // 返回指定文件文件名及其代码行数、空白行数、注释行数
41         List<String> messages = new ArrayList<>();
42         messages.add("指定文件" + filePath + "的代码行数目:" + codeLineCounts + "\n");
43         messages.add("指定文件" + filePath + "的空白行数目:" + blankLineCounts + "\n");
44         messages.add("指定文件" + filePath + "的注释行数目:" + noteLineCounts + "\n");
45         fileReader.close();
46         return messages;
47     }
  • WordCountMain.main(String[] args)
1 // 程序主入口
 2     public static void main(String[] args) throws IOException, ClassNotFoundException, UnsupportedLookAndFeelException, InstantiationException, IllegalAccessException {
 3         // 提供给用户菜单
 4         show();
 5         // 读取用户输入的操作
 6         BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
 7         String input = null;
 8         // 创建一个判断输入参数的类实例
 9         ParamsUtil paramsUtil = new ParamsUtil();
10         while ((input = reader.readLine()) != null) {
11             if(input.contains("-frame")){
12                 new FileFrame();
13             }
14             // 判断用户的输入是否合法,如果合法,按用户要求的操作执行,如果不合法,要求用户重新输入
15             // 对输入进行 "添加通配符" 的处理
16             if (!input.contains("-s")) {
17                 // 如果没有输入"-s",自动添加默认的通配符
18                 input = input.concat(" .*");
19             } else {
20                 // 如果输入了"-s",但没有输入通配符,也添加默认的通配符
21                 String[] params = input.split(" ");
22                 if (!params[params.length - 1].contains(".")) {
23                     input = input.concat(" .*");
24                 }
25             }27             if (paramsUtil.checkInputParams(input)) {
28                 // 获得用户输入的参数数组
29                 String[] params = input.split(" ");
30                 int paramsLength = params.length;
31                 for (int i = 1; i < paramsLength; i++) {
32                     // FileUtil#getFileList()第一个参数为指定的文件路径,第二个参数为指定文件的通配符
33                     switch (params[i]) {
34                         case "-c":
35                             // 输出指定文件的字符数
36                             for (File f : FileUtil.getFileList(params[paramsLength - 2],
37                                     params[paramsLength - 1])) {
38                                 System.out.println(FileUtil.getCharsetsCount(f.getPath()));
39                             }
40                             break;
41                         case "-w":
42                             // 输出指定文件的单词数
43                             for (File f : FileUtil.getFileList(params[paramsLength - 2],
44                                     params[paramsLength - 1])) {
45                                 System.out.println(FileUtil.getWordsCount(f.getPath()));
46                             }
47                             break;
48                         case "-l":
49                             // 输出指定文件的行数
50                             for (File f : FileUtil.getFileList(params[paramsLength - 2],
51                                     params[paramsLength - 1])) {
52                                 System.out.println(FileUtil.getLinesCount(f.getPath()));
53                             }
54                             break;
55                         case "-a":
56                             // 输出指定文件的代码行数、空白行数、注释行数
57                             for (File f : FileUtil.getFileList(params[paramsLength - 2],
58                                     params[paramsLength - 1])) {
59                                 for(String message:FileUtil.getCBNLinesCount(f.getPath())){
60                                     System.out.print(message);
61                                 }
62                             }
63                             break;
64                         default:
65                             break;
66                     }
67                 }
68             }
69             // 再次显示菜单提示用户输入
70             show();
71         }
72     }
  • FileFrame
1 public class FileFrame implements ActionListener {
  2     JFrame frame = new JFrame("文件选择");
  3     JTabbedPane tabPane = new JTabbedPane();// 选项卡布局
  4     Container container = new Container();
  5     JPanel panel = new JPanel();
  6     JLabel directoryLabel = new JLabel("文件目录");
  7     JLabel fileLabel = new JLabel("选择文件");
  8     JLabel consistentLabel = new JLabel("指定后缀");
  9     JLabel operationLabel = new JLabel("选择操作");
 10     // 显示选择的文件夹的路径
 11     JTextField text1 = new JTextField();
 12     // 显示选择的文件的路径
 13     JTextField text2 = new JTextField();
 14     // 文件夹选择按钮
 15     JButton directoryButton = new JButton("选择");
 16     // 文件选择按钮
 17     JButton fileButton = new JButton("选择");
 18     // 后缀名下拉框
 19     JComboBox<String> comboBox = new JComboBox<>();
 20     // 操作复选框
 21     JCheckBox jCheckBoxC = new JCheckBox("-c");
 22     JCheckBox jCheckBoxW = new JCheckBox("-w");
 23     JCheckBox jCheckBoxL = new JCheckBox("-l");
 24     JCheckBox jCheckBoxA = new JCheckBox("-a");
 25     // 文件选择器
 26     JFileChooser fileChooser = new JFileChooser();
 27     // 提交按钮
 28     JButton submitButton = new JButton("确定");
 29     // 用一个List存放查询结果
 30     List<String> messages = new ArrayList<>();
 31     // 用一个List存放选择的操作
 32     List<JCheckBox> checkBoxList = new ArrayList<>();
 33 
 34     {
 35         // 设置按钮格式
 36         directoryButton.setBorder(BorderFactory.createRaisedBevelBorder());
 37         directoryButton.setFocusPainted(false);
 38         fileButton.setBorder(BorderFactory.createRaisedBevelBorder());
 39         fileButton.setFocusPainted(false);
 40         submitButton.setBorder(BorderFactory.createRaisedBevelBorder());
 41         submitButton.setFocusPainted(false);
 42     }
 43 
 44     public FileFrame() throws ClassNotFoundException, UnsupportedLookAndFeelException, InstantiationException, IllegalAccessException {
 45         String lookAndFeel = UIManager.getSystemLookAndFeelClassName();
 46         UIManager.setLookAndFeel(lookAndFeel);
 47 
 48         // 获得显示屏幕的宽度
 49         double lx = Toolkit.getDefaultToolkit().getScreenSize().getWidth();
 50         // 获得显示屏幕的高度
 51         double ly = Toolkit.getDefaultToolkit().getScreenSize().getHeight();
 52 
 53         // 设置窗口出现的位置
 54         frame.setLocation(new Point((int) (lx / 2) - 150, (int) (ly / 2) - 150));
 55         // 设置窗口的大小
 56         frame.setSize(400, 220);
 57         // 设置布局
 58         frame.setContentPane(tabPane);
 59 
 60         // 设置标签、按钮的位置和大小
 61         fileLabel.setBounds(30, 20, 70, 20);
 62         text2.setBounds(100, 20, 160, 20);
 63         text2.setEditable(false);
 64         fileButton.setBounds(270, 20, 80, 20);
 65 
 66         directoryLabel.setBounds(30, 50, 70, 20);
 67         text1.setBounds(100, 50, 160, 20);
 68         text1.setEditable(false);
 69         directoryButton.setBounds(270, 50, 80, 20);
 70 
 71         consistentLabel.setBounds(30, 80, 70, 20);
 72         comboBox.setBounds(100, 80, 160, 20);
 73 
 74         operationLabel.setBounds(30, 110, 70, 20);
 75         jCheckBoxC.setBounds(100, 110, 40, 20);
 76         jCheckBoxW.setBounds(140, 110, 40, 20);
 77         jCheckBoxL.setBounds(180, 110, 40, 20);
 78         jCheckBoxA.setBounds(220, 110, 40, 20);
 79 
 80         submitButton.setBounds(270, 110, 80, 20);
 81 
 82         // 为三个按钮添加事件处理
 83         directoryButton.addActionListener(this);
 84         fileButton.addActionListener(this);
 85         submitButton.addActionListener(this);
 86 
 87         // 将组件全部添加到container中
 88         container.add(directoryLabel);
 89         container.add(text1);
 90         container.add(directoryButton);
 91         container.add(fileLabel);
 92         container.add(text2);
 93         container.add(fileButton);
 94         container.add(submitButton);
 95         container.add(consistentLabel);
 96         container.add(comboBox);
 97         container.add(operationLabel);
 98         container.add(jCheckBoxC);
 99         container.add(jCheckBoxW);
100         container.add(jCheckBoxL);
101         container.add(jCheckBoxA);
102 
103         checkBoxList.add(jCheckBoxC);
104         checkBoxList.add(jCheckBoxW);
105         checkBoxList.add(jCheckBoxL);
106         checkBoxList.add(jCheckBoxA);
107 
108         // 设置窗口可见
109         frame.setVisible(true);
110         // 设置关闭窗口结束程序
111         frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
112         tabPane.add(container);
113     }
114 
115     public void actionPerformed(ActionEvent e) {
116         File directoryChecked = null;
117         File fileChecked = null;
118         // 点击文件夹选择按钮,选择文件夹
119         if (e.getSource().equals(directoryButton)) {
120             // 设置其只能选择到文件夹
121             fileChooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
122             // 打开文件选择器
123             int state = fileChooser.showOpenDialog(null);
124             // 如果关闭文件选择,直接返回;否则将选择的文件的文件名显示在面板上
125             if (state == 1) {
126                 return;
127             } else {
128                 directoryChecked = fileChooser.getSelectedFile();
129                 text1.setText(directoryChecked.getAbsolutePath());
130                 comboBox.removeAllItems();
131                 for (String s : getConsistentList()) {
132                     comboBox.addItem(s);
133                 }
134                 container.add(comboBox);
135                 tabPane.add(container);
136             }
137         }
138         // 点击文件选择按钮,选择文件
139         if (e.getSource().equals(fileButton)) {
140             // 设置其只能选择到文件
141             fileChooser.setFileSelectionMode(JFileChooser.FILES_ONLY);
142             // 打开文件选择器
143             int state = fileChooser.showOpenDialog(null);
144             // 如果关闭文件选择,直接返回;否则将选择的文件的文件名显示在面板上
145             if (state == 1) {
146                 return;
147             } else {
148                 fileChecked = fileChooser.getSelectedFile();
149                 text2.setText(fileChecked.getAbsolutePath());
150                 comboBox.removeAllItems();
151                 for (String s : getConsistentList()) {
152                     comboBox.addItem(s);
153                 }
154                 container.add(comboBox);
155                 tabPane.add(container);
156             }
157         }
158         // 点击提交按钮后弹出窗口,显示查询结果
159         if (e.getSource().equals(submitButton)) {
160             if (!text1.getText().equals("")) {
161                 for (File f : FileUtil.getFileList(text1.getText(), (String) comboBox.getSelectedItem())) {
162                     addMessages(messages, f, checkBoxList);
163                 }
164             }
165             if (!text2.getText().equals("")) {
166                 for (File f : FileUtil.getFileList(text2.getText(), (String) comboBox.getSelectedItem())) {
167                     addMessages(messages, f, checkBoxList);
168                 }
169             }
170             JOptionPane.showMessageDialog(null, messages.toArray(),
171                     "选择的文件的查询结果", JOptionPane.PLAIN_MESSAGE);
172             messages = new ArrayList<>();
173         }
174     }
175 
176     // 将指定的文件进行指定操作的查询,并将结果放置messages中
177     private void addMessages(List<String> messages, File f, List<JCheckBox> checkBoxList) {
178         try {
179             if (checkBoxList.get(0).isSelected()) {
180                 messages.add(FileUtil.getCharsetsCount(f.getPath()) + "\n");
181             }
182             if (checkBoxList.get(1).isSelected()) {
183                 messages.add(FileUtil.getWordsCount(f.getPath()) + "\n");
184             }
185             if (checkBoxList.get(2).isSelected()) {
186                 messages.add(FileUtil.getLinesCount(f.getPath()) + "\n");
187             }
188             if (checkBoxList.get(3).isSelected()) {
189                 messages.addAll(FileUtil.getCBNLinesCount(f.getPath()));
190             }
191         } catch (IOException ex) {
192             ex.printStackTrace();
193         }
194     }
195 
196     // 获取选择的后缀
197     private List<String> getConsistentList() {
198         // 先获取选择的文件
199         String directoryPath = text1.getText();
200         String filePath = text2.getText();
201         // 创建一个List存放文件的后缀
202         List<String> consistentList = new ArrayList<>();
203         consistentList.add(".*");
204         // 遍历所有文件,获得选择的文件、文件夹中的子文件的所有后缀名
205         if (!filePath.equals("")) {
206             String s = filePath.substring(filePath.lastIndexOf("."));
207             // 如果List中不存在这个后缀名,将其加入List
208             if (!consistentList.contains(s)) {
209                 consistentList.add(s);
210             }
211         }
212         if (!directoryPath.equals("")) {
213             for (File f : FileUtil.getFileList(directoryPath, ".*")) {
214                 String s = f.getPath().substring(f.getPath().lastIndexOf("."));
215                 // 如果List中不存在这个后缀名,将其加入List
216                 if (!consistentList.contains(s)) {
217                     consistentList.add(s);
218                 }
219             }
220         }
221         return consistentList;
222     }
223 }

测试运行:

  • 测试用例:

java wordcount java wordcount程序_文件路径

java wordcount java wordcount程序_代码行数_02


java wordcount java wordcount程序_List_03

 

java wordcount java wordcount程序_java wordcount_04

  • 单个测试“-c, -w, -l, -a, -s”每一个操作:

java wordcount java wordcount程序_文件路径_05

java wordcount java wordcount程序_java wordcount_06

java wordcount java wordcount程序_代码行数_07

java wordcount java wordcount程序_List_08

java wordcount java wordcount程序_java wordcount_09

  • 同时进行多个操作:

java wordcount java wordcount程序_java wordcount_10

  • 调出用户图形界面

java wordcount java wordcount程序_List_11

java wordcount java wordcount程序_java wordcount_12

  • 使用图形界面分别进行只选择文件、只选择文件夹和同时选择文件和文件夹的操作

  对指定文件进行操作:

java wordcount java wordcount程序_java wordcount_13

获得文件夹中所有的文件后缀名供用户选择:

java wordcount java wordcount程序_List_14

  对指定文件夹中的文件进行操作:

java wordcount java wordcount程序_代码行数_15

 

   对选择的文件、文件夹进行操作:

  

java wordcount java wordcount程序_文件路径_16

 

   

java wordcount java wordcount程序_文件路径_17

PSP表格:

java wordcount java wordcount程序_List_18

项目小结:

  • 要做好一个项目,要把基本的框架构建好,做好前期的计划工作甚至要比编码要更重要。一个好的计划能让你事半功倍,在没有做好计划之前就动手编码会使你走很多的弯路。一个项目的开发重点不只在于代码的编写,前期的计划、项目分析、规范和设计,后期的复审、测试和改进同样重要。
  • 项目完成了基本要求,扩展要求和高级要求,但实际的代码中还存在不足,前期的计划不够完善导致编写代码时还会走不少的弯路,有待改进。