软件工程实践第二次作业

221900330_詹鹏翔 学生 2022-02-25 02:28:51
这个作业属于哪个课程2022年福大-软件工程;软件工程实践-W班
这个作业要求在哪里软件工程实践第二次作业——个人实战
这个作业的目标Gitcode,PSP表,接口设计,性能改进,单元测试,异常处理
其他参考文献csdn,bilibili等网站

目录

  • 一:gitCode项目地址
  • 二:PSP表格
  • 三:解题思路描述
  • 1.数据爬取
  • 2.读写文件
  • 3.解析json
  • 4.其他
  • 四:设计与实现过程
  • 五:关键代码展示
  • 六:性能改进思路
  • 七:单元测试
  • 1.测试函数说明
  • 2.部分测试函数
  • 3.测试覆盖率
  • 4.如何优化覆盖率
  • 八:异常处理说明
  • 九:心得体会

一:gitCode项目地址

gitCode项目地址
注:相关commit记录保存在README.md


二:PSP表格

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

三:解题思路描述

本题要点包括,数据爬取读写文件解析json
(注明:本次作业的爬虫行为仅用于教学,并无恶意)

1.数据爬取

一开始没有反应过来需要爬取信息,以为就只是读写文件,所以后面写完也回不了头了
2月15日之后的每日赛程信息就采用发送get请求获取json信息,经过处理后再保存到本地的方式来获取。
主要分为两步,寻找api和处理数据。

寻找api可以直接点击网站相关信息,按F12,查找到有关的地址,修改startdatecn的日期字段就可以获得其他日期的地址
这里贴出一个2月20日的每日赛程地址: 地址
总榜思路也和这个一样,此处省略

请添加图片描述

处理数据部分,因为获取下来的数据和原来的文件有几个问题:
1.只有一行难以阅读;2.中文字符显示成unicode;3,'/'变成'\/'
解决方法:可以用vscode等编译器把json格式化,就有分行了,再删去开头结尾的OM{.....},unicode很容易可以在网上找到转码网站(比如这个) ,然后ctrl+f全局替换'\/'为'/'再保存就行了

2.读写文件

第一反应就是用最简单的输入输出流,后面想着用BufferedReader和BufferedWriter缓冲流来读写文件提高性能

3.解析json

一开始不让用第三方库,就用了正则表达式来解析(后面让用了就又写了一版Gson的,结果还慢了...)

