688
社区成员
发帖
与我相关
我的任务
分享| 这个作业属于哪个课程 | 2023年福大-软件工程实践-W班 |
|---|---|
| 这个作业要求在哪里 | 软件工程实践第二次作业 |
| 这个作业的目标 | 完成对澳大利亚网球公开赛相关数据的收集,并实现一个能够对赛事数据进行统计的控制台程序 |
| 其他参考文献 | 工程师的能力评估和发展、单元测试 & 回归测试、阿里巴巴JAVA开发手册、JAVA中文件写入的6种方法 |
oorty / Project-Java · GitCode
| PSP | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
|---|---|---|---|
| Planning | 计划 | ||
| • Estimate | • 估计这个任务需要多少时间 | 15 | 30 |
| Development | 开发 | ||
| • Analysis | • 需求分析 (包括学习新技术) | 40 | 50 |
| • Design Spec | • 生成设计文档 | 30 | 30 |
| • Design Review | • 设计复审 | 15 | 10 |
| • Coding Standard | • 代码规范 (为目前的开发制定合适的规范) | 20 | 25 |
| • Design | • 具体设计 | 20 | 30 |
| • Coding | • 具体编码 | 480 | 550 |
| • Code Review | • 代码复审 | 20 | 25 |
| • Test | • 测试(自我测试,修改代码,提交修改) | 180 | 220 |
| Reporting | 报告 | ||
| • Test Repor | • 测试报告 | 180 | 200 |
| • Size Measurement | • 计算工作量 | 15 | 15 |
| • Postmortem & Process Improvement Plan | • 事后总结, 并提出过程改进计划 | 20 | 15 |
| 合计 | 1050 | 1200 |
爬取数据
打开需要的网页,F12调出开发者工具,点击某日程,选择名为“ result ”的请求,因为并没有要求程序内爬取,请求的 Response 即为所需内容

指令解析
和读取指令方法放在一个类中,供主类调用,返回值表示不同的解析类型
JSON解析
一开始的想法是转化为bean处理,没有想到 json 结构这么复杂,对象数组各种嵌套,于是采用(暴力的)直接解析,使用 fastjson
这里还涉及处理后字符串的拼接,需要选择合适的类
文件的读写
这部分参考了网上许多的文章,在不断优化的过程中,自己对于这些读写类也有了更深的理解

