软件工程实践第二次作业---文件读取
| 这个作业属于哪个课程 | 软件工程-23年春季学期 |
|---|---|
| 这个作业要求在哪里 | 软件工程实践第二次作业—文件读取 |
| 这个作业的目标 | 1.完成对澳大利亚网球公开赛相关数据的收集。 2.实现一个能够对赛事数据进行统计的控制台程序。 3.撰写博客 |
| 其他参考文献 | CSDN、博客园 |
目录
- 一.Gitcode项目地址
- 二.PSP表格
- 三.解题思路描述
- 3.1 数据的获取
- 3.2 文件的读写
- 3.3 JSON数据的解析
- 3.4 input文件中信息的处理
- 四.接口设计和实现过程
- 4.1 程序的类
- 4.2 类中的函数
- 4.3 函数间的关系
- 五.关键代码展示
- 5.1 GetPlayersMes类
- 5.2 GetResultMes类
- 5.3 handel_mes函数
- 5.4 writeMes函数
- 六.性能改进
- 6.1 可改进点的分析
- 6.2 相应的解决方法
- 七.单元测试
- 7.1 单元测试代码
- 7.2 覆盖率测试情况
- 八.异常处理
- 九.心得体会
一.Gitcode项目地址
仓库地址
二.PSP表格
| PSP | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
|---|---|---|---|
| Planning | 计划 | 20 | 15 |
| • Estimate | • 估计这个任务需要多少时间 | 20 | 15 |
| Development | 开发 | 1000 | 1785 |
| • Analysis | • 需求分析 (包括学习新技术) | 60 | 120 |
| • Design Spec | • 生成设计文档 | 30 | 20 |
| • Design Review | • 设计复审 | 10 | 10 |
| • Coding Standard | • 代码规范 (为目前的开发制定合适的规范) | 30 | 40 |
| • Design | • 具体设计 | 30 | 15 |
| • Coding | • 具体编码 | 750 | 1080 |
| • Code Review | • 代码复审 | 30 | 20 |
| • Test | • 测试(自我测试,修改代码,提交修改) | 60 | 480 |
| Reporting | 报告 | 70 | 80 |
| • Test Repor | • 测试报告 | 30 | 40 |
| • Size Measurement | • 计算工作量 | 10 | 20 |
| • Postmortem & Process Improvement Plan | • 事后总结, 并提出过程改进计划 | 30 | 20 |
| 合计 | 1090 | 1880 |
三.解题思路描述
3.1数据的获取
- 打开所要获取数据的网站(澳大利亚网球公开赛官网),找到界面上方导航栏TOURNAMENT下的result,点击进入后按F12打开开发者工具。
- 在开发者工具界面上点击网络,选择Fetch/XHR筛选后按Ctrl+R进行刷新,即会显示界面出现对应的加载文件。
3.找到名称为result的文件,获得其请求URL。打开后即可得到JSON数据。
3.2 文件的读写
- 文件的读取利用FileInputStream来获取文件中的数据,并用StringBuffer将读取的数据拼接成一整个字符串。
- 文件的写入利用FileOutputStream,这种方法下次打开写入会把原文件中的内容覆盖掉;也可利用BufferedWriter + FileOutputStream进行写入,可以不覆盖掉原文件中原来的信息。
3.3 JSON数据的解析
- 预先下载好GSON的jar包,并将其导入到IDEA中。(具体操作流程)
- 利用 Map map = new Gson().fromJson(s,Map.class) 将json反序列化成Map,这样就不需要自己去创建一个与JSON属性相匹配的类,避免了在创建类过程中漏掉一些信息而导致编译错误。
- 再用map.get(“aaa”)去获取自己需要的信息。
- 最后利用String来拼接获得的数据用于最后的输出。
3.4 input文件中信息的处理
- 先利用 BufferedReader 来按行读取文件内容。
- 再编写一个方法对每一行的内容进行处理判断其属于什么类型以及其是否符合要求。
- 然后根据不同类型的正确指令去获取不同文件中的数据。
四.接口设计和实现过程
4.1 程序的类
//包含main函数,用于调用其他类,并将正确的命令行参数传给其他类public class AOSearch{}//用于文件的读写和input内容的处理public class Lib{}//用于处理并获取Players的信息public class GetPlayersMes{}//用于处理并获取Result的信息public class GetResultMes{}
4.2 类中的函数
- Lib类中的函数
//用于启动程序
//获取input文件的内容,并把得到所有的数据写入到output文件中
public static void run_procedure(String arg1,String arg2) throws IOException;//获得input中每个正确指令所要输出的数据
public static String get_mes(String s) throws IOException;//用于判断input中每行指令的类型
public static String handel_mes(String s);//把一个文件中的内容读取成一个String字符串
public static String getStr(File jsonFile) throws IOException;//用于文件的打开
public static File open_file(String s) throws IOException;//用于检测字符串是否全为数字
public static boolean isNumeric(String s);//用于把获取到的数据写入到文件中
public static void writeMes(ArrayList ss, String arg2) throws IOException;
- GetPlayersMes类中的函数
//从数据中获取需要的选手信息并拼接
public static String getMes(String s);
- GetResultMes类中的函数
//从数据中获取需要的比赛结果信息并拼接
public static String getMes(String s);//获得选手的缩写名
//先通过team_id先获得选手的uuid ,再用uuid获得缩写名
public static String get_short_name(Map map,String win);//把选手的uuid转换成选手的缩写名
public static String uuid_to_name(Map map,String s);
4.3 函数间的关系
- main函数调用run_procedure(…)用于启动程序
Lib类中函数间的关系:
- run_procedure函数
调用get_mes(…)用于获得需要输出的数据
调用writeMes(…)用于输出获得的数据 - get_mes函数
调用handel_mes(…)用于处理并给input中指令分类
调用open_file(…)用于打开文件
调用getStr(…)用于把文件中数据转换成字符串
调用GetPlayersMes.getMes(…) or GetResultMes.getMes(…)用于从字符串中获取需要的数据 - handel_mes函数调用isNumeric(…)用于判断字符串是否全为数字
GetResultMes类中函数间的关系:
- getMes函数调用get_short_name(…)用于获得选手的缩写名
- get_short_name函数调用uuid_to_name(…)用于把选手的uuid转换成缩写名
五. 关键代码展示
5.1 GetPlayersMes类
算法关键:
- GetPlayersMes类中只有一个getMes函数用于获取选手信息。
- 将json数据反序化成Map类型,再通过map.get(“…”)去找到所需要的数据。
- 利用StringBuilder来拼接并存储要输出的数据。
//获得选手信息public static String getMes(String s){StringBuilder sb = new StringBuilder();//存储要输出最终的数据//将json反序列化成MapMap map = new Gson().fromJson(s,Map.class);List
5.2 GetResultMes类
算法关键:
- GetResultMes类是用于获取比赛结果的i相关信息。
- json数据的处理方式与GetPlayersMes类相同,但是更为复杂,需用多个循环才可找到相应数据。
- 其中最为麻烦的是得到获胜选手的缩写名,需通过得到的team_id去获得选手的uuid,再用uuid获得缩写名。
- 下面是获取选手缩写民的部分代码:
//通过team_id先获得选手的uuid 再用uuid获得缩写名public static String get_short_name(Map map,String win){String name = "";String[] short_names = new String[10];//记录每场选手个数及其idint k = 0;List teams = (List) map.get("teams");//根据比赛中team_id找到选手的uuidfor(Map team : teams){String uuid = team.get("uuid").toString();if(win.equals(uuid)){//获得team_id下的所有选手的uuidArrayList player_id = (ArrayList) team.get("players");short_names[k++] = String.valueOf(player_id.size());//每个team下的选手个数for(int j = 0;j < player_id.size();++j){short_names[k] = uuid_to_name(map,player_id.get(j).toString());//从选手的uuid转换成缩写名并存储++k;}break;}}}//数据从选手的uuid转换成缩写名public static String uuid_to_name(Map map,String s){List players = (List) map.get("players");for(Map player : players){String uuid = player.get("uuid").toString();if(uuid.equals(s))return player.get("short_name").toString();}return null;}
5.3 handel_mes函数
算法关键:
- handel_mes函数位于Lib类中,是用于处理input中每行指令的数据并判断其的类型。
- 利用trim()去除掉前后的所以空格,然后来判断其长度是否符合要求。
- 判断日期时,将指令分为左右两个部分,右半部分用自定义的isNumeric(…)检测后转换成int类型,来判断范围是否在规定日期内。其余的直接用equals(…)比较。
- 指令类型分为四类:-1为无法识别的错误指令;0为识别不到日期的错误指令;1为players的正确指令;2为result 0xxx类型的正确指令;3为result Qx类型的正确指令。
//处理并判断input中指令的类型//0表示N/A -1表示error 1表示players 2表示result 0xxx类型 3表示result Qx类型public static int handel_mes(String s){s = s.trim();//去掉前后空格int length = s.length();//判断字符串指令是否符合格式要求if(length < 6) return -1;else{if(length == 6){if(s.equals("result")) return 0;else return -1;}else if(s.equals("players")) return 1;else{int day = 0;//存储日期String l = s.substring(0, 7);if(l.equals("result ")){String r = s.substring(7,length);r = r.trim();//去掉前后空格if(r.equals("Q1") || r.equals("Q2") || r.equals("Q3") || r.equals("Q4"))return 3;else if(r.length() != 4) return 0;else if(!isNumeric(r)) return 0;else{day = Integer.parseInt(r);if(day >= 116 && day <= 129)return 2;else return 0;}}else return -1;}}}//检测字符串是否全为数字public static boolean isNumeric(String s){for (int i = 0; i < s.length(); i++) {if (!Character.isDigit(s.charAt(i))) {return false;}}return true;}
5.4 writeMes函数
算法关键:
- writeMes函数位于Lib类中,用于把所要输出的数据写到文件中。
- ArrayList + 一个for循环可以避免文件的多次被打开,ArrayList中存储的是所有要输出的数据。
- 利用FileOutputStream进行文件的写入,for循环中的每次写入不会覆盖原有数据,但是在下次重新打开文件时再写会覆盖掉原来信息。
//把获得的数据写入到文件中public static void writeMes(ArrayList ss,String arg2) throws IOException {File f = new File(arg2);//判断文件是否存在,不存在创建一个文件if(!f.exists()){System.out.println("output.txt文件不存在,正在创建一个新的文件!");f.createNewFile();//创建一个新文件}//会覆盖掉原来信息的写入方法FileOutputStream fileOutputStream = new FileOutputStream(f);for(String s : ss)fileOutputStream.write(s.getBytes("utf-8"));fileOutputStream.flush();fileOutputStream.close();//不会覆盖掉文件原来信息的写入方法
// BufferedWriter out = new BufferedWriter(
// new OutputStreamWriter(new FileOutputStream(f,true)));
// out.write(s);
// out.close();}
六.性能改进
6.1 可改进点的分析
- 准备优化前应该先进行代码的分析,来粗略判断哪些地方减低了运行效率、是可以进行优化来缩短代码的运行时间,下一步就可以对其进行修改优化。
- 未优化前运行1k+行处理指令的时间:13秒188毫秒
分析点1 :字符串的拼接。利用String进行拼接会消耗比较长的时间,这是因为在拼接时它会使用StringBuilder,并调用append,之后再调用toString方法,每次都要这样,这样产生的代价就是很大的。
分析点2 :文件的读取方式。我原先的文件读取方式是用reader,这种方法进行了大量的I/O操作,而且每次都要把数据先读成byte字节,再进行转码,这样做效率就实在太慢了。
分析点3 :为了减少文件写入的次数,我原先是把所有要输出的数据拼接完再写入到文件中,这样如果数据量大的话可能会超出字符的最大存储数量,会导致数据没法完全输出或者出现bug。
分析点4 :当input文件里有大量的数据时,就有可能会出现很多相同的指令行,这时编译过程中就会处理重复的指令,有些代码可以避免重复执行,就可加大程序的运行效率。
6.2 相应的解决方法
分析点1:可以直接利用StringBuilder代替String来拼接字符串。修改完后我发现运行效率并没有快多少,经过测试和查阅资料后,发现原来我每次拼接的地方不超过3500+,在拼接次数少的情况下String和StringBuilder所消耗的时间并不会相差太明显。但是如果大量执行命令行,速度就会明显提升。
- 第一次优化结束后的运行时间:12秒126毫米
分析点2:直接用BufferedReader来进行文件读取,每次读取一行文件,相对于read方法而言,减少了I/O操作,修改完后会发现速度明显快了许多,看来减少程序的I/O次数在优化过程中是十分必要的。
- 第二次优化结束后的运行时间:4秒761毫米
分析点3:可以用ArrayList去存储每次指令处理后要输出的数据,然后在打开一次文件后用write.write进行多次写入,这样既可以避免多次打开文件进行读写,也可避免要写入的数据过大。
分析点4:可以用一个静态的HashMap用来存放已经获得过的数据,接下来如果碰到已经处理过的相同指令,就可以直接调取HashMap中对应的数据。优化后会发现速率大大提高。
- 这是优化后执行100k+正确指令的运行时间:
七.单元测试
7.1 单元测试代码
- 对AOSearch的单元测试
@org.junit.Testpublic void AOSearch_main() throws IOException {String[] args = {"input.txt","output.txt"};//正确的输入格式String[] error_args = {"input.txt"};//错误的输入格式AOSearch.main(args);AOSearch.main(error_args);}
- 对run_procedure函数的单元测试
@org.junit.Testpublic void run_procedure() throws IOException {String input_test = "src/input_test.txt";String output_test = "src/output_test.txt";String error_input_test = "src/in_test.txt";String error_output_test = "src/out_test.txt";Lib.run_procedure(input_test,output_test);//正确的input和output文件名Lib.run_procedure(input_test,error_output_test);//正确的input和错误的output文件名Lib.run_procedure(error_input_test,error_output_test);//错误的input和output文件名}
- 对handel_mes函数的单元测试
@org.junit.Testpublic void handel_mes() {assertEquals(0,Lib.handel_mes("result"));//错误的指令assertEquals(-1,Lib.handel_mes("res ult"));//错误的指令assertEquals(0,Lib.handel_mes("result Q"));//没有指定赛程assertEquals(-1,Lib.handel_mes("aaaaaaa"));//错误的指令assertEquals(-1,Lib.handel_mes("bbbbbb 0116"));//错误的指令assertEquals(0,Lib.handel_mes("result"));//缺少没有日期或赛程assertEquals(0,Lib.handel_mes("result 0115"));//日期超出范围assertEquals(0,Lib.handel_mes("result 121"));//日期格式不规范assertEquals(0,Lib.handel_mes("result Q6"));//赛程超出范围assertEquals(0,Lib.handel_mes("result a116"));//错误指令 日期格式错误assertEquals(2,Lib.handel_mes("result 0116"));//正确指令 中间多了个空格assertEquals(1,Lib.handel_mes("players"));//正确的players指令 输出运动员的信息assertEquals(1,Lib.handel_mes("players "));//正确的players指令 输出运动员的信息assertEquals(3,Lib.handel_mes("result Q2"));//正确的result指令 输出Q2赛程的信息assertEquals(2,Lib.handel_mes("result 0120"));//正确的result指令 输出1月20号赛程的信息assertEquals(2,Lib.handel_mes(" result 0116 "));//正确的result指令 输出1月16号赛程的信息assertEquals(-1,Lib.handel_mes("player"));assertEquals(-1,Lib.handel_mes("Players"));assertEquals(-1,Lib.handel_mes("result0116"));assertEquals(0,Lib.handel_mes("result 1312"));assertEquals(0,Lib.handel_mes("result sss"));assertEquals(0,Lib.handel_mes("result 116"));assertEquals(0,Lib.handel_mes("result 0130"));assertEquals(0,Lib.handel_mes("result Q5"));assertEquals(1,Lib.handel_mes("players"));}
- 对open_file函数的单元测试
@org.junit.Testpublic void open_file() throws IOException {Lib.open_file("src/data/players.json");//可找到相应文件Lib.open_file("src/data/player.json");//未找到相应文件}
7.2 覆盖率测试情况

八.异常处理
- 打开文件异常处理
public static File open_file(String s) throws IOException
- 读取文件的异常处理
public static String getStr(File jsonFile) throws IOException
- 写入文件的异常处理
public static void writeMes(ArrayList ss,String arg2) throws IOException
- 运行过程中的异常捕获
try {Lib lib = new Lib();lib.run_procedure(args[0],args[1]);}catch (IOException e) {e.printStackTrace();}
九.心得体会
- 本次作业一开始我是蒙圈的,对于利用GitHub进行代码管理、单元测试、覆盖率测试、甚至是用命令行进行参数传入我都是一无所知的,只了解一些用Git上传本地文件的知识。起初我是有点不想写,因为太多东西不会,对于无知的东西,人往往都是有些抵触的。但是作业怎么能不交呢,所以呀,咱不会就查,不懂就问,直接冲就完事了。
- 经过上网查阅资料和同学间的探讨以及一系列的操作,最终还是完成了本次作业的编写。在学习的过程中,对以往没有接触的地方,我慢慢有了一定的认识,学会了怎么用Git管理代码,制定了自己的代码规范,了解该如何单元测试以及覆盖率测试。
- 当然,也会遇到一些问题,就比如在处理JSON数据时碰到的一些的bug、用cmd执行Java文件时找不到下载好的Gson第三方库,甚至还碰到字符串乱码的问题。还有,在做作业的过程中,我的建议是边写博客边写代码,如果像我一样最后再写博客,有些你上网找到的资料以及问题的解决方法,之后就很难再找回来了。
- 总之,这次作业我学到了许多,也能看到自己的一点点进步,虽然代码可能还有需要改进优化的地方,但千里之行始于足下,未来的路还长,继续努力加油。
本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场,不承担相关法律责任。如若转载,请注明出处。 如若内容造成侵权/违法违规/事实不符,请点击【内容举报】进行投诉反馈!