4.其他

  • 因为一开始看作业目录就俩java类,嫌麻烦也没想着用什么json对象封装,就直接存储HashMap传参了
  • 中间存储字符串、json有关的信息基本都想着用ArrayList,HashMap(因为没有固定长度,想咋加就咋加,贼顺手)
  • 资料和很多灵感都来自csdn和别的大佬的博客(比如这位
  • java 很多方法基本都忘得差不多了,边查边用(面向百度编程)

四:设计与实现过程

代码分为Lib工具类和调用Lib实现功能的OlympicSearch类

Lib类

//工具类
public class Lib {
    //存储已访问过的指令对应的输出
    static HashMap<String,ArrayList<String>> usedData=new HashMap<>();

    //读取文件中的内容
    public static String readFile(String filePath) throws IOException
    
    //保存数据到文件中
    public static boolean writeFile(String filePath, ArrayList<String> records) throws IOException
    
    //读取json文件相对路径,获取指令(正确输出文件路径,错误输出N/A或Error)
    public static ArrayList<String> getJsonFilePath(String filePath) throws IOException
    
    //输出奖牌总榜(使用正则表达式解析数据)
    public static ArrayList<String> searchMedals(String jsonFilePath) throws IOException

    //输出每日赛程(使用正则表达式解析数据)
    public static ArrayList<String> searchMatch(String jsonFilePath) throws ParseException,IOException
    
    //输出无法识别的非法指令
    public static ArrayList<String> searchError()

    //输出日期越界的非法指令
    public static ArrayList<String> searchNA()

    //输出每日赛程的json数据(需手动处理)
    public static boolean crawlOnlineData(String startdatecn)
}

OlympicSearch类

public class OlympicSearch {
    //输入文件路径
    String inputFilePath;
    //输出文件路径
    String outputFilePath;
    //指令
    ArrayList<String> jsonFilePaths;
    
    //构造函数
    public OlympicSearch(String inputFilePath, String outputFilePath) throws IOException

    //查询输出统计启动方法
    public void search() throws IOException, ParseException

    //主函数
    public static void main(String[] args)
}

流程图

在这里插入图片描述

函数关系解释

  • main调用OlympicSearch构造函数构建实例,其中调用Lib.getJsonFilePath
    • Lib.getJsonFilePath再调用Lib.readFile返回存储input.txt文件内容的字符串列表
    • Lib.getJsonFilePath处理判断字符串列表生成指令列表并返回给OlympicSearch构造函数
    • OlympicSearch实例中通过以上步骤得以包含变量:输入文件路径、输出文件路径、指令列表,再将实例返回给main
  • main获取OlympicSearch实例后调用实例search方法启动统计功能
  • search方法遍历实例中的指令列表,根据不同的指令运行相应的Lib.searchNALib.searchMedalsLib.searchMatchLib.searchError方法,获取相应的输出语句
  • search方法最后汇总成一个最终结果字符串,再使用Lib.writeFile写入输出文件中

五:关键代码展示

  • search:开始进行数据统计并输出
    遍历构造函数时获取到的指令列表,根据指令列表进行判断:
    • 当指令是N/A,运行Lib.searchNA
    • 当指令是Error,运行Lib.searchError
    • 当指令是total文件路径,运行Lib.searchMedals
    • 当指令是每日赛程文件路径,运行Lib.searchMatch

遍历结束后,将汇总的ArrayList数据writeFile写入文件

    public void search() throws IOException, ParseException {
        ArrayList<String> result = new ArrayList<>();
        for (String jsonFilePath : jsonFilePaths) {
            if (jsonFilePath.contains("Error")) {
                System.out.print("无法识别的非法指令Error\n");
                result.addAll(Lib.searchError());
                
            } else if (jsonFilePath.contains("N/A")) {
                System.out.print("日期越界的非法指令N/A\n");
                result.addAll(Lib.searchNA());
                
            } else {
                if (jsonFilePath.contains("total")) {
                    System.out.print("输出奖牌总榜\n");
                    result.addAll(Lib.searchMedals(jsonFilePath));
                } else if (jsonFilePath.contains("schedule")) {
                    System.out.print("输出每日赛程\n");
                    result.addAll(Lib.searchMatch(jsonFilePath));
                }
            }
        }
        Lib.writeFile(outputFilePath, result);
    }
  • readFile:读取文件
    通过BufferedReader将文件读取,并使用StringBuilder拼接成字符串,每一行末尾添加换行符号以及最后去除多余的换行
      public static String readFile(String filePath) throws IOException {
          ...
          BufferedReader reader = new BufferedReader(
                  new InputStreamReader(new FileInputStream(file), StandardCharsets.UTF_8));
          String line;
          StringBuilder jsonStr = new StringBuilder();
          while ((line = reader.readLine()) != null) {
              //trim()去除前后空格
              jsonStr = jsonStr.append(line.trim() + "\n");
          }
          reader.close();
          ...
          return jsonStr.deleteCharAt(jsonStr.length() - 1).toString();
      }
    
  • getJsonFilePath:获取指令
    先用readFile读入文件内容生成字符串,再用split("\n")进行切片生成字符串列表
    遍历字符串列表,使用正则表达式匹配字符串,进行相应的判断,获取指令并汇总输出
    • 1."^total$"若字符串符合,指令为total文件路径
    • 2."^schedule\s+.*$""^schedule$"若字符串符合,则为可以识别的指令:
      • 2.1字符串符合"^(schedule)\s+(\d{4})$"且日期数字dateNum >= 202 && dateNum <= 220,则指令为对应的每日总榜文件路径
      • 2.2若不符合,则为N/A
    • 3.不符合1和2,则为 Error
    public static ArrayList<String> getJsonFilePath(String filePath) throws IOException {
        String content = readFile(filePath);
        String regex = "^(schedule)\\s+(\\d{4})$";
        ArrayList<String> paths = new ArrayList<>();
        String path;
        String[] orders = content.split("\n");
        for (String order : orders) {
            if (order.equals("")) {
                continue;
            }
            if (order.matches("^total$")) {
                path = "src/data/total.json";
            } else if (order.matches("^schedule\\s+.*$")) {
                if (order.matches(regex)) {
                    Matcher matcher = Pattern.compile(regex).matcher(order);
                    matcher.find();
                    int dateNum = Integer.parseInt(matcher.group(2));
                    if (dateNum >= 202 && dateNum <= 220) {
                        path = "src/data/" + matcher.group(1) + "/" + matcher.group(2) + ".json";
                    } else {
                        path = "N/A";
                    }
                } else {
                    path = "N/A";
                }
            } else {
                path = "Error";
            }
            if (order.equals("schedule")) {
                path = "N/A";
            }
            paths.add(path);
        }
        return paths;
    }
  • searchMedals:获取奖牌总榜
    先检查usedData中是否有查询过该信息,若有直接返回数据,没有则确定对应的正则表达式子,匹配,获取数据并存入HashMap中,之后再次遍历HashMap,用 StringBuilder拼接每一条数据的字符串,最后汇总存储于ArrayList中并返回
    public static ArrayList<String> searchMedals(String jsonFilePath) throws IOException{
        if (usedData.containsKey(jsonFilePath)) {
            return usedData.get(jsonFilePath);
        }
        String jsonStr = Lib.readFile(jsonFilePath);
        ArrayList<HashMap<String, String>> result = new ArrayList<>();
        String regex = "\\{\\s*\"bronze\": \"(.*?)\",\\s*\"rank\": \"(.*?)\",\\s*" +
                "\"count\": \"(.*?)\",\\s*\"silver\": \"(.*?)\",\\s*" +
                "\"countryname\": \"(.*?)\",\\s*\"gold\": \"(.*?)\",\\s*" +
                "\"countryid\": \"(.*?)\"\\s*}";
        Matcher matcher = Pattern.compile(regex).matcher(jsonStr);
        HashMap<String, String> country;
        while (matcher.find()) {
            country = new HashMap<>();
            ...//获取相应的数据存入country
            result.add(country);
        }
        
        ArrayList<String> records = new ArrayList<>();
        for (HashMap<String, String> single : result) {
            StringBuilder record = new StringBuilder();
            ...//拼接字符串
            records.add(record.toString());
        }
        if (!usedData.containsKey(jsonFilePath)) {
            usedData.put(jsonFilePath, records);
        }
        return records;
    }
  • searchMatch:获取每日赛程
    思路和获取奖牌总榜的类似,但因为数据的字段过多,故直接用HashMap存储需要的数据,注意name字段的获取
    public static ArrayList<String> searchMatch(String jsonStr) throws ParseException {
        ...
        Matcher matcher = Pattern.compile(regex).matcher(jsonStr);
        HashMap<String, String> match;
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        SimpleDateFormat sdf2 = new SimpleDateFormat("HH:mm");
        while (matcher.find()) {
            match = new HashMap<>();
            Date start = sdf.parse(matcher.group(18));
            match.put("time", sdf2.format(start));
            match.put("sport", matcher.group(39));
            match.put("name", matcher.group(10) + (matcher.group(27).equals("") ? "" : " " + matcher.group(27) + "VS" + matcher.group(29)));
            match.put("venue", matcher.group(19));
            result.add(match);
        }
        ...

    }
  • searchError:获取无法识别的非法指令输出
    直接输出固定的数据就可以了,searchNA方法也是类似
      public static ArrayList<String> searchError() {
          if (!usedData.containsKey("Error")) {
              ArrayList<String> records = new ArrayList<String>();
              records.add("Error\n" + "-----");
              usedData.put("Error", records);
          }
          return usedData.get("Error");
      }
    
  • crawlOnlineData:输出每日赛程的json数据(爬取)
    先通过matches("2022\d{4}"))判断输入的日期字符串变量是否符合“2022mmdd”格式
    若通过就用MessageFormat.format构建相应的Url,然后发送请求,若成功输出返回的json字段
      public static boolean crawlOnlineData(String startdatecn) {
          if (!startdatecn.matches("2022\\d{4}")) {
              System.out.println("输入日期格式错误,请检查:" + startdatecn);
              return false;
          }
          String json = null;
          String strUrl = MessageFormat.format(
                  "https://api.cntv.cn/Olympic/getBjOlyMatchList?startdatecn={0}&t=jsonp&cb=OM&serviceId=2022dongao"
                  , startdatecn);
          try {
              URL url = new URL(strUrl);
              HttpURLConnection connection = (HttpURLConnection) url.openConnection();
              ...//设置连接的超时时间等请求头字段
              connection.connect();
              if (connection.getResponseCode() == 200) {
                  BufferedReader reader = new BufferedReader(
                          new InputStreamReader(connection.getInputStream(), StandardCharsets.UTF_8));
                  json = reader.readLine();
                  System.out.println(json);
                  return true;
              } else {
                  System.out.println("GET请求失败,返回码:" + connection.getResponseCode());
                  return false;
              }
          } catch (Exception e) {
              System.out.println("GET请求出错:" + e);
              e.printStackTrace();
              return false;
          }
      }
    