AOSearch
仅含main函数,负责程序总的流程控制,调用其它方法
CommandReader
包含两个静态方法 read 和 check,分别负责指令的读取与检验
PlayerReader & TournamentReader
包含各自返回相应字符串的静态方法 getResult ,同时引入了缓存机制
赛程中获取winner是个比较麻烦的点,需要team_id -- player_id -- short_name这么一个过程。在遍历matches我使用ArrayList<String>保存获胜的team_id,在遍历teams时替换为对应的player_id,值得注意的是player_id可能不止一个,但该情况并不多见,于是放弃将数组改为ArrayList<String[]>(太浪费了),改用一个HashMap存储,键为player_id,值为对应的数组下标。(略显丑陋的做法
Tournament
赛程类对象,主要目的在于方便输出结果(toString,以及一些特殊输出)。为什么没有player对象?因为player解析没这么麻烦,不需要
核心控制流程
try (BufferedWriter bufferedWriter = new BufferedWriter(new FileWriter(outputPath,true))){
CommandReader.read(inputPath);
for(String s:CommandReader.command){
switch (CommandReader.check(s)){
case 0:
bufferedWriter.append("ERROR\n-----\n");
break;
case 1:
bufferedWriter.append(PlayerReader.getResult());
break;
case 2:
String date = s.substring(7);
bufferedWriter.append(TournamentReader.getResult(date));
break;
case 3:
bufferedWriter.append("N/A\n-----\n");
break;
}
}
}
catch (FileNotFoundException e){
System.out.println();
}
指令检验
/**
* 检验指令的合法性,返回结果
* @return 检测结果
* 0-ERROR 1-players 2-result 3-N/A
*/
public static int check(String command) {
//按空格拆分字符串
String[] s = command.split("\\s+");
if(s[0].equals("players")&& s.length==1){
return 1;
}
else if(s[0].equals("result")){
if(s.length != 2){
return 3;
}
//检验日期是否为 Q1 ~ Q4
if(s[1].charAt(0) == 'Q'){
if(s[1].length() == 2 && s[1].charAt(1) > '0' && s[1].charAt(1) < '5'){
return 2;
}
return 3;
}
//检验日期是否为 0116 ~ 0129
else if(s[1].charAt(0) != '0' || s[1].length()!=4){
return 3;
}
int date = 0;
try {
date = Integer.parseInt(s[1]);
} catch (NumberFormatException e) {
return 3;
}
if(date<116 || date>129){
return 3;
}
return 2;
}
return 0;
}
赛程处理
public static String getResult(String date) throws Exception {
// 读取缓存
String res = memory.get(date);
if(res != null){
System.out.println("return "+date+" memory");
return res;
}
//构造文件路径
String path = "src/main/java/data/"+date+".json";
File file = new File(path);
if (!file.exists()) {
throw new FileNotFoundException("赛程文件 " + date + ".json 不存在");
}
//转为字符流
InputStreamReader isr = new InputStreamReader(new FileInputStream(file), StandardCharsets.UTF_8);
StringBuilder sb = new StringBuilder();
int ch;
while ((ch = isr.read()) != -1) {
sb.append((char) ch);
}
isr.close();
//记录获胜者信息(team_id ~ player_id ~ name
ArrayList<String> winner = new ArrayList<>();
Map<String,Integer> anotherWinner = new HashMap<>();
List<Tournament> tournaments = new ArrayList<>();
JSONObject root = JSONObject.parseObject(sb.toString());
JSONArray matches = root.getJSONArray("matches");
for (int i = 0; i < matches.size(); i++) {
//双方比分
ArrayList<String> scores = new ArrayList<>();
//回合数
int n = 0;
//赛程对象
Tournament temp = new Tournament();
//记录比赛时间
String time = matches.getJSONObject(i).getString("actual_start_time");
//平局
if(time == null){
temp.setWO(true);
tournaments.add(temp);
continue;
}
temp.setTime(time);
JSONArray teams = matches.getJSONObject(i).getJSONArray("teams");
for (int l = 0; l < 2; l++) {
JSONArray sco = teams.getJSONObject(l).getJSONArray("score");
n = sco.size();
for (int p = 0; p < n; p++) {
scores.add(sco.getJSONObject(p).getString("game"));
}
}
//记录获胜者team_id
if (teams.getJSONObject(0).getString("status") != null) {
winner.add(teams.getJSONObject(0).getString("team_id"));
} else {
winner.add(teams.getJSONObject(1).getString("team_id"));
}
//记录比分
StringBuilder scoBuilder = new StringBuilder();
for (int q = 0; q < n; q++) {
scoBuilder.append(scores.get(q) + ":" + scores.get(q + 2));
//添加间隔
if (q != n - 1) {
scoBuilder.append(" | ");
}
}
temp.setScore(scoBuilder.toString());
tournaments.add(temp);
}
//记录获胜者player_id
JSONArray teams = root.getJSONArray("teams");
for (int j = 0; j < teams.size(); j++) {
int q = winner.indexOf(teams.getJSONObject(j).getString("uuid"));
if(q == -1){
continue;
}
JSONArray players = teams.getJSONObject(j).getJSONArray("players");
winner.set(q,players.getString(0));
//若获胜者不止一个
if (players.size() > 1) {
anotherWinner.put(players.getString(1),q);
}
}
//记录获胜者姓名
JSONArray players = root.getJSONArray("players");
for(int k=0;k<players.size();k++){
String uuid = players.getJSONObject(k).getString("uuid");
int q = winner.indexOf(uuid);
//数组没有,map找找
if(q == -1){
if(anotherWinner.containsKey(uuid)==true){
int t = anotherWinner.get(uuid);
anotherWinner.remove(uuid);
tournaments.get(t).setWinner(players.getJSONObject(k).getString("short_name"));
}
continue;
}
//数组有
tournaments.get(q).setWinner(players.getJSONObject(k).getString("short_name"));
}
StringBuilder sBuilder = new StringBuilder();
for(Tournament t:tournaments){
sBuilder.append(t.toString());
}
res = sBuilder.toString();
memory.put(date,res);
return res;
}
将字符串拼接操作由StringBuffer替换为 StringBuilder
对于字符串的修改,有String 、StringBuffer、StringBuilder三种方式
StringBuffer是线程安全的,公开方法中都有synchronized 修饰StringBuilder不是线程安全的,因此效率比StringBuffer高因为该程序为单线程,所以StringBuffer替换为 StringBuilder
替换前

替换后

覆盖率测试
未到达100%的原因在于异常处理部分,单元测试无法全部覆盖

基本包含了所有情况

读取文件失败,输入文件无法打开的异常
//in CommandReader
File file = new File(path);
if (!file.exists()) {
throw new FileNotFoundException("输入文件不存在");
}
//TournamentReader
File file = new File(path);
if (!file.exists()) {
throw new FileNotFoundException("赛程文件 " + date + ".json 不存在");
}
//PlayerReader
File file = new File(path);
if (!file.exists()) {
throw new FileNotFoundException("选手信息文件不存在");
}
以上异常在main中捕获

写入文件失败,输出文件无法打开的异常
try (BufferedWriter bufferedWriter = new BufferedWriter(new FileWriter(outputPath,true))){....}
使用此方式打开输出文件,若文件不存在将自动新建
此外,在 JDK 7 以上的版本,使用 try-with-resource 的方式可以实现资源的释放,上面的BufferedWriter 和 FileWriter都将在执行语句块后自行释放
代码不要想着一下子写得很好,面面俱到,先抓大体,能跑再说。初期阶段过于注重细节会导致开发效率低下,心态还容易崩
可以之后再重构~
优化是否可以再进一步?