软件工程实践第二次作业
| 这个作业属于哪个课程 | 2022年福大-软件工程;软件工程实践-W班 |
|---|---|
| 这个作业要求在哪里 | 软件工程实践第二次作业——个人实战 |
| 这个作业的目标 | Gitcode,PSP表,接口设计,性能改进,单元测试,异常处理 |
| 其他参考文献 | csdn,bilibili等网站 |
目录
- 一:gitCode项目地址
- 二:PSP表格
- 三:解题思路描述
- 1.数据爬取
- 2.读写文件
- 3.解析json
- 4.其他
- 四:设计与实现过程
- 五:关键代码展示
- 六:性能改进思路
- 七:单元测试
- 1.测试函数说明
- 2.部分测试函数
- 3.测试覆盖率
- 4.如何优化覆盖率
- 八:异常处理说明
- 九:心得体会
一:gitCode项目地址
gitCode项目地址
注:相关commit记录保存在README.md中
二:PSP表格
| PSP | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
|---|---|---|---|
| Planning | 计划 | 30 | 30 |
| • Estimate | • 估计这个任务需要多少时间 | 30 | 30 |
| Development | 开发 | 580 | 1015 |
| • Analysis | • 需求分析 (包括学习新技术) | 90 | 100 |
| • Design Spec | • 生成设计文档 | 30 | 35 |
| • Design Review | • 设计复审 | 30 | 10 |
| • Coding Standard | • 代码规范 (为目前的开发制定合适的规范) | 20 | 30 |
| • Design | • 具体设计 | 20 | 30 |
| • Coding | • 具体编码 | 180 | 180 |
| • Code Review | • 代码复审 | 30 | 30 |
| • Test | • 测试(自我测试,修改代码,提交修改) | 180 | 600 |
| Reporting | 报告 | 60 | 70 |
| • Test Report | • 测试报告 | 30 | 30 |
| • Size Measurement | • 计算工作量 | 10 | 10 |
| • Postmortem & Process Improvement Plan | • 事后总结, 并提出过程改进计划 | 20 | 30 |
| 合计 | 670 | 1115 |
三:解题思路描述
本题要点包括,数据爬取,读写文件,解析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尽量减少
- 分清每一个函数的功能和可能的异常情况
八:异常处理说明
本次作业的异常包括以下:
- 用户输入异常
文件异常(IOExecption和文件不存在)

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

其他异常(ParseException)【SimpleDateFormat的parse方法】
在main函数中进行捕获处理
九:心得体会
- git写完一块就commit,不要写完一堆再git,不然代码会很乱(而且完不成10次以上commit要求...)
- 下次写代码要看清需求,拒绝想当然,把需求逐条列出
- 还是敲得少了,很多方法都不熟悉,敲一趟下来跟面向百度编程一样
- 单元测试和异常处理不太会写,想了解一下规范的写法
相关推荐
PairProject:W班软件工程实践结对第二次作业仓库
结对第二次作业-顶会热词统计的实现 为了不遗漏作业内容,这里有一个清单检查表方便同学们快速检阅。 fork仓库,和伙伴商讨如何通过github协作/代码规范等 编程实现 功能一,操作爬取的论文列表 功能二,爬取结果分析展示 功能三,爬取论文信息(附加功能,实现有附加分) 其他附加功能 部署 发布发布版本,发起拉取请求 撰写博客,在预先之前提交一份博客 包含作业描述和目录 核对评分标准 上一次结对作业中,大家都和小伙伴完成了原型的设计,那么现在再联合小伙伴动手实现原型中的部分功能吧。采用网络技术来实现原型中的功能。 一,作业内容 1.基础功能 功能1:对已爬取的论文列表进行操作 可对论文列表进行删除; 可对论文列表进行查询详细信息(支持模糊查询,查询结果的展示,排序等功能可自行设计); 功能2:分析已爬取到的论文信息,提取top10个热门领域或热门研究方向 形成如关键字图谱之类直观的查看方式
软件工程第二次作业 计算机学院
软件工程第二次作业 计算机学院
2024年机器人软件工程学课程作业项目仓库_包含四次作业任务_第一次作业使用OpenCV实现图片显示功能_第二次作业开发简易聊天程序_第三次作业构建消息发布订阅系统_期末大作业完成.zip
2024年机器人软件工程学课程作业项目仓库_包含四次作业任务_第一次作业使用OpenCV实现图片显示功能_第二次作业开发简易聊天程序_第三次作业构建消息发布订阅系统_期末大作业完成.zip
第二次作业.zip
第二次作业.zip
2024年机器人软件工程学课程作业项目仓库-包含四次作业任务-第一次作业使用OpenCV实现图片显示功能-第二次作业开发简易聊天程序-第三次作业构建消息发布订阅系统-期末大作业完成.zip
搜索引擎2024年机器人软件工程学课程作业项目仓库_包含四次作业任务_第一次作业使用OpenCV实现图片显示功能_第二次作业开发简易聊天程序_第三次作业构建消息发布订阅系统_期末大作业完成.zip