六:性能改进思路

  • 使用StringBuilder拼接字符串

    • 因为程序中需要大量使用字符串拼接,直接用String会创建大量的对象降低效率
  • 减少IO次数:

    • 一开始searchError()等输出方法是直接将结果输出到文件上的,导致IO次数极多,后面我将应输出的字符串列表先return合并保存在一个ArrayList上,最后只需一次文件写入就可以了
    • search方法中,本来是无论指令是否合法,都会运行一次readFile方法,后面改成只有指令是正确路径的时候再去读取文件
  • 合并循环,减少中间变量,有一些中间的循环是可以省略掉或者合并掉的

  • 失败的思路:

    • 使用GSON解析JSON数据(代码好写了,但是性能并未有明显的提高,甚至还下降了...)
  • 对同一个测试用例
    优化前:

    在这里插入图片描述


    优化后:

    在这里插入图片描述

  • 2.25日新增优化:用hashmap存储已查询过的信息,减少io次数和创建的对象(类似缓存)

    在这里插入图片描述

  • 最终测试:使用2016000(约2百万)条包含各种情况的数据进行性能测试,output.txt文件大小约4.2G

    在这里插入图片描述


    在这里插入图片描述


七:单元测试

1.测试函数说明

  • 大部分都是用BufferedWriter把数据写入文件,再在测函数中使用Lib类的方法获取相应的数据或进行相应的数据处理,再使用Assert.assertEquals和期待的结果进行比较
  • 一些异常情况代码,如网络异常等无法进行测试
  • 测试用例,考虑了各种非法

