142
社区成员




这个作业属于哪个课程 | 2022年福大-软件工程;软件工程实践-W班 |
---|---|
这个作业要求在哪里 | 软件工程实践第二次作业——个人实战 |
这个作业的目标 | Gitcode,PSP表,接口设计,性能改进,单元测试,异常处理 |
其他参考文献 | csdn,bilibili等网站 |
gitCode项目地址
注:相关commit记录保存在README.md中
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
(注明:本次作业的爬虫行为仅用于教学,并无恶意)
一开始没有反应过来需要爬取信息,以为就只是读写文件,所以后面写完也回不了头了
2月15日之后的每日赛程信息就采用发送get请求获取json信息,经过处理后再保存到本地的方式来获取。
主要分为两步,寻找api和处理数据。
寻找api可以直接点击网站相关信息,按F12,查找到有关的地址,修改startdatecn的日期字段就可以获得其他日期的地址
这里贴出一个2月20日的每日赛程地址: 地址
总榜思路也和这个一样,此处省略
处理数据部分,因为获取下来的数据和原来的文件有几个问题:
1.只有一行难以阅读;2.中文字符显示成unicode;3,'/'变成'\/'
解决方法:可以用vscode等编译器把json格式化,就有分行了,再删去开头结尾的OM{.....},unicode很容易可以在网上找到转码网站(比如这个) ,然后ctrl+f全局替换'\/'为'/'再保存就行了
第一反应就是用最简单的输入输出流,后面想着用BufferedReader和BufferedWriter缓冲流来读写文件提高性能
一开始不让用第三方库,就用了正则表达式来解析(后面让用了就又写了一版Gson的,结果还慢了...)
代码分为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)
}
流程图
函数关系解释
遍历结束后,将汇总的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);
}
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();
}
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;
}
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;
}
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);
}
...
}
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");
}
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拼接字符串
减少IO次数:
合并循环,减少中间变量,有一些中间的循环是可以省略掉或者合并掉的
失败的思路:
对同一个测试用例
优化前:
2.25日新增优化:用hashmap存储已查询过的信息,减少io次数和创建的对象(类似缓存)
最终测试:使用2016000(约2百万)条包含各种情况的数据进行性能测试,output.txt文件大小约4.2G
@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);
}
本次作业的异常包括以下:
文件异常(IOExecption和文件不存在)
网络请求异常(不抛出直接在crawlOnlineData中处理)
其他异常(ParseException)【SimpleDateFormat的parse方法】
在main函数中进行捕获处理
设计流程图很详细清晰,赞!优化后的性能也挺不错。写代码还是不太习惯写注释?
单元测试和异常处理不太会写,想了解一下规范的写法
开始学吗?