项目要求
实现一个统计程序,它能正确统计程序文件中的字符数、单词数、行数,以及还具备其他扩展功能,并能够快速地处理多个文件。
具体功能要求:
程序处理用户需求的模式为:
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 返回更复杂的数据(代码行 / 空行 / 注释行)。(实现)
- 高级功能:
-x 参数。这个参数单独使用。如果命令行有这个参数,则程序会显示图形界面,用户可以通过界面选取单个文件,程序就会显示文件的字符数、行数等全部统计信息。
(实现)
需求举例:
wc.exe -s -a *.c (实现)
返回当前目录及子目录中所有*.c 文件的代码行数、空行数、注释行数。
Github项目地址:https://github.com/kvhong/JAVA-to-bulid-wc.exe
解题思路
主函数思路:通过输入的命令来判断执行各个功能函数,将输入的命令分割为两部分,第一部分是指令,第二部分是文件路径,用指令来调用各个功能函数,文件路径则作为参数输入到各个功能函数中实现功能。
功能函数的共同部分:通过传入的文件路径并用Buffer流读取文件内容,对文件内容进行相应统计然后输出结果,需实现文件不存在或者路径输入错误提示。
词数统计函数:需要去除空行、各种符号,将独立的词统计出来;
字符统计函数:需要去除空行、空格,将其余的内容的每一个单元都示为一个字符统计出来;
行数统计函数:文件中全部行数除结尾空行不算外其他都统计;
空行:TAB、空格、回车形成的空行都算入;
注释行:单独存在//、/*、*/的算入注释行;
代码行:除了空行、注释行外属于代码行;
帮助:将各种命令列出,以更好的帮助使用;
用户使用说明
-c 文件(须包含文件完整路径) 统计程序文件中的字符数
-w 文件(须包含文件完整路径) 统计程序文件中的单词数
-l 文件(须包含文件完整路径) 统计程序文件中的行数
-a 文件(须包含文件完整路径) 统计程序文件中的空行数、代码行数、注释行数
-s-a 文件路径或者文件夹路径 递归统计程序文件中的空行数、代码行数、注释行数(-s 命令不可单独使用)
? 帮助
end 结束程序
遇到的问题及解决方法
一开始不知道如何对词、字符进行有效的分割,然后在网上学习到可以用正则表达式进行分割:使用下面语句将数字和和中英文标点符号和中文都替换成空格,以通过空格分割出各个词。
str.replaceAll("[\\p{Nd}\\u9fa5-\\uffe5\\p{Punct}\\s&&[^-]]", " ");
使用下面的正则匹配器匹配注释行和空行
Pattern nodeLinePattern = Pattern.compile("((//)|(/\\*+)|((^\\s)*\\*)|((^\\s)*\\*+/))+", Pattern.MULTILINE + Pattern.DOTALL); // 注释匹配器(匹配单行、多行、文档注释) Pattern spaceLinePattern = Pattern.compile("^\\s*$"); // 空白行匹配器(匹配回车、tab键、空格)
设计及代码
该程序通过一个主函数文件和一个方法函数文件组成。主函数文件用于输入参数并判断所输入的各种操作以调用方法函数。方法函数用于实现各种功能:
文件词数统计函数:getwordnumber() 命令:-w 文件路径(须包含完整路径)
//文件词统计函数 int getwordnumber(String filename) throws IOException {int num=0; String[] strword = null; File file = new File(filename); if(file.exists()) { //读取文件 FileReader fr = new FileReader(filename); br = new BufferedReader(fr); String line = null; StringBuffer sbf = new StringBuffer(); while((line=br.readLine())!= null) { sbf.append(line); String str = sbf.toString(); //正则表达式替换符号 str = str.replaceAll("[\\p{Nd}\\u9fa5-\\uffe5\\p{Punct}\\s&&[^-]]", " "); //按空格将内容分割 strword = str.split("\\s+"); num=strword.length; } br.close(); fr.close(); }else { System.out.println("文件不存在,请重新输入文件!"); } return num; }
文件字符数统计函数:getCharacternumber() 命令:-c 文件路径(须包含完整路径)
//文件字符统计函数 int getCharacternumber(String filename) throws IOException {int number = 0; String[] strword = null; File file = new File(filename); if(file.exists()) { //读取文件 FileReader fr = new FileReader(filename); br = new BufferedReader(fr); String line = null; String str=null; StringBuffer sbf = new StringBuffer(); while((line=br.readLine())!= null) { sbf.append(line); str = sbf.toString(); strword = str.split("\\s+"); } for(int i=0;i) { Pattern pattern = Pattern.compile("[0-9a-zA-Z]*"); Matcher matcher = pattern.matcher(strword[i]); if(matcher.find()) { number+=matcher.regionEnd(); } } br.close(); fr.close(); }else { System.out.println("文件不存在,请重新输入文件!"); } return number; }
文件行数统计函数:getlinenumber() 命令:-l 文件路径(须包含完整路径)
//文件行数统计函数 int getlinenumber(String filename) throws IOException {int linenum = 0; File file = new File(filename); if(file.exists()) { //读取文件 FileReader fr = new FileReader(filename); //读取文件行数 LineNumberReader lnr = new LineNumberReader(fr); while(lnr.readLine()!= null) { linenum=lnr.getLineNumber(); } lnr.close(); fr.close(); }else { System.out.println("文件不存在,请重新输入文件!"); } return linenum; }
统计文件或者文件夹中程序文件的空行、代码行、注释行,有递归文件目录功能的函数,将递归功能一同实现在此函数中:diffline() 命令:-a 文件路径或文件夹路径/-s-a 文件路径或文件夹路径
//统计文件或者文件夹中程序文件的空行、代码行、注释行,有递归文件目录功能 int[] difflineGUI(File file) throws IOException { int spaceline = 0; int nodeline = 0; int codeline = 0; if (file == null || !file.exists()) { System.out.println(file + ",文件不存在!"); }else { if (file.isDirectory()) { File[] files = file.listFiles(new FileFilter() { public boolean accept(File pathname) { return pathname.getName().endsWith(".java")|| pathname.isDirectory()|| pathname.getName().endsWith(".c")||pathname.getName().endsWith(".cpp") ; } }); //递归文件目录 for (File target : files) { diffline(target); } } else { BufferedReader bufr = null; // 将指定路径的文件与字符流绑定 bufr = new BufferedReader(new InputStreamReader(new FileInputStream(file))); // 定义匹配每一行的正则匹配器 Pattern nodeLinePattern = Pattern.compile("((//)|(/\\*+)|((^\\s)*\\*)|((^\\s)*\\*+/))+", Pattern.MULTILINE + Pattern.DOTALL); // 注释匹配器(匹配单行、多行、文档注释) Pattern spaceLinePattern = Pattern.compile("^\\s*$"); // 空白行匹配器(匹配回车、tab键、空格) // 遍历文件中的每一行,并根据正则匹配的结果记录每一行匹配的结果 String line = null; while((line = bufr.readLine()) != null) { if (nodeLinePattern.matcher(line).find()) { nodeline ++; }else if (spaceLinePattern.matcher(line).find()) { spaceline ++; }else{ codeline ++; } } } } int[] Sline= {spaceline,nodeline,codeline}; return Sline; }
用于wctest.java文件:
//统计文件或者文件夹中程序文件的空行、代码行、注释行,有递归文件目录功能 void diffline(File file) throws IOException { int spaceline = 0; int nodeline = 0; int codeline = 0; if (file == null || !file.exists()) { System.out.println(file + ",文件不存在!"); }else { if (file.isDirectory()) { File[] files = file.listFiles(new FileFilter() { public boolean accept(File pathname) { return pathname.getName().endsWith(".java")|| pathname.isDirectory()|| pathname.getName().endsWith(".c")||pathname.getName().endsWith(".cpp") ; } }); //递归文件目录 for (File target : files) { diffline(target); } } else { System.out.println("文件名:"+file.getAbsolutePath()); // 将指定路径的文件与字符流绑定 BufferedReader bufr = new BufferedReader(new InputStreamReader(new FileInputStream(file))); // 定义匹配每一行的正则匹配器 Pattern nodeLinePattern = Pattern.compile("((//)|(/\\*+)|((^\\s)*\\*)|((^\\s)*\\*+/))+", Pattern.MULTILINE + Pattern.DOTALL); // 注释匹配器(匹配单行、多行、文档注释) Pattern spaceLinePattern = Pattern.compile("^\\s*$"); // 空白行匹配器(匹配回车、tab键、空格) // 遍历文件中的每一行,并根据正则匹配的结果记录每一行匹配的结果 String line = null; while((line = bufr.readLine()) != null) { if (nodeLinePattern.matcher(line).find()) { nodeline ++; }else if (spaceLinePattern.matcher(line).find()) { spaceline ++; }else{ codeline ++; } } System.out.println("空行数:"+spaceline); System.out.println("注释行数:"+nodeline); System.out.println("代码行数:"+codeline); } } }
帮助函数:help() 命令:?
//帮助函数 void help(){ System.out.println("-c 文件(须包含文件完整路径) 统计程序文件中的字符数"); System.out.println("-w 文件(须包含文件完整路径) 统计程序文件中的单词数"); System.out.println("-l 文件(须包含文件完整路径) 统计程序文件中的行数"); System.out.println("-a 文件(须包含文件完整路径) 统计程序文件中的空行数、代码行数、注释行数"); System.out.println("-s-a 文件路径或者文件夹路径 递归统计程序文件中的空行数、代码行数、注释行数"); System.out.println("-x 打开用户界面"); System.out.println("? 帮助"); System.out.println("end 结束程序"); }
主函数:wctest.java
package wc; import java.io.File; import java.io.IOException; import java.util.Scanner; public class wctest{ private static Scanner scanner; public static void main(String[] args) throws IOException{ String str = null; wcfunction wcf = new wcfunction(); //循环询问命令输入 while(true) { System.out.print("请输入命令:"); //命令输入 scanner = new Scanner(System.in); if(scanner.hasNext()) { str=scanner.nextLine(); } //分割命令,第一个作为判断第二个为文件路径 String[] strword = str.split(" "); if(strword.length==2) { if(strword[0].equals("-c")) { int chara=wcf.getCharacternumber(strword[1]); System.out.println("该文件的字符数:"+chara); }else if(strword[0].equals("-w")) { int word=wcf.getwordnumber(strword[1]); System.out.println("该文件的词数:"+word); }else if(strword[0].equals("-l")) { int line=wcf.getlinenumber(strword[1]); System.out.println("该文件的行数:"+line); }else if(strword[0].equals("-a")) { File file = new File(strword[1]); int[] linenum=wcf.diffline(file); System.out.println("该文件的空行数:"+linenum[0]); System.out.println("该文件的注释行数:"+linenum[1]); System.out.println("该文件的代码行数:"+linenum[2]); }else if(strword[0].equals("-s-a")) { File file = new File(strword[1]); int[] linenum=wcf.diffline(file); System.out.println("该文件的空行数:"+linenum[0]); System.out.println("该文件的注释行数:"+linenum[1]); System.out.println("该文件的代码行数:"+linenum[2]); } }else { if(strword[0].equals("?")) { wcf.help(); }else if(strword[0].equals("-x")) { wcGUI.main(null); }else if(strword[0].equals("end")) { break; }else { System.out.println("命令输入错误,请重新输入!"); } } } } }
用户界面文件:wcGUI.java 命令:-x
package wc; import java.awt.EventQueue; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.io.BufferedReader; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStreamReader; import javax.swing.JFrame; import javax.swing.JPanel; import java.awt.BorderLayout; import javax.swing.JScrollPane; import javax.swing.JButton; import javax.swing.JFileChooser; import javax.swing.JTextArea; import javax.swing.JTextField; import javax.swing.JLabel; import javax.swing.SwingConstants; public class wcGUI { private JFrame frmWc; private JTextField codenum; private JTextField wordnum; private JTextField linenum; private JTextField spaceline; private JTextField nodeline; private JTextField codeline; File file; wcfunction wcf=new wcfunction(); /** * Launch the application. */ public static void main(String[] args) { EventQueue.invokeLater(new Runnable() { public void run() { try { wcGUI window = new wcGUI(); window.frmWc.setVisible(true); } catch (Exception e) { e.printStackTrace(); } } }); } /** * Create the application. */ public wcGUI() { initialize(); } /** * Initialize the contents of the frame. */ private void initialize() { frmWc = new JFrame(); frmWc.setTitle("wc"); frmWc.setResizable(false); frmWc.setBounds(280, 50, 800, 600); frmWc.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE); JPanel panel = new JPanel(); frmWc.getContentPane().add(panel, BorderLayout.CENTER); panel.setLayout(null); JScrollPane scrollPane = new JScrollPane(); scrollPane.setBounds(291, 0, 493, 562); panel.add(scrollPane); JPanel panel_1 = new JPanel(); scrollPane.setViewportView(panel_1); panel_1.setLayout(new BorderLayout(0, 0)); JTextArea textArea = new JTextArea(); textArea.setEditable(false); panel_1.add(textArea, BorderLayout.CENTER); JButton choosefile = new JButton("选择文件"); choosefile.setBounds(10, 32, 122, 23); panel.add(choosefile); JLabel filename = new JLabel("文件名:"); filename.setBounds(10,70,80,23); panel.add(filename); JTextArea filepath = new JTextArea(); filepath.setBounds(10, 95, 260, 40); filepath.setLineWrap(true); filepath.setWrapStyleWord(true); filepath.setEditable(false); panel.add(filepath); JButton code = new JButton("统计字符数"); code.setBounds(10, 277, 100, 30); panel.add(code); JButton word = new JButton("统计词数"); word.setBounds(10, 317, 93, 30); panel.add(word); JButton line = new JButton("统计行数"); line.setBounds(10, 357, 93, 30); panel.add(line); JButton diffline = new JButton("统计空行、注释行、代码行"); diffline.setBounds(10, 397, 224, 30); panel.add(diffline); codenum = new JTextField(); codenum.setBounds(120, 278, 93, 29); codenum.setEditable(false); panel.add(codenum); codenum.setColumns(10); wordnum = new JTextField(); wordnum.setBounds(113, 318, 93, 30); wordnum.setEditable(false); panel.add(wordnum); wordnum.setColumns(10); linenum = new JTextField(); linenum.setColumns(10); linenum.setBounds(113, 358, 93, 30); linenum.setEditable(false); panel.add(linenum); spaceline = new JTextField(); spaceline.setColumns(10); spaceline.setBounds(70, 437, 93, 29); spaceline.setEditable(false); panel.add(spaceline); nodeline = new JTextField(); nodeline.setColumns(10); nodeline.setBounds(70, 476, 93, 29); nodeline.setEditable(false); panel.add(nodeline); codeline = new JTextField(); codeline.setColumns(10); codeline.setBounds(70, 515, 93, 30); codeline.setEditable(false); panel.add(codeline); JLabel label = new JLabel("空行"); label.setHorizontalAlignment(SwingConstants.CENTER); label.setBounds(10, 437, 54, 29); panel.add(label); JLabel label_1 = new JLabel("注释行"); label_1.setHorizontalAlignment(SwingConstants.CENTER); label_1.setBounds(10, 476, 54, 29); panel.add(label_1); JLabel label_2 = new JLabel("代码行"); label_2.setHorizontalAlignment(SwingConstants.CENTER); label_2.setBounds(10, 515, 54, 30); panel.add(label_2); JLabel label_3 = new JLabel("个"); label_3.setBounds(216, 277, 54, 30); panel.add(label_3); JLabel label_4 = new JLabel("个"); label_4.setBounds(216, 317, 54, 30); panel.add(label_4); JLabel label_5 = new JLabel("行"); label_5.setBounds(216, 357, 54, 30); panel.add(label_5); JLabel label_6 = new JLabel("行"); label_6.setBounds(173, 437, 54, 30); panel.add(label_6); JLabel label_7 = new JLabel("行"); label_7.setBounds(173, 476, 54, 30); panel.add(label_7); JLabel label_8 = new JLabel("行"); label_8.setBounds(173, 515, 54, 30); panel.add(label_8); choosefile.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { JFileChooser filechooser = new JFileChooser("."); int result=filechooser.showOpenDialog(null); if(result == JFileChooser.APPROVE_OPTION){ file=filechooser.getSelectedFile(); if(file!=null) { if(textArea.getText()!=null) { textArea.setText(""); } if(filepath.getText()!=null) { filepath.setText(""); filepath.setText(file.getPath()); } try { InputStreamReader read = new InputStreamReader(new FileInputStream(file), "GB2312"); BufferedReader br = new BufferedReader(read); String line=null; while((line=br.readLine())!=null) { textArea.append(line+"\r\n"); } br.close(); } catch (IOException e1) { e1.printStackTrace(); } } } } }); code.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { if(file.exists()) { String filename = file.getAbsolutePath(); try { int chara=wcf.getCharacternumber(filename); codenum.setText(chara+""); } catch (IOException e1) { // TODO Auto-generated catch block e1.printStackTrace(); } } } }); word.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { if(file.exists()) { String filename = file.getAbsolutePath(); try { int word=wcf.getwordnumber(filename); wordnum.setText(word+""); } catch (IOException e1) { // TODO Auto-generated catch block e1.printStackTrace(); } } } }); line.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { if(file.exists()) { String filename = file.getAbsolutePath(); try { int line=wcf.getlinenumber(filename); linenum.setText(line+""); } catch (IOException e1) { // TODO Auto-generated catch block e1.printStackTrace(); } } } }); diffline.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { if(file.exists()) { try { int[] line=wcf.difflineGUI(file); spaceline.setText(line[0]+""); nodeline.setText(line[1]+""); codeline.setText(line[2]+""); } catch (IOException e1) { // TODO Auto-generated catch block e1.printStackTrace(); } } } }); } }
测试
已将程序打包成wc.exe文件,放在Github项目中。可直接打开exe文件输入命令和文件路径直接使用,也可使用CMD命令行运行wc.exe再输入命令和文件路径使用。
CMD命令行使用:
exe直接使用:
用户界面:
代码覆盖率
代码覆盖率:97.5%
PSP
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
Planning | 计划 | 45 | 60 |
· Estimate | · 估计这个任务需要多少时间 | 45 | 60 |
Development | 开发 | 1290 | 1540 |
· Analysis | · 需求分析 (包括学习新技术) | 80 | 120 |
· Design Spec | · 生成设计文档 | 40 | 60 |
· Design Review | · 设计复审 (和同事审核设计文档) | 40 | 40 |
· Coding Standard | · 代码规范 (为目前的开发制定合适的规范) | 30 | 30 |
· Design | · 具体设计 | 120 | 160 |
· Coding | · 具体编码 | 780 | 920 |
· Code Review | · 代码复审 | 80 | 90 |
· Test | · 测试(自我测试,修改代码,提交修改) | 120 | 120 |
Reporting | 报告 | 240 | 380 |
· Test Report | · 测试报告 | 120 | 220 |
· Size Measurement | · 计算工作量 | 30 | 40 |
· Postmortem & Process Improvement Plan | · 事后总结, 并提出过程改进计划 | 90 | 120 |
合计 | 1575 | 1980 |
总结
先是从课堂上更加意识到制定设计流程的重要性,了解到计划的设计不仅仅只存在于流程,对时间的规划也是非常重要的。在编写代码过程中实质遇到的困难是在如何实现词数的统计、正则表达式以及对文件目录的递归实现。最后通过学习正则表达式实现了对词数的统计,也进一步学习了正则表达式这一较为陌生的概念。也学会了一些新的插件和软件的使用:Eclipse的EclEmma插件用于代码覆盖率的统计;Git Bash软件对本地项目上传到Github使用;exe4j软件对JAVA项目进行EXE文件的打包使用。这次的项目作业对我来说是受益匪浅,对以后的项目实行有很大的帮助。