2.部分测试函数

  • 测试写入文件
      @Test
      public void testReadFile() throws Exception {
          BufferedWriter writer = new BufferedWriter(new FileWriter("test_read.txt", false));
          String str = "123455678990asdcasdcasd*&……%&¥%……&长官杀杀杀随时随地的";
          writer.write(str);
          writer.close();
          String readStr = Lib.readFile("test_read.txt");
          Assert.assertEquals(str, readStr);
      }
    
  • 测试非法指令
      @Test
      public void testSearchError() {
          ArrayList<String> tests = Lib.searchError();
          StringBuilder testBuilder = new StringBuilder();
          for (String s : tests) {
              testBuilder = testBuilder.append(s + "\n");
          }
          String testStr = testBuilder.deleteCharAt(testBuilder.length() - 1).toString();
          Assert.assertEquals("Error\n-----", testStr);
      }
    
  • 测试输出总榜部分(部分)
    @Test
    public void testSearchMedals() throws Exception {
        String fileName = "src/data/total.json";
        String successFileName = "total_success.txt";
        String line;
        ArrayList<String> tests = Lib.searchMedals(fileName);
        BufferedReader successReader = new BufferedReader(
                new InputStreamReader(new FileInputStream(successFileName), StandardCharsets.UTF_8));
        StringBuilder successBuilder = new StringBuilder();
        while ((line = successReader.readLine()) != null) {
            successBuilder = successBuilder.append(line + "\n");
        }
        String successStr = successBuilder.deleteCharAt(successBuilder.length() - 1).toString();
        successReader.close();
        StringBuilder testBuilder = new StringBuilder();
        for (String s : tests) {
            testBuilder = testBuilder.append(s + "\n");
        }
        String testStr = testBuilder.deleteCharAt(testBuilder.length() - 1).toString();
        Assert.assertEquals(successStr, testStr);
    }

