软件工程实践第二次作业--文件读取

该作业是软件工程课程的一部分,涉及使用Java进行数据爬取、JSON解析、文件读写、性能优化和单元测试。学生需要从澳大利亚网球公开赛网站抓取数据,编写控制台程序进行统计和显示,并通过FastJson解析JSON文件。此外,还进行了性能改进,如使用HashMap缓存结果,减少重复计算,以及优化字符串拼接和数据结构。单元测试覆盖了命令检查、文件转换和结果获取等功能。
摘要由CSDN通过智能技术生成
这个作业属于哪个课程软件工程-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表格

PSPPersonal Software Process Stages预估耗时(分钟)实际耗时(分钟)
Planning计划6045
• Estimate• 估计这个任务需要多少时间2015
Development开发710880
• Analysis• 需求分析 (包括学习新技术)9075
• Design Spec• 生成设计文档3035
• Design Review• 设计复审2020
• Coding Standard• 代码规范 (为目前的开发制定合适的规范)3030
• Design• 具体设计6045
• Coding• 具体编码330360
• Code Review• 代码复审3015
• Test• 测试(自我测试,修改代码,提交修改)120300
Reporting报告6075
• Test Repor• 测试报告2025
• Size Measurement• 计算工作量1010
• Postmortem & Process Improvement Plan• 事后总结, 并提出过程改进计划3040
合计8301000

3. 解题思路描述

3.1.开发前的思考和准备
① 使用JAVA+IDEA完成编程,使用JUnit进行单元测试,使用JProfiler进行性能分析,使用FastJson库解析json文件。
② 使用ArrayList和HashMap作为主要数据结构,使用BufferedReader/BufferedWriter完成文件输入/输出工作。

3.2.数据爬取
注:本次数据的爬取行为仅用于课程教学
① 按下F12进入开发者模式,然后打开澳大利亚网球公开赛官网,点击Results或者Players进入需要爬取的数据页面。
在这里插入图片描述
② 在请求数据搜索栏里输入想要的数据,这里以比赛结果为例,在搜索框里输入results,然后双击内容为json方式存储的文件,即可获得当日的比赛结果的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提交代码到代码仓库,这样代码出现不可逆的问题时可以回滚,而且可以在不同设备上进行项目的更新和开发,还可以清楚地知道代码的更改历史。
  • 项目的单元测试对于一个项目的开发流程是不可或缺、至关重要的,通过全面、细致的单元测试可以提高代码的鲁棒性和正确性。但是创建全面细致的单元测试是不容易的,往往我们需要花费大量的时间去设计单元测试,
  • 项目的性能优化是十分重要的,良好的性能优化在面对大量的数据时会有更好的表现。同时性能优化可以从多个角度入手,比如文件的输入输出,数据结构,缓存机制等,因此对程序员的基本功和知识有着更高的要求。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值