这个作业属于哪个课程 | 软件工程-23年春季学期 |
---|---|
这个作业要求在哪里 | 软件工程实践第二次作业—文件读取 |
这个作业的目标 | 1.完成对澳大利亚网球公开赛相关数据的爬取。 2.实现一个对赛事数据进行统计和显示的控制台程序。 3.撰写作业的博客说明 |
其他参考文献 | CSDN |
目录:
1. Gitcode项目地址
2. PSP表格
3. 解题思路描述
- 3.1 开发前的思考和准备
- 3.2 数据爬取
- 3.3 json文件的解析和保存
- 3.4 input.txt文件的命令读取
- 3.5 获得结果并输出到output.txt
- 3.6 性能优化
- 3.7 单元测试
- 3.8 项目打包
4. 接口设计和实现过程
5. 关键代码展示
6. 性能改进
- 6.1 字符串拼接
- 6.2 数据结构
- 6.3 重复命令的输出
- 6.4 输入输出
7. 单元测试
8. 异常处理
9. 心得体会
1. Gitcode项目地址
2. PSP表格
PSP | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | 60 | 45 |
• Estimate | • 估计这个任务需要多少时间 | 20 | 15 |
Development | 开发 | 710 | 880 |
• Analysis | • 需求分析 (包括学习新技术) | 90 | 75 |
• Design Spec | • 生成设计文档 | 30 | 35 |
• Design Review | • 设计复审 | 20 | 20 |
• Coding Standard | • 代码规范 (为目前的开发制定合适的规范) | 30 | 30 |
• Design | • 具体设计 | 60 | 45 |
• Coding | • 具体编码 | 330 | 360 |
• Code Review | • 代码复审 | 30 | 15 |
• Test | • 测试(自我测试,修改代码,提交修改) | 120 | 300 |
Reporting | 报告 | 60 | 75 |
• Test Repor | • 测试报告 | 20 | 25 |
• Size Measurement | • 计算工作量 | 10 | 10 |
• Postmortem & Process Improvement Plan | • 事后总结, 并提出过程改进计划 | 30 | 40 |
合计 | 830 | 1000 |
3. 解题思路描述
3.1.开发前的思考和准备
① 使用JAVA+IDEA完成编程,使用JUnit进行单元测试,使用JProfiler进行性能分析,使用FastJson库解析json文件。
② 使用ArrayList和HashMap作为主要数据结构,使用BufferedReader/BufferedWriter完成文件输入/输出工作。
3.2.数据爬取
注:本次数据的爬取行为仅用于课程教学
① 按下F12进入开发者模式,然后打开澳大利亚网球公开赛官网,点击Results或者Players进入需要爬取的数据页面。
② 在请求数据搜索栏里输入想要的数据,这里以比赛结果为例,在搜索框里输入results,然后双击内容为json方式存储的文件,即可获得当日的比赛结果的json文件
③ 将json数据复制到VS code里右键格式化文档,得到格式相对美观的json文件,并保存为UTF-8编码即可。
3.3. json文件解析和保存
① 利用BufferedReader将文件读入,并使用StringBuilder将每行拼接起来,得到json文件字符串。
② 学习FastJson库的使用方法,将json文件序列化为JAVABean对象,并使用ArrayList和HashMap等数据结构保存需要的信息。
③ 利用序列化的结果和数据结构中的保存的信息,将要输出的结果用StringBuilder拼接为一个字符串。
3.4. input.txt文件的命令读取
① 利用BufferedReader将input.txt文件一行一行读入,每一行为一条命令。
② 每读入一行命令对其进行判定,如果某一条命令不符合规范则程序抛出异常,如果符合规范则放入存放所有命令的ArrayList中,为后续的操作打下基础。
3.5. 获得结果并输出到output.txt
① 遍历存放所有命令的ArrayList,逐条执行命令。
② 将一条命令的执行结果转换为一个字符串,利用BufferedWriter输出到文件中。
3.6. 性能优化
① 通过思考和查阅博客,确定在字符串拼接、数据结构、重复命令的输出和文件输入输出这四个方面来进行性能优化。
② 重新从头到尾阅读自己的代码,根据四个方面点来优化自己的代码,并进行性能分析。
3.7. 单元测试
① 学习Junit的使用方法,在IDEA中引入Juint的插件。
② 设计并实现单元测试,利用测试结果再对程序进行修正。
3.8. 打包为jar包
① 打包项目中的模块代码。
① 打包项目中的工件
4. 接口设计和实现过程
4.1 主要类和函数
- MyUtil类
/**
* 根据输入文件获得需要执行的命令列表。
* @param inputFilename 输入文件
* @return 用ArrayList保存的命令列表
*/
public static ArrayList<String> getCommandList(String inputFilename) throws IOException
/**
* 执行命令列表中的命令,并把执行结果输出到指定的输出文件
* @param commandList 传入的命令列表
* @param outputFilename 输出文件名
*/
public static void executeCommandList(ArrayList<String> commandList, String outputFilename) throws IOException
/**
* 将存有json数据的文件转换为json字符串
* @param filePath 文件路径
* @return json字符串
* @throws IOException
*/
public static String fileToJson(String filePath) throws IOException
/**
* 判断命令的类型
* @param command 命令
* @return
* 1表示获取玩家信息的命令
* 2表示获取某天比赛结果的命令
* 3表示获取某季比赛结果的命令
* 4表示result命令的日期不符合要求N/A
* 5表示无法识别的指令Error
* 6表示空行跳过
*/
public static int checkCommand(String command)
- AOPlayers
/**
* @param filePath 玩家信息的json文件路径
* @param flag 用于判断是否需要使用HashMap建立玩家UUID和玩家对象映射关系和队伍UUID和队员列表的映射关系,0不需要,1需要
* @return 存储玩家信息的字符串
* @throws IOException
*/
public static String getPlayersResult(String filePath, int flag) throws IOException
- AOMatches
/**
* 根据文件获得并保存比赛结果
* @param filePath 比赛结果的json文件路径
* @return 存储比赛结果的字符串
* @throws IOException
*/
static public String getMatchesResult(String filePath) throws IOException
- Player类
String uuid; //选手的UUID
String full_name; //选手的全名
String short_name; //选手的简写名
String gender; //选手的性别
String nationality; //选手的国籍
public Player(String uuid, String full_name, String short_name, String gender, String nationality)
{
this.uuid = uuid;
this.full_name = full_name;
this.short_name = short_name;
this.gender = gender.equals("M") ? "male" : "female";
this.nationality = nationality;
}
- Team类
String uuid; //队伍的UUID
ArrayList<String> players; //队伍的选手UUID列表
public Team(String uuid, ArrayList<String> players)
{
this.uuid = uuid;
this.players = players;
}
4.2 代码组织关系
主函数调用MyUtil.getCommandList()获得命令列表,再调用MyUtil.executeCommandList()执行命令列表。
MyUtil.executeCommandList()中遍历命令列表,对于每条命令,调用MyUtil.checkCommand()判断命令类型:
- 如果是获取玩家信息命令,调用AOPlayers.getPlayersResult()获得玩家信息并输出到文件。
- 如果是获取比赛结果命令,先调用AOPlayers.getPlayersResult()获得玩家信息并建立对象之间的映射关系,再调用AOPlayers.getMatchesResult()根据映射关系获得比赛结果并输出到文件。
- 如果是命令的日期不符合要求,则输出N/A到文件。
- 如果是无法识别的命令,则输出Error到文件。
- 如果是空行,则不处理跳过。
在AOPlayers.getPlayersResult()函数中,调用MyUtil.fileToJson()获取json字符串,解析json字符串为JAVABean对象,同时建立对象之间的映射关系,得到玩家信息输出到文件。
在AOMatches.getMatchesResult()函数中,调用MyUtil.fileToJson()获取json字符串,根据对象之间建立的映射关系获得比赛信息,将输出到文件
4.3. 算法的关键
- 首先是要快速方便地解析json文件获取需要的信息,可以使用FastJson来解析json字符串。
- 其次是要快速地找到队伍的所有队员和根据胜利队伍的UUID找到队伍,可以使用HashMap建立对象之间的映射关系来快速找到。
- 最后是判定命令类型的函数,可以利用正则表达式和if/else语句相结合来完成命令类型的判定。
4.4独到之处
- 采用FastJson解析json字符串,可以方便快速地解析json字符串获得所需要的信息。
- 建立一个HashMap来缓存已经得到的结果,下次遇到相同的命令可以直接得到结果,而不必重复运行相应的模块。
5. 关键代码展示
- 获得比赛结果
/**
* 根据文件获得并保存比赛结果
*
* @param filePath 比赛结果的json文件路径
* @return 存储比赛结果的字符串
* @throws IOException
*/
static public String getMatchesResult(String filePath) throws IOException
{
stringBuilder.setLength(0);
Map matchMap = JSON.parseObject(MyUtil.fileToJson(filePath), Map.class);
List<HashMap> matchMapList = (List<HashMap>) matchMap.get("matches");
for (HashMap map : matchMapList)
{
String status = (String) ((HashMap) map.get("match_status")).get("name");//获得比赛状态
if (status.equals("Walk-Over"))
{
stringBuilder.append("W/O\n-----\n");
continue;
}
List<HashMap> teamList = (List<HashMap>) map.get("teams");
HashMap teamA = teamList.get(0);
HashMap teamB = teamList.get(1);
String uuidA = (String) teamA.get("team_id");
String uuidB = (String) teamB.get("team_id");
String winnerTeamId = teamList.get(0).get("status") != null ? uuidA : uuidB;//获取胜利队伍UUID
String time = (String) map.get("actual_start_time");
stringBuilder.append("time:" + time + "\n");//获得比赛时间
ArrayList<String> players = AOPlayers.teamHashMap.get(winnerTeamId).getPlayers();
stringBuilder.append("winner:");//获得胜利队伍的成员
for (int j = 0; j < players.size(); j++)
{
if (j > 0) stringBuilder.append(" & ");
stringBuilder.append(AOPlayers.playerHashMap.get(players.get(j)).getShort_name());
}
List<HashMap> scoresA = (List<HashMap>) teamA.get("score");
List<HashMap> scoresB = (List<HashMap>) teamB.get("score");
stringBuilder.append("\nscore:");//获得比赛得分
for (int j = 0; j < scoresA.size(); j++)
{
HashMap gameA = scoresA.get(j);
HashMap gameB = scoresB.get(j);
if (j > 0) stringBuilder.append(" | ");
stringBuilder.append(gameA.get("game") + ":" + gameB.get("game"));
}
stringBuilder.append("\n-----\n");
}
return stringBuilder.toString();
}
- 获取玩家信息
/**
* @param filePath 选手信息的json文件路径
* @param flag 用于判断是否需要使用HashMap建立选手UUID和选手对象映射关系和队伍UUID和队员列表的映射关系,0不需要,1需要
* @return 存储选手信息的字符串
* @throws IOException
*/
public static String getPlayersResult(String filePath, int flag) throws IOException
{
playerArrayList.clear();
stringBuilder.setLength(0);
String json = MyUtil.fileToJson(filePath);
Map playerMap = JSON.parseObject(json, Map.class);
List<HashMap> playerMapList = (List<HashMap>) playerMap.get("players");
for(HashMap map: playerMapList)
{
Player player = new Player((String) map.get("uuid"), (String) map.get("full_name"), (String) map.get("short_name"),
(String) map.get("gender"), (String) ((HashMap)map.get("nationality")).get("name"));
if(flag == 1)playerHashMap.put(player.getUuid(), player);//如果指令为result,建立选手UUID和选手对象的映射关系
playerArrayList.add(player);
}
if(flag == 1)//如果指令为result,则建立队伍和选手的映射关系
{
Map teamMap = JSON.parseObject(json, Map.class);
List<HashMap> teamMapList = (List<HashMap>) teamMap.get("teams");
for(HashMap map : teamMapList)
{
String uuid = (String) map.get("uuid");
ArrayList<String> teamPlayerList = (ArrayList<String>)(map.get("players"));
teamHashMap.put(uuid, new Team(uuid, teamPlayerList));
}
}
if(flag == 0)//如果指令为players,则输出所有的选手信息
{
for (Player player : playerArrayList)
{
stringBuilder.append("full_name:");
stringBuilder.append(player.getFull_name());
stringBuilder.append("\ngender:");
stringBuilder.append(player.getGender());
stringBuilder.append("\nnationality:");
stringBuilder.append(player.getNationality());
stringBuilder.append("\n-----\n");
}
}
return stringBuilder.toString();
}
6. 性能改进
首先考虑一下哪些地方可以进行优化,哪些操作比较耗时。主要从字符串拼接、数据结构、重复命令的输出和输入输出等方面进行优化。
6.1.字符串拼接
因为需要调用循环,用+号来进行字符串拼接,JAVA会对其进行使用StringBuilder进行优化,但在循环中进行字符串拼接,那么会重复创建StringBuilder对象增加耗时,因此可以将StringBuilder的新建放在循环外。比如每执行一条命令,只会产生一个StringBuilder对象。
6.2.数据结构
在输出比赛结果时,需要根据获胜队伍的UUID来查询队伍,再根据队伍查询队员的名单,但是如果都用链表存储,那么每次查询的复杂度是O(n),因此可以考虑使用HashMap来建立队伍的UUID和队伍,队伍和队员之间映射信息,HashMap的理想复杂度在O(1)。
6.3.重复命令的输出
input.txt中可能存在大量的重复命令,因此并不需要每次命令都通过运行相应的模块得到。可以将运行过的结果保存在一个HashMap缓存,下次执行命令时可以先查询HashMap中是否存有已经运行过的结果,如果有则直接输出,如果没有就运行相应模块得到结果并把结果保存在HashMap中缓存。经过性能分析,此条优化方法可以大幅缩短运行时间,当命令越多优化效果越明显。
* 未使用HashMap缓存
- 使用HashMap缓存
6.4.文件输入输出
因为BufferedReader/BufferedWriter在IO流是速度比较快的,所以可以采用BufferedReader/BufferedWriter进行文件的输入输出,提高输入输出的速度。
7. 单元测试
- checkCommand()函数的测试,用于测试检查命令正确性的功能
@Test
public void testCheckCommand()
{
assertEquals(1, MyUtil.checkCommand("players"));
assertEquals(5, MyUtil.checkCommand("player"));
assertEquals(5, MyUtil.checkCommand("Players"));
assertEquals(2, MyUtil.checkCommand("result 0116"));
assertEquals(5, MyUtil.checkCommand("result0116"));
assertEquals(4, MyUtil.checkCommand("result 01 16"));
assertEquals(2, MyUtil.checkCommand("result 0129"));
assertEquals(5, MyUtil.checkCommand("res2121ult 0129"));
assertEquals(4, MyUtil.checkCommand("result 0122229"));
assertEquals(5, MyUtil.checkCommand("rseult Q1"));
assertEquals(5, MyUtil.checkCommand("resul Q1"));
assertEquals(3, MyUtil.checkCommand("result Q1"));
assertEquals(3, MyUtil.checkCommand("result Q4"));
assertEquals(4, MyUtil.checkCommand("result q4"));
assertEquals(4, MyUtil.checkCommand("result Q71"));
assertEquals(4, MyUtil.checkCommand("result Q5"));
assertEquals(4, MyUtil.checkCommand("result Q 1"));
assertEquals(4, MyUtil.checkCommand("result "));
assertEquals(4, MyUtil.checkCommand("result"));
}
- testFileToJson()函数的测试,用于测试将文件内容转换为json字符串的功能
@Test
public void testFileToJson()
{
Assertions.assertThrows(IOException.class, () -> {
MyUtil.fileToJson("./src/data/schedul31e/02226.json");
});
Assertions.assertThrows(IOException.class, () -> {
MyUtil.fileToJson("/src/data/schedule/0116.json");
});
Assertions.assertThrows(IOException.class, () -> {
MyUtil.fileToJson("./src/data/player.json");
});
Assertions.assertThrows(IOException.class, () -> {
MyUtil.fileToJson("./src/data/0116.json");
});
Assertions.assertThrows(IOException.class, () -> {
MyUtil.fileToJson("./src/data/schedule/0311.json");
});
Assertions.assertThrows(IOException.class, () -> {
MyUtil.fileToJson("./src/data/q1.json");
});
Assertions.assertThrows(IOException.class, () -> {
MyUtil.fileToJson("./src/data/schedule/q5.json");
});
Assertions.assertThrows(IOException.class, () -> {
MyUtil.fileToJson("./src/data/0116.json");
});
}
- GetMatchesResult()函数和GetPlayersResult()函数的测试,用于测试比赛结果和玩家信息获取的功能
@Test
public void testGetResult() throws IOException
{
String matchesResult = AOMatchs.getMatchesResult("./src/data/schedule/0124.json");
String playersResult = AOPlayers.getPlayersResult("./src/data/players.json", 0);
assertNotEquals(0, matchesResult.length());
assertNotEquals(0, playersResult.length());
}
- executeCommandList()函数的测试,用于测试输入命令正确执行和结果正常输出的功能
@Test
public void TestExecuteCommandList() throws IOException
{
String outputFilename = "outputTest.txt";
ArrayList<String> commandList = new ArrayList<>();
commandList.add("players");
commandList.add("result 0124");
MyUtil.executeCommandList(commandList, outputFilename);
int playerSize = AOPlayers.playerArrayList.size();
assertNotEquals(0, playerSize);
}
- getCommandList()函数的测试,用于测试从输入文件正确获得命令的功能
public void TestGetCommandList() throws IOException
{
String inputFilename = "inputTest.txt";
int size = MyUtil.getCommandList(inputFilename).size();
assertNotEquals(0, size);
}
- 用于output.txt结果的正确性验证
@Test
public void testResultCorrect() throws IOException
{
try
{
BufferedReader bufferedReader1 = new BufferedReader(new InputStreamReader(new FileInputStream("output.txt"), "UTF-8"));
BufferedReader bufferedReader2 = new BufferedReader(new InputStreamReader(new FileInputStream("result.txt"), "UTF-8"));
String line1, line2;
int cnt = 1;
while ((line1 = bufferedReader1.readLine()) != null && (line2 = bufferedReader2.readLine()) != null)
{
if (!line1.equals(line2))
{
System.out.println("第" + cnt + "行结果不同!");
}
assertEquals(line1, line2);
}
} catch (Exception e)
{
e.printStackTrace();
}
}
- 测试结果
8. 异常处理
- 文件读取异常
//判断文件是否存在
File file = new File(inputFilename);
if(!file.exists())
{
throw new FileNotFoundException("文件(" + file.getPath() + ")不存在,请检查输入的文件路径!");
}
- 参数异常
if(args.length != 2)
{
System.out.println("输入的参数不符合规范,请检查输入的参数!");
System.exit(-1);
}
- 异常处理
所有的异常都在主函数进行处理
try
{
ArrayList<String> commandList;
commandList = MyUtil.getCommandList(inputFilename); //获得需要执行的命令列表
MyUtil.executeCommandList(commandList, outputFilename); //执行命令列表中的列表
}
catch (Exception e)
{
e.printStackTrace();
}
9. 心得体会
- 多使用Git提交代码到代码仓库,这样代码出现不可逆的问题时可以回滚,而且可以在不同设备上进行项目的更新和开发,还可以清楚地知道代码的更改历史。
- 项目的单元测试对于一个项目的开发流程是不可或缺、至关重要的,通过全面、细致的单元测试可以提高代码的鲁棒性和正确性。但是创建全面细致的单元测试是不容易的,往往我们需要花费大量的时间去设计单元测试,
- 项目的性能优化是十分重要的,良好的性能优化在面对大量的数据时会有更好的表现。同时性能优化可以从多个角度入手,比如文件的输入输出,数据结构,缓存机制等,因此对程序员的基本功和知识有着更高的要求。