3.测试覆盖率

请添加图片描述


在这里插入图片描述

4.如何优化覆盖率

  • 简洁代码,不要留下太多空行,if尽量减少
  • 分清每一个函数的功能和可能的异常情况

八:异常处理说明

本次作业的异常包括以下:

  • 用户输入异常

    img

  • 文件异常(IOExecption和文件不存在)

    请添加图片描述

  • 网络请求异常(不抛出直接在crawlOnlineData中处理)

  • 其他异常(ParseException)【SimpleDateFormat的parse方法】
    在main函数中进行捕获处理


九:心得体会

  • git写完一块就commit,不要写完一堆再git,不然代码会很乱(而且完不成10次以上commit要求...)
  • 下次写代码要看清需求,拒绝想当然,把需求逐条列出
  • 还是敲得少了,很多方法都不熟悉,敲一趟下来跟面向百度编程一样
  • 单元测试和异常处理不太会写,想了解一下规范的写法
...全文
897 4 打赏 收藏 转发到动态 举报
写回复
用AI写文章
4 条回复
切换为时间正序
请发表友善的回复…
发表回复
Jingbin-Wang 2022-03-03
  • 打赏
  • 举报
回复

设计流程图很详细清晰,赞!优化后的性能也挺不错。写代码还是不太习惯写注释?

单元测试和异常处理不太会写,想了解一下规范的写法

开始学吗?

221900330_詹鹏翔 学生 2022-03-03
  • 举报
回复
@Jingbin-Wang 谢谢老师,注释是一开始看某个代码规范中不推荐用//,然后我就基本只写了javadoc,单元测试和异常处理之前基本就没咋写过,有点陌生
Jingbin-Wang 2022-03-04
  • 举报
回复
@221900330_詹鹏翔 必要的注释还是需要在代码中体现,阅读代码的时候就明白了,比起再看javadoc效率高些。
221900330_詹鹏翔 学生 2022-03-04
  • 举报
回复
@Jingbin-Wang 了解了,谢谢老师,已经重新填写了注释

139

社区成员

发帖
与我相关
我的任务
社区描述
2022年福大-软件工程;软件工程实践-W班
软件工程 高校
社区管理员
  • FZU_SE_teacherW
  • 丝雨_xrc
  • Lyu-
加入社区
  • 近7日
  • 近30日
  • 至今
社区公告
暂无公告

试试用AI创作助手写篇文章吧