寒假作业(2/2)——疫情统计
1 作业描述
这个作业属于哪个课程 | https://edu.cnblogs.com/campus/fzu/2020SpringW/ |
---|---|
这个作业要求在哪里 | https://edu.cnblogs.com/campus/fzu/2020SpringW/homework/10281 |
这个作业的目标 | 最近新型冠状病毒疫情严重,全国人民都感到担忧,迫切希望能够及时了解到病毒最新的情况,作为IT学子,大家请你帮忙开发一个疫情统计程序。 |
作业正文 | |
其他参考文献 | 菜鸟教程,廖雪峰的博客,bilibili相关课程,github,学长参考博文 |
2 PSP表格
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | 45 | 50 |
Estimate | 估计这个任务需要多少时间 | 3360 | 20 |
Development | 开发 | 1000 | 1200 |
Analysis | 需求分析 (包括学习新技术) | 720 | 800 |
Design Spec | 生成设计文档 | 480 | 180 |
Design Review | 设计复审 | 60 | 30 |
Coding Standard | 代码规范 (为目前的开发制定合适的规范) | 40 | 30 |
Design | 具体设计 | 60 | 80 |
Coding | 具体编码 | 500 | 700 |
Code Review | 代码复审 | 30 | 30 |
Test | 测试(自我测试,修改代码,提交修改) | 600 | 700 |
Reporting | 报告 | 450 | 500 |
Test Report | 测试报告 | 180 | 180 |
Size Measurement | 计算工作量 | 30 | 30 |
Postmortem & Process Improvement Plan | 事后总结, 并提出过程改进计划 | 60 | 50 |
Summary | 合计 | 1495 | 1750 |
3 解题思路描述
在看到题目之后,我首先阅读了一遍要求,又看了一下助教给的相关提示,我首先需要梳理一下大致的流程:
读取命令->解析参数->根据参数执行命令->读取日志文件获得要存储的内容->保存日志到文件
在这个过程中命令要是可扩展的,参数最好也是也扩展的,用正则表达式匹配字符串最好也比较容易扩展,与此同时要有一些简单的办法来处理这些问题,根据助教的提示我准备尝试使用命令模式和责任链模式.
在完成以上简单的思考之后我准备先学习额外的知识包括设计模式,git的使用,对于git的使用我准备在知乎上寻找对应入门教程,对于设计模式我准备在bilibili上寻找对应教程.除此之外我准备使用idea作为我的开发工具,当然idea的一些高级技巧我也要学习使用,有很多博客提供参考.
经过以上准备之后我开始思考如何做出这道作业,我刚开始思考的时候是传进来的参数必须要改变它的结构封装成为一个map的形式,方便根据参数名找到参数值,并且这个类不对参数做过多的操作,更进一步的操作由具体的命令来执行,这里只起到一个简单的封装的功能.在一开始我还没想好怎么处理除了log,out之外的参数,所以我暂时先放一边,准备先完成从从日志中读取内容并处理之后写入文件上(参数先固定住),这样一来以便完成简单的功能.
根据助教的提示,对于匹配每一行我选择使用正则表达式,并使用责任链模式,以便减少if-else结构的使用,对于写入文件,我希望我最终得到的可以写入文件的信息是规整的只需要根据命令的参数就可以写入文件,所以我选择了map结构存放我的结果,我选择map的键为省份名,map的值为一个Integer数组,全部初始化为0,代表了感染人数,疑似人数,治愈人数,死亡人数,这样上面正则表达式模块也可以返回这种格式,处理起来就比较方便.
最后对于命令的执行和处理日志文件之间的衔接我当时还没想好,准备先写完这两个模块之后在衔接.具体的细节可能得等到实现的时候才发现有些错误.
4 设计实现过程
整个流程为 main
->ArgParser
->ListCommand
->AllInformation
->RegXXX
->AllInformation
4.1 参数解析器ArgParser
这个类就是将String[] args
类型的数组变换为Map<String,List<String>>
类型的数据结构,这样就可以简单地通过参数名得到参数值,里面还有一个打印参数信息的方法,能够进行简单的调试.关键函数流程图为,
4.2 命令执行ListCommand
这个模块是整个程序中第二个重要的类,这个类的设计采用了设计模式中的命令模式,有一个接口Command,两个类实现了这个接口一个是,NoCommand,另一个是ListCommand,还有一个类为CommandReceiver,里面有命令对应的方法,例如本程序中有list命令,那么该类中就有一个list方法用来执行list命令,这样以后有其他命令需要扩展的时候只需要实现Command接口,并在CommandReceiver里面定义对应的方法即可,流程图如下
4.3 正则表达式RegXXX
这个模块主要是用来实现对于日志文件中的每一行进行处理,通过把日志文件中的每一行交给正则表达式模块处理,返回一个可以被AllInformation处理的数据格式,在这里由于由8个正则表达式,如果使用if-else模块会比较繁琐,所以我决定使用责任链模式,由于有8个正则表达式类,并且每一个类的内容都大同小异,因此这里只展示一个类的内容
虽然我已经实现了责任链模式来处理但是我发现处理了8个正则表达式之后会出现一个栈溢出的现象,在查找了资料发现有可能是正则表达式是采用递归实现的,所以出现了栈溢出,我尝试了一些方法但是无法解决,因此暂时放弃了使用责任链模式,使用简单的if-else模式,逻辑图如下:
4.4 写入文件AllInformation
这个类应该是最重要的一个类了里面至少有以下几个方法,能够读取日志文件内容并对其进行处理,能够判断每一行应该有哪一个正则表达式处理,将处理后的信息写入到文件中
他有私有变量,以及8个正则表达式
Map<String,Integer[]> info;
String REGEX1 = "(\\S+) 新增 感染患者 (\\d+)人";
String REGEX2 = "(\\S+) 新增 疑似患者 (\\d+)人";
String REGEX3 = "(\\S+) 感染患者 流入 (\\S+) (\\d+)人";
String REGEX4 = "(\\S+) 疑似患者 流入 (\\S+) (\\d+)人";
String REGEX5 = "(\\S+) 死亡 (\\d+)人";
String REGEX6 = "(\\S+) 治愈 (\\d+)人";
String REGEX7 = "(\\S+) 疑似患者 确诊感染 (\\d+)人";
String REGEX8 = "(\\S+) 排除 疑似患者 (\\d+)人";
整个类的逻辑如下
5 代码说明
运行方法:下载代码到本地之后进入221701139/src目录之下,打开命令行,执行命令即可
java InfectStatistic list -log 221701139/log -out D:/output.txt
注意目录必须是221701139/log
,输出目录可以随便,其他参数根据自己需要设置即可,系统会按照下面语句执行函数
public static void main(String[] args) throws IOException {
Lib lib = new Lib(args);
lib.execute();
}
我们按上面提到的整体流程和程序的运行过程来展示关键代码:
5.1 参数解析器ArgParser
参数解析器里面关键的就是它的构造函数,将String[] args,变换为Map类型,代码如下
public ArgParser(String[] args) {
this.command = args[0];
this.arguments = new HashMap<>();
// 根据参数列表创建参数映射
for (int i = 1;i<args.length;i++) {
// 如果以-开头代表是参数名
if (args[i].startsWith("-")) {
String key = args[i];
int j = i+1;
List<String> vals = new ArrayList<>();
while (!args[j].startsWith("-")) {
vals.add(args[j]);
j++;
if (j==args.length) {
break;
}
}
arguments.put(key,vals);
}
}
}
程序很简单,举个例子即可明白,对于命令
list -log xxx -out aaa -type ip sp
会变为以下格式的map
-log -> [xxx]
-out -> [aaa]
-type -> [ip,sp]
5.2 命令执行ListCommand
这个模块包括了一个接口Command,两个类实现了这个接口一个是,NoCommand,另一个是ListCommand,还有一个类为CommandReceiver,里面有命令对应的方法,例如本程序中有list命令,那么该类中就有一个list方法用来执行list命令,Command接口只有一个函数execute()所以就不介绍了,主要介绍ListCommand,和CommandReceiver
ListCommand的构造函数为
public ListCommand(String[] args,CommandReceiver receiver) {
this.args = args;
this.receiver = receiver;
}
由于listCommand方法实现了Command接口一次复写方法
除此之外它还有一个重要的方法judgeType用来判断type参数并进行转换
public static List<Integer> judgeType(ArgParser argParser) {
List<String> types = argParser.getVals("-type");
List<Integer> newTypes = new ArrayList<>();
// 如果没有给type参数就按顺序输出
if (types==null) {
newTypes.add(0);
newTypes.add(1);
newTypes.add(2);
newTypes.add(3);
return newTypes;
}
// 如果有给type参数就按type参数值的顺序输出
for (String type:types) {
if (type.equals("ip")) {
newTypes.add(0);
}
else if (type.equals("sp")) {
newTypes.add(1);
}
else if (type.equals("cure")) {
newTypes.add(2);
}
else if (type.equals("dead")){
newTypes.add(3);
}
}
return newTypes;
}
例如type参数值为 ip cure,那么会被替换为[0,2]这个被替换的值就会被之后的程序利用
CommandReceiver目前里面只有一个函数就是list函数它用来执行具体的命令,实现如下
public void list(String[] args) {
// 封装参数
ArgParser argParser = new ArgParser(args);
argParser.printArg();
// 根据参数调用不同的类
String root = argParser.getVals("-log").get(0);
String outFileName = argParser.getVals("-out").get(0);
List<String> dates = argParser.getVals("-date");
//执行完成,输出提示信息
File path = new File(root);
// 读取文件
File[] files = path.listFiles();
String filePath = null;
if (dates!=null) {
// 如果提供了要处理的文件的日期名
for (File file:files) {
if (file.getName().contains(dates.get(0))) {
filePath = path + "/" + file.getName();
}
break;
}
}
else {
// 如果没有给定日期就使用文件的最后一个
File lastFile = files[files.length-1];
filePath = path+"/"+lastFile.getName();
}
AllInformation allInformation = new AllInformation();
allInformation.processInfo(filePath);
allInformation.writeIntoLog(outFileName,argParser);
allInformation.printInfo();
}
这个函数里面主要做了两件事一个是根据提供的date参数修改相关的文件名信息,第二个就是将参数传递给AllInformation类,由该类执行最后的工作
修改date相关参数主要是看命令执行时有没有给date参数,如果有给date参数就拼接处指定的路径,如果没有给date参数读取目标文件夹下最后一个文件。
5.3 正则表达式RegXXX
这个类主要的函数为process,由于这个相关的类有8个并且都很简单,因此我只展示其中的一个,具体实现请看下面
public class RegEight {
// 处理的正则表达式为
// (\\S+) 排除 疑似患者 (\\d+)人
// 返回一个省份对应信息的映射
public Map<String, Integer[]> process(String line) {
Map<String,Integer[]> map = new HashMap<>();
String[] result = line.split(" ");
// 获取省份
String province = result[0];
// 获取人数
Integer population = Integer.parseInt(result[3].substring(0,result[3].length()-1));
// 封装成map
Integer[] num = {0,-population,0,0};
map.put(province,num);
map.put("全国",new Integer[]{0,-population,0,0});
return map;
}
}
对于输入语句福建 排除 疑似患者 2人
,返回的数据为
全国 -> {0,-2,0,0}
福建 -> {0,-2,0,0}
写入文件AllInformation
这个类是最重要的一个类,我们按照程序中的处理流程来讲解这个类,首先是构造函数,暴力构造
public AllInformation() {
info = new LinkedHashMap<>();
info.put("全国",new Integer[]{0,0,0,0});
info.put("安徽",new Integer[]{0,0,0,0});
info.put("北京",new Integer[]{0,0,0,0});
info.put("重庆",new Integer[]{0,0,0,0});
info.put("福建",new Integer[]{0,0,0,0});
info.put("甘肃",new Integer[]{0,0,0,0});
info.put("广东",new Integer[]{0,0,0,0});
info.put("广西",new Integer[]{0,0,0,0});
info.put("贵州",new Integer[]{0,0,0,0});
info.put("海南",new Integer[]{0,0,0,0});
info.put("河北",new Integer[]{0,0,0,0});
info.put("河南",new Integer[]{0,0,0,0});
info.put("黑龙",new Integer[]{0,0,0,0});
info.put("湖北",new Integer[]{0,0,0,0});
info.put("湖南",new Integer[]{0,0,0,0});
info.put("吉林",new Integer[]{0,0,0,0});
info.put("江苏",new Integer[]{0,0,0,0});
info.put("江西",new Integer[]{0,0,0,0});
info.put("辽宁",new Integer[]{0,0,0,0});
info.put("内蒙",new Integer[]{0,0,0,0});
info.put("宁夏",new Integer[]{0,0,0,0});
info.put("青海",new Integer[]{0,0,0,0});
info.put("山东",new Integer[]{0,0,0,0});
info.put("山西",new Integer[]{