142
社区成员




这个作业属于哪个课程 | 2022年福大-软件工程、实践-W班 |
---|---|
这个作业要求在哪里 | 软件工程实践第二次作业——个人实战 |
这个作业的目标 | GitCode项目上传、代码编写并测试、博客撰写 |
其他参考文献 | 参考文献见于文末 |
PSP | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | 25 | 20 |
• Estimate | • 估计这个任务需要多少时间 | 25 | 20 |
Development | 开发 | 640 | 1010 |
• Analysis | • 需求分析 (包括学习新技术) | 80 | 85 |
• Design Spec | • 生成设计文档 | 30 | 25 |
• Design Review | • 设计复审 | 20 | 20 |
• Coding Standard | • 代码规范 (为目前的开发制定合适的规范) | 30 | 40 |
• Design | • 具体设计 | 30 | 45 |
• Coding | • 具体编码 | 300 | 360 |
• Code Review | • 代码复审 | 30 | 35 |
• Test | • 测试(自我测试,修改代码,提交修改) | 120 | 400 |
Reporting | 报告 | 55 | 70 |
• Test Repor | • 测试报告 | 15 | 30 |
• Size Measurement | • 计算工作量 | 20 | 20 |
• Postmortem & Process Improvement Plan | • 事后总结, 并提出过程改进计划 | 20 | 25 |
合计 | 720 | 1105 |
之前有听说过17级的学长学姐的软工实践作业是疫情数据的收集和统计。因为是特别贴近我们生活的事情,当时觉得好厉害,原来到了大三可以完成这么有实际作用的项目。(我以为等到大三以后自己的编程能力能飞速成长,就可以像学长学姐一样。谁知道自己大三以后一看题目还是两眼一模黑,知道思路但是代码不会写)
我认为这次的题目和之前的疫情数据收集很相似,都是需要爬取网站的数据然后根据用户的需求将数据按照指定的格式输出。思路有了,不会那就学!对题目做了充分的分析之后,我觉得这次的题目主要有以下需求:
爬取冬奥会的相关数据并进行适当的处理
对获得的json对象进行解析
读取用户输入的文件,分析用户输入的指令
根据用户指令将相应的数据或是错误信息写入输出文件
注:本次数据的爬取行为仅用于课程教学
一开始以为要在程序中实现数据爬取,所以还急急忙忙地搜了好多数据爬取的教学视频 (之前只是听过爬虫,从来没有深入了解过T_T) 。
后来知道可以直接到网站上爬取数据然后存放在本地文件夹中,松了一口气~
根据作业的要求,我们需要得到2022年冬奥会的奖牌总榜数据以及每日赛程数据。下面演示爬取奖牌总榜数据的过程:
首先,需要打开央视的冬奥专栏,点击进入奖牌榜页面。然后按下F12
打开开发者模式,选择网络一栏并重新载入页面,便可以查阅浏览器详细的网络活动信息。
依次查看浏览器请求的文件,可以发现有一个名为getbjOlyMedals
的文件就是我们要找的奖牌榜的数据。
鼠标右键点击该文件在新的标签页中打开,就可以获得我们需要的源数据
——奖牌榜api。
由于每日赛程数据的获取过程与总榜相似,所以就不再进行演示了。在查找每日赛程的数据时,在冬奥专栏的每日赛程板块中获取数据,很容易发现参数startdatecn
代表的就是是对应的赛程日期,通过修改startdatecn字段的值就可以得到相应日期的赛程信息。
这里给出一个2月14日的赛程api:2月14日赛程
原本打算用Java语言发送网络请求来获取网页数据,但因为冬奥会的赛事数据是固定的且题目没有要求在程序中爬取数据,所以我直接复制api中的数据
进行处理。
由于从冬奥会专栏中复制下来的源数据只有一行,且其中的中文字符显示为Unicode编码,并非要求的UTF-8。为了便于后续程序的开发,要对数据进行适当的处理。
我先将获取的源数据放到vscode
中,将json数组两端多余的括号删去。再使用vscode-json
插件对所得数据进行格式化。这样就得到了与老师所给数据格式相同的数据。
因为没有在程序中爬取数据的需求,所以我将处理好的数据都保存在本地的文件夹
中。这样每次查询时就不需要再在网页中爬取数据,可以减少因网络请求而消耗的时间。
说来惭愧,因为自己开始的比较晚,大家在群里已经列举了很多有关输入指令的问题。所以我把所有可能出现的指令以及对应的输出都列举出来:
正确的指令: total和schedule 赛程日期
(赛程日期需在冬奥会比赛期间且格式为例如0203的四位数字)
日期不符合要求的指令: schedule 不符合要求的字符串
(如shcedule 1312、shcedule sss、shcedule 212、shcedule 0228等)
不正确的指令: 指令的第一个词不是total或schedule的字符串
(如schedule0202、Total、sss等)
根据以上分类,我使用split
函数按照空白字符对每行指令包含的词进行划分,由此来对指令分类处理。
对于正确的指令,我根据题目给的示范输出样例进行了分析。将json文件中将会用到的属性都提前罗列下来,保证数据的正确性。为后续json对象的解析提供一定便利。(事实证明,这样做是有好处的。把数据分析清楚能够让编程思路更清晰
,不至于手忙脚乱忽略了细节)
需求更新说可以使用第三方库之后,我一开始打算使用Gson来解析json对象。但是因为自己不太理解Gson的使用并且没有看到非常适合的教程,中途想过不使用第三方库,直接用字符串函数来解析json对象。
然后在DDL即将到来之际,我看到了一篇博客:就是这篇就是这篇! 这篇博客非常详细地讲解了使用jsonObject
对象解析json文件的过程。因为上学期Android课上老师讲的json解析就是采用的jsonObject对象。我看完之后茅塞顿开,仿佛找到了救命稻草,火速转成使用Fastjson
来解析json文件。
以下是json解析部分的代码:
//获取奖牌总榜列表
public static ArrayList<CountryRank> getMedalsList() throws IOException{
ArrayList<CountryRank> medalsList =new ArrayList<>(); //存放奖牌总榜列表
//运用JSONObject对象实现json字符串到JavaBean对象之间的转换
JSONObject object= JSONObject.parseObject(jsonToString("./src/data/total.json")); //获得json字符串对应的对象
JSONObject data=object.getJSONObject("data"); //获得data对应的json对象
JSONArray list=data.getJSONArray("medalsList"); //获得data对象中的medalsList对象数组
for (int i=0;i<list.size();i++){
JSONObject o=list.getJSONObject(i); //通过数组下标获取json对象
//将json字符串转换为JavaBean对象
CountryRank country=JSON.parseObject(o.toJSONString(),CountryRank.class);
medalsList.add(country);
}
return medalsList; //返回赛程总榜列表
}
BufferedReader
和BufferedWriter
对象。静态的ArrayList
中,等所有指令都处理完后再将结果输出到文件中。这样只需要打开一次
输出文件就可以完成结果的写入。//主类,负责文件中指令的读取、分类指令并调用其他类中指令处理函数、调用输出函数
public class OlympicSearch {...}
//工具类,负责完成程序的主要功能
public class Lib {...}
//存放国家奖牌榜信息的类
class CountryRank{...}
//存放赛程信息的类
class Match{...}
//指令的输出列表
ArrayList<String> resultList=new ArrayList<String>();
//程序的入口函数,负责读取输入文件中的指令并调用Lib中的函数分类处理,最后输出至文件中
public static void main(String[] args){...}
//使用HashMap存放已经查找过的数据,每次输出时就只需要查表获来获取数据
static HashMap<String,ArrayList<String>> presentData=new HashMap<>();
//判断输入的指令类型(-1:整行空格;0:赛程信息;1:日期非法指令;2:奖牌总榜;3:错误指令
public static int judgeInstruction(String instruction){...}
//读取json文件内容并转换为json格式字符串
public static String jsonToString(String filename) throws IOException{...}
//实现json字符串到JavaBean对象数组的转换,返回奖牌总榜对象数组
public static ArrayList<CountryRank> getMedalsList() throws IOException{...}
//实现json字符串到JavaBean对象数组的转换,获取对应日期的赛程信息
public static ArrayList<Match> getMatchList(String date) throws IOException{...}
//返回奖牌总榜对应的字符串数组,用于主函数中输出结果的拼接
public static ArrayList<String> showTotal() throws IOException{...}
//返回对应日期赛程的字符串数组,用于主函数中输出结果的拼接
public static ArrayList<String> showMatchList(String instruction) throws IOException{...}
//返回输入错误的提示信息,用于主函数中输出结果的拼接
public static String showError(){...}
//返回N/A信息的提示信息,用于主函数中输出结果的拼接
public static String showNA(){...}
//判断日期是否合法,日期应该为合法日期且是冬奥会的比赛时间
public static boolean isRightDate(String date){...}
//将数据写入输出文件中
public static void writeFile(String filename,ArrayList<String> resultList) throws IOException{...}
//去除文件末尾的换行符
public static void deleteEndLine(String filename) throws IOException{...}
private String rank; //国家奖牌数排名
private String countryid; //国家名称缩写
private String gold; //金牌数
private String silver; //银牌数
private String bronze; //铜牌数
private String count; //奖牌总数
//生成符合输出格式的国家奖牌榜信息的字符串
public String toString() {...}
//以及所有字段对应的get和set函数
...
private String startdatecn; //比赛时间
private String itemcodename; //运动类别
private String title; //赛事名称
private String homename; //对抗方一国家名
private String awayname; //对抗方二国家名
private String venuename; //比赛场地
//生成符合输出格式的赛程信息的字符串
public String toString() {...}
//以及所有字段对应的get和set函数
...
1. main函数:
调用Lib.judgeInstruction
函数进行指令类型的判断;
调用Lib.showMatchList
函数获得指定日期的赛程信息对应的字符串列表
调用Lib.showTotal
函数获得奖牌总榜对应的字符串列表。
调用Lib.showError
函数获得错误信息对应的字符串
调用Lib.showNA
函数获得格式错误信息的字符串
调用Lib.writeFile
函数将结果列表输出到输出文件中
2. Lib.judgeInstruction函数:
isRightDate
函数来判断日期是否符合要求3. Lib.showMatchList函数:
调用Lib.getMatchList
函数获得指定日期赛程信息的Match对象列表
调用Match.toString
函数获得指定格式的输出字符串
4. Lib.showTotalList函数:
调用Lib.getMatchList
函数获得指定日期赛程信息的Match对象列表
调用CountryRank.toString
函数获得指定格式的输出字符串
5. Lib.getMatchList函数和getMedalsList函数:
Lib.jsonToString
函数输入对应的json文件路径来获取相应json文件对应的json格式的字符串。6. Lib.writeFile函数:
Lib.deleteEndLine
函数来删除文件末尾多余的换行符split
函数对每一行指令按照空白字符切割。根据切割而来的数组判断指令的类型:-1
:表示指令整行都为空白字符,跳过不处理
0
:表示指令符合输出赛程信息的指令规范,主函数中将添加赛程
信息1
:表示指令对应的赛程日期不符合要求,主函数中将添加N/A
信息2
:表示指令符合输出奖牌榜的规范,主函数中将添加奖牌榜
信息3
:表示指令为无法识别的指令,主函数中将添加Error
信息//-1:整行空格;0:赛程信息;1:日期非法指令;2:奖牌总榜;3:错误指令
public static int judgeInstruction(String instruction){
//因为指令前后多余的空格忽略,所以使用split(“\\s+”)来删去所有的空白字符,包括空格、制表符、换页符
String regex="\\s+";
String ins[]=instruction.trim().split(regex); //将每一行指令按照空白字符切割
if (ins[0].equals(""))
return -1; //读到整行都为空格,则跳过
//用户输入指令为输出赛程信息
if (ins[0].equals("schedule")){
if (ins.length==2&& Lib.isRightDate(ins[1]))
return 0; //日期格式符合要求
else //用户输入的赛程日期不合法
return 1;
}
//用户输入指令为输出奖牌榜
else if (ins[0].equals("total")){
if (ins.length==1)
return 2;
else //该行除了total指令外还有其他语句,属于无法识别的指令
return 3;
}
else //其余输入均为错误输入
return 3;
}
BufferReader
对象读取json文件中的内容,然后利用stringBuffer
对象进行字符串的拼接,最后返回json格式的字符串
。//读取json文件内容并转换为json字符串
public static String jsonToString(String filename) throws IOException{
String jsonLine; //存放json文件的每一行
StringBuffer stringBuffer=new StringBuffer(); //用来进行字符串的拼接
BufferedReader reader = new BufferedReader(new
InputStreamReader(new FileInputStream(filename), StandardCharsets.UTF_8));
while ((jsonLine=reader.readLine())!=null)
stringBuffer.append(jsonLine); //将json文件的每一行拼接成一个字符串
reader.close(); //关闭输入流
return stringBuffer.toString();
}
parseObject
方法将json格式的字符串转换为json对象。再通过getJSONObject
方法通过键值获取赛程信息的json对象数组matchList
。最后利用parseObject
方法将json对象转换为Match对象,并返回Match对象的列表
。获取奖牌总榜的getMedalsList函数同理。//获取对应日期的赛程信息并返回
public static ArrayList<Match> getMatchList(String date) throws IOException{
ArrayList<Match> matchList =new ArrayList<>(); //存放赛程列表
//运用JSONObject对象实现json字符串到JavaBean对象之间的转换
//获得json字符串对应的对象
JSONObject object= JSONObject.parseObject(jsonToString("./src/data/schedule/"+date+".json"));
JSONObject data=object.getJSONObject("data"); //获得data对应的json对象
JSONArray list=data.getJSONArray("matchList"); //获得data对象中的matchList对象数组
for (int i=0;i<list.size();i++){
JSONObject o=list.getJSONObject(i); //通过数组下标获取json对象
//将json字符串转换为JavaBean对象
Match match=JSON.parseObject(o.toJSONString(),Match.class);
matchList.add(match);
}
return matchList; //返回赛程列表
}
split
函数切割指令获得用户指定的赛程日期。再将日期作为键值,判断presentData表
中是否存在该日期对应赛程信息。presentData表
中方便下次查找。//返回对应日期的赛程信息
public static ArrayList<String> showMatchList(String instruction) throws IOException{
String regex="\\s+"; //筛选所有空白字符
String ins[]=instruction.trim().split(regex); //将每一行指令按照空白字符切割
String date=ins[1]; //获得指定的日期
//若当前表中已有对应日期的赛程数据则直接返回
if (presentData.containsKey(date))
return presentData.get(date);
//若当前没有对应日期的赛程数据则获取数据并添加到已有数据表中
ArrayList<Match> list=getMatchList(date); //获得赛程信息列表
ArrayList<String> matchList=new ArrayList<>(); //存放输出的字符串列表
for (Match country:list)
matchList.add(country.toString());
presentData.put(date,matchList); //将数据添加到presentData表中方便下次查找
return matchList;
}
StringBuffer
对象进行字符串的拼接。对于对抗赛事的输出,我观察了以下原本的json文件,发现非对抗赛事的homename
数据为空。所以我从homename字段入手,通过判断该字段是否为空来选择是否加上对抗信息。//生成赛程信息的字符串
public String toString() {
StringBuffer stringBuffer=new StringBuffer(); //用来进行字符串的拼接
stringBuffer.append("time:");
stringBuffer.append(startdatecn.substring(11,16)); //将时间的时和分切割出来
stringBuffer.append("\nsport:");
stringBuffer.append(itemcodename);
stringBuffer.append("\nname:");
stringBuffer.append(title);
if (!homename.equals("")){ //出战国家名非空,说明为对抗赛事
stringBuffer.append(" ");
stringBuffer.append(homename);
stringBuffer.append("VS");
stringBuffer.append(awayname);
}
stringBuffer.append("\nvenue:");
stringBuffer.append(venuename);
stringBuffer.append("\n-----\n");
return stringBuffer.toString();
}
BufferedWriter
对象将输出列表的数据写入输出文件中,最后去掉末尾的换行符。//将数据写入输出文件中
public static void writeFile(String filename,ArrayList<String> resultList) throws IOException{
File file=new File(filename);
if (!file.exists()) //输出文件不存在则创建一个新的文件
file.createNewFile();
BufferedWriter writer = new BufferedWriter(new
OutputStreamWriter(new FileOutputStream(filename), StandardCharsets.UTF_8));
for (String result:resultList) //依次向文件中输出结果
writer.write(result);
writer.close(); //关闭输出流
deleteEndLine(filename); //去除文件末尾的换行符
}
我最开始尝试跑了一万条左右的数据,发现性能优化前后的运行时间相差并不太大。我还暗暗担忧是不是自己性能优化的效果不好。
结果在我尝试用优化前的程序跑了一百五十万条数据(input1.txt
)后,我看着我的命令行缓慢地输出,风扇呼呼地响,电脑烫的起飞。最后的最后,我等到的不是结果,是静止的电脑。因为程序占用的内存太大了,抛出了异常!(太久没清内存了T_T) 这时我才意识到,性能优化有多重要!
分析了一下代码发现,其实影响本次程序运行效率的主要因素有以下两点:
减少读取的次数
。我将已经输出过的奖牌总榜和赛事数据,都存放在HashMap
中。StringBuffer
对象进行字符串的拼接。 StringBuffer是可变类和线程安全的字符串操作类,任何对它指向的字符串的操作都不会产生新的对象。这样可以缩短因频繁创建对象而消耗的时间。为了进行性能测试,我构建了一个约有十七万条有效指令(只包含total和schedule 正确日期的指令)的数据文件input2.txt
。使用该文件作为输入文件,观察改进前后程序的运行时间。
未改进前,程序的运行时长为:286718ms
(jar包运行)。
改进后,程序的运行时长为:17391ms
(jar包运行)。
可以看到,才十万级的数据,性能就提高了大概十六倍
!多么可怕的数字哇~
我再使用之前一百五十万行左右的有效指令文件input1.txt
进行测试,运行时长为26169ms
(源码运行),此时输出文件大小为3.2G
。(没有异常了,电脑不卡了,好起来了!)
JProfiler分析如下:
由此可以见得性能优化在项目开发过程中真的很重要!
(在性能的调试过程中我还发现了一个很神奇的现象:一开始我直接在命令行运行jar包得到的程序的运行时间。但是因为要使用JProfiler插件来监控运行情况,所以我在IDEA中又运行了一遍原来的数据。对比二者运行时间后我发现:对于相同的指令文件,控制台程序的运行速度居然比命令行的jar包要快十倍以上
!但是很遗憾没有找到解释这种现象的博客,所以不太明白其中的缘由)
因为IDEA可以很方便地安装JUnit的单元测试插件,所以我在IDEA中使用JUnit4
进行单元测试。设计并计算好好要输入的指令和预期所得的结果,编写测试的代码并运行,对比实际的结果和预期结果是否相符,获得测试的结果,并观察代码的覆盖率。
@Test
void judgeInstruction() {
assertEquals(-1,Lib.judgeInstruction(" ")); //空格,跳过
assertEquals(0,Lib.judgeInstruction("schedule 0212")); //输出赛程的指令
assertEquals(1,Lib.judgeInstruction("schedule 1312")); //日期不合法的指令
assertEquals(1,Lib.judgeInstruction("schedule sss")); //日期不合法的指令
assertEquals(1,Lib.judgeInstruction("schedule 212")); //日期不合法的指令
assertEquals(1,Lib.judgeInstruction("schedule 0228")); //日期不合法的指令
assertEquals(2,Lib.judgeInstruction("total")); //输出赛程总榜的指令
assertEquals(3,Lib.judgeInstruction("Total")); \\错误的指令
assertEquals(3,Lib.judgeInstruction("schdule0202")); \\错误的指令
assertEquals(3,Lib.judgeInstruction("-sss")); \\错误的指令
}
@Test
void jsonToString() {
String content,result=new String();
String inputPath="test/jsonToStringIn.txt"; //输入的json文件的文件路径
String correctPath="test/jsonToStringCo.txt"; //预期结果的文件路径
StringBuffer buffer=new StringBuffer();
try { //读取预期结果文件中的数据并存在buffer中
BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream(correctPath), StandardCharsets.UTF_8));
while ((content=reader.readLine())!=null)
buffer.append(content);
result=Lib.jsonToString(inputPath); //获取jsonToString函数得到的实际结果
} catch (IOException e) {
e.printStackTrace();
}
assertEquals(buffer.toString(),result); //对比预期结果和实际结果
}
最终的覆盖率为98%
,除了一些异常处理没有办法自行抛出,其他部分代码都得到了覆盖。
本次作业的异常主要有以下几种:
RuntimeException: 主要是用户参数输入错误
的异常
FileNotFoundException: 文件不存在
的异常
IOException: 主要是与文件读写
有关的异常
主函数
中进行捕获并处理虽然之前做项目的时候有用过Git、GitHub,但是并没有真正掌握git作为版本控制工具的要义。通过这次作业我更加深入地了解了Git的相关知识,明白了版本控制的重要性,体会到了Git的便捷。
自己的知识面还是不够广,很多项目相关的知识还是不了解 (之前从来没听说过单元测试) 以前看到过一句话感觉说的很有道理:“不要停下前进的脚步,否则世界就会忘了你”
接下来的学习过程中还是要拓宽自己的知识面多接触新的知识,跳出舒适区,不断学习,这样才能提升自己的编程水平!希望自己能通过接下来的实践学到更多的项目开发的知识。 (认真脸)
一定要边编程边写博客,这样能及时将自己的解题思路记录下来。博客也能作为编写代码时的参考,可以不断完善内容。最后才写博客容易忘东忘西,思路不清晰。拒绝拖延症!!!早点开始写真的会从容很多! (自己的经验教训T_T)
性能的优化对于软件的开发来说真的很重要!虽然输出的结果是相同的,都能满足客户的需求,但是好的程序能给用户最好的体验。这一定是未来的软件开发生涯中必须要时刻注意的。
控制台程序的运行速度居然比命令行的jar包要快十倍以上!但是很遗憾没有找到解释这种现象的博客,所以不太明白其中的缘由
控制台是指你的 IDE么?