688
社区成员
发帖
与我相关
我的任务
分享| 这个作业属于哪个课程 | 2023年福大-软件工程实践-W班 |
|---|---|
| 这个作业要求在哪里 | 作业详情 |
| 这个作业的目标 | 文件读写、json数据解析、爬取数据 |
| 其他参考文献 | CSDN |
项目地址:由此进

| PSP | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
|---|---|---|---|
| Planning | 计划 | 20 | 20 |
| • Estimate | • 估计这个任务需要多少时间 | 20 | 20 |
| Development | 开发 | 840 | 1015 |
| • Analysis | • 需求分析 (包括学习新技术) | 180 | 150 |
| • Design Spec | • 生成设计文档 | 60 | 55 |
| • Design Review | • 设计复审 | 10 | 10 |
| • Coding Standard | • 代码规范 (为目前的开发制定合适的规范) | 90 | 110 |
| • Design | • 具体设计 | 60 | 45 |
| • Coding | • 具体编码 | 360 | 480 |
| • Code Review | • 代码复审 | 20 | 15 |
| • Test | • 测试(自我测试,修改代码,提交修改) | 60 | 150 |
| Reporting | 报告 | 220 | 225 |
| • Test Repor | • 测试报告 | 180 | 200 |
| • Size Measurement | • 计算工作量 | 20 | 10 |
| • Postmortem & Process Improvement Plan | • 事后总结, 并提出过程改进计划 | 20 | 15 |
| 合计 | 1080 | 1260 |
| 问题 | 解决方案 | 具体过程 |
|---|---|---|
| 需要大量的文件读写操作,使用原生的java代码过于繁琐 | 采用第三方库commons-io | 从Maven仓库中下载commons-io-2.4.jar,导入项目依赖 |
| 需要解析json数据 | 采用第三方库fastjson | 从Maven仓库中下载fastjson-1.2.62.jar,导入项目依赖 |
| 需要在网页上爬取数据 | 参考自怎样使用Chrome浏览器获取网页页面的Json数据 | 在火狐浏览器中,利用开发者工具获取响应数据 |
| 需要进行单元测试 | 采用单元测试框架junit | 从Maven仓库中下载junit-4.10.jar,导入项目依赖 |
使用第三方库commons-io,读取input.txt内的指令,用换行符来切割多条指令。利用InputStream和第三方库commons-io,根据指令读取AOSearch.jar内的json文件,接着利用第三方库fastjson解析数据,得到想要的输出结果。最后使用commons-io库,把结果写入输出文件。
编码完成后,创建AOSearchTest.java,利用Junit框架来进行单元测试。
在火狐浏览器中,F12打开开发者工具,在“网络”中,过滤器里输入“schedule”进行筛选,查看“响应”的“原始数据”,即可获取到json字符串。将它复制粘贴到新建的文件中,利用VSCode对代码进行格式化,即可拿到比赛数据。

本次项目共有一个类AOSearch,一个测试类AOSearchTest,三个第三方库。AOSearch类中共有13个方法。
在main方法中调用readPlayers方法,在readPlayers方法中调用readPlayersInfo方法来形成所需的数据格式。其中,在readPlayersInfo方法中调用getGender方法来获取性别male/female。
在main方法中调用readMatch方法。在readMatch方法中调用readMatchInfo获取所需格式的比赛信息。在readMatchInfo中调用getWinner获取胜者名字,调用getScore获取比分。在getWinner方法中调用getNamesByteamId获取胜者名字。
在main方法中调用readBlank方法。
调用handleError方法,在handleError方法中调用findErrorType方法,来确定错误是“Error”还是“N/A”。

文件读写
本次作业需要进行文件的读写操作,不仅要读写本地的input.txt和output.txt文件,还要读取jar包内的文件。若要读本地的输入文件input.txt,可调用commons-io库的FileUtils类的readFileToString(File file, String encoding)方法;若要创建输出文件output.txt,则调用commons-io库的FileUtils类的touch(File file)方法;若要写入输出文件output.txt,可调用commons-io库的FileUtils类的writeStringToFile(File file, String str , String encoding)方法。
数据文件均被打包在AOSearch.jar包内,所以需要读取jar内的文件。利用this.getClass().getResourceAsStream方法可直接获取文件的输入流,进而获取文件内容。
json数据解析
解析json分为:解析json对象、解析json数组。当解析json对象时,使用fastjson库的getJSONObject(int index)方法;当解析json数组时,调用fastjson库的getJSONArray(String key)方法。
// 读取命令行的参数
String inputFileName = args[0]; // 输入文件名
String outputFileName = args[1]; // 输出文件名
String inputs = null;
File outputFile = null;
try {
// 读取input文件内的内容
inputs = FileUtils.readFileToString(new File(inputFileName), "utf8");
// 创建output.txt文件Java -jar AOSearch.jar input.txt output.txt
try {
outputFile = new File(outputFileName);
FileUtils.touch(outputFile);
} catch (IOException e) {
// 无法创建输出方法,结束
System.out.println("无法创建输出文件" + outputFileName);
return;
}
} catch (Exception e) {
// 找不到输入文件
System.out.println("您的输入文件" + inputFileName + "不存在");
return;
}
AOSearch search = new AOSearch();
// 将指令按行分割
String[] inputCode = inputs.split("\\r?\\n");
public String formatInput(String code) {
code = code.trim(); // 去掉首尾空格
if (code.startsWith("result ")) {
// 切分字符串,切出日期
String date = code.substring(7);
date = date.trim();
code = "result " + date;
}
return code;
}
InputStream is = this.getClass().getResourceAsStream("data/schedule/" + date + ".json");
String content = IOUtils.toString(is, "utf8");
// 读取players.json文件的内容
InputStream is = this.getClass().getResourceAsStream("data/players.json");
String content = IOUtils.toString(is, "utf8");
// 将json字符串转为JSON对象
JSONObject jsonObject = JSONObject.parseObject(content);
// 获取players数组
JSONArray players = jsonObject.getJSONArray("players");
// 整理获得输出数据
resultStr += readPlayersInfo(players);
public String readPlayersInfo(JSONArray players) {
// 遍历players数组,收集有用信息
String playerInfo = "";
JSONObject player = null;
for (int i = 0, size = players.size(); i < size; i++) {
player = players.getJSONObject(i);
playerInfo += "full_name:" + player.getString("full_name") + "\n";
playerInfo += "gender:" + getGender(player.getString("gender")) + "\n";
playerInfo += "nationality:" + player.getJSONObject("nationality").getString("name") + "\n" +
"-----\n";
}
return playerInfo;
}
// 读取相应比赛日期文件的内容
InputStream is = this.getClass().getResourceAsStream("data/schedule/" + date + ".json");
String content = IOUtils.toString(is, "utf8");
// 将json字符串转为JSON对象
JSONObject jsonObject = JSONObject.parseObject(content);
// 获取courts数组
JSONArray courts = jsonObject.getJSONObject("schedule").getJSONArray("courts");
// 获取结果,把结果写入文件
resultStr += readMatchInfo(courts, jsonObject);
FileUtils.writeStringToFile(file, resultStr, "utf8");
public String readMatchInfo(JSONArray courts, JSONObject jsonObject) {
String courtStr = "";
JSONObject court = null;
JSONArray sessions = null;
for (int i = 0, size = courts.size(); i < size; i++) {
court = courts.getJSONObject(i);
sessions = court.getJSONArray("sessions");
JSONObject session =null;
JSONArray activities = null;
for (int j = 0, sessionSize = sessions.size(); j < sessionSize; j++) {
session = sessions.getJSONObject(j);
activities = session.getJSONArray("activities");
JSONObject activity = null;
JSONArray teams = null;
JSONObject matchStatus = null;
String matchAbbr = null;
for (int k = 0, actSize = activities.size(); k < actSize; k++) {
activity = activities.getJSONObject(k);
if (activity.getString("actual_start_time") != null) {
courtStr += "time:" + activity.getString("actual_start_time") + "\n"; // 获取比赛时间
teams = activity.getJSONArray("teams");
courtStr += "winner:" + getWinner(teams, jsonObject) + "\n";
courtStr += "score:" + getScore(teams, jsonObject) + "\n";
courtStr += "-----\n";
} else {
matchStatus = activity.getJSONObject("match_status");
if (matchStatus != null) {
matchAbbr = matchStatus.getString("abbr");
if ("W/O".equals(matchAbbr)) {
// 弃赛
courtStr += "W/O\n-----\n";
}
}
}
}
}
}
return courtStr;
}
public String getWinner(JSONArray teams, JSONObject jsonObject) {
String names = "";
JSONObject team = null;
String isWin = null;
String teamId = null;
for (int i = 0, size = teams.size(); i < size; i++) {
team = teams.getJSONObject(i);
isWin = team.getString("status");
if ("Winner".equals(isWin)) {
// 获取胜者名字
teamId = team.getString("team_id");
names += getNamesByteamId(teamId, jsonObject);
return names;
}
}
return names;
}
public String getNamesByteamId(String teamId, JSONObject jsonObject) {
JSONArray teams = jsonObject.getJSONArray("teams");
ArrayList<String> playersCode = new ArrayList<>(); // 装着players的编号
ArrayList<String> playersName = new ArrayList<>(); // 装着players的名字
// 根据teamID查找队伍
JSONObject team = null;
for (int i = 0, size = teams.size(); i < size; i++) {
team = teams.getJSONObject(i);
if (team.getString("uuid").equals(teamId)) {
// 找到了队伍
JSONArray players = team.getJSONArray("players");
for (int j = 0, pSize = players.size(); j < pSize; j++) {
playersCode.add(players.getString(j));
}
break;
}
}
// 根据球员编号获取球员名字
JSONArray players = jsonObject.getJSONArray("players");
JSONObject player = null;
String uuid = null;
for (int i = 0, size = playersCode.size(); i < size; i++) {
for (int j = 0; j < players.size(); j++) {
player = players.getJSONObject(j);
uuid = player.getString("uuid");
if (uuid.equals(playersCode.get(i))) {
// 找到了
playersName.add(player.getString("short_name"));
break;
}
}
}
String names = "" + playersName.get(0);
for (int i = 1, size = playersName.size(); i < size; i++) {
names += " & " + playersName.get(i);
}
return names;
}
public String getScore(JSONArray teams, JSONObject jsonObject) {
String score = "";
JSONArray scoreA = teams.getJSONObject(0).getJSONArray("score"); // A队的分数数组
JSONArray scoreB = teams.getJSONObject(1).getJSONArray("score"); // B队的分数数组
score += scoreA.getJSONObject(0).getString("game") + ":"
+ scoreB.getJSONObject(0).getString("game");
for (int i = 1, size = scoreA.size(); i < size; i++) {
score += " | " + scoreA.getJSONObject(i).getString("game") + ":"
+ scoreB.getJSONObject(i).getString("game");
}
return score;
}
public String findErrorType(String input) {
if (input != null && (input.startsWith("result ") || "result".equals(input))) {
return "N/A\n-----\n";
} else return "Error\n-----\n";
}
FileUtils.writeStringToFile(file, resultStr, "utf8");
例如,优化前的代码:
public String readPlayersInfo(JSONArray players) {
// 遍历players数组,收集有用信息
String playerInfo = "";
for (int i = 0; i < players.size(); i++) {
JSONObject player = players.getJSONObject(i);
playerInfo += "full_name:" + player.getString("full_name") + "\n";
playerInfo += "gender:" + getGender(player.getString("gender")) + "\n";
playerInfo += "nationality:" + player.getJSONObject("nationality").getString("name") + "\n" +
"-----\n";
}
return playerInfo;
}
这种做法会导致内存中有players.size()份的JSONObject对象存在,并且players.size()很大,此时就会消耗大量内存了。
将代码优化为:
public String readPlayersInfo(JSONArray players) {
// 遍历players数组,收集有用信息
String playerInfo = "";
JSONObject player = null;
for (int i = 0; i < players.size(); i++) {
player = players.getJSONObject(i);
playerInfo += "full_name:" + player.getString("full_name") + "\n";
playerInfo += "gender:" + getGender(player.getString("gender")) + "\n";
playerInfo += "nationality:" + player.getJSONObject("nationality").getString("name") + "\n" +
"-----\n";
}
return playerInfo;
}
这样的话,内存中只有一份JSONObject对象,每次循环的时候,JSONObject对象就可指向不同的JSONObject对象,但是内存中只有一份。对于本次作业来说,多重循环是不可避免的,优化了以后,就能大大节省内存空间了。
对方法的调用,即使方法中只有一条语句,也是有时间和空间上的消耗。
例如,优化前的代码:
public String readPlayersInfo(JSONArray players) {
// 遍历players数组,收集有用信息
String playerInfo = "";
JSONObject player = null;
for (int i = 0; i < players.size(); i++) {
player = players.getJSONObject(i);
playerInfo += "full_name:" + player.getString("full_name") + "\n";
playerInfo += "gender:" + getGender(player.getString("gender")) + "\n";
playerInfo += "nationality:" + player.getJSONObject("nationality").getString("name") + "\n" +
"-----\n";
}
return playerInfo;
}
若是这样写代码,则players数组的长度有多大,size方法就会被调用多少次。
优化后的代码:
public String readPlayersInfo(JSONArray players) {
// 遍历players数组,收集有用信息
String playerInfo = "";
JSONObject player = null;
for (int i = 0, size = players.size(); i < size; i++) {
player = players.getJSONObject(i);
playerInfo += "full_name:" + player.getString("full_name") + "\n";
playerInfo += "gender:" + getGender(player.getString("gender")) + "\n";
playerInfo += "nationality:" + player.getJSONObject("nationality").getString("name") + "\n" +
"-----\n";
}
return playerInfo;
}
优化后,只会调用一次size方法,当数组长度很大时,就减少了大量消耗。
本次单元测试使用了junit-4.10框架。项目分为两大功能,一是输出所有球员的数据,二是输出每日或每赛季的比赛数据,此外还要处理一些错误的指令。
对球员的测试,设计了正确的测试用例"players",同时也设置了错误的指令"player"和"Players"。
对比赛数据的测试,分为每日和每赛季。对于每日比赛的单人赛,设计了正确的测试用例"result 0116"和"result 0117",错误的测试用例"reslut0116"、"result 0130"、"result sss"、"result 116"。对于双人比赛,设计了正确的测试用例"0129"。
@Test
public void testReadPlayer() {
try {
String content = FileUtils.readFileToString(new File("src/data/players.json"));
JSONObject jsonObject = JSONObject.parseObject(content);
// 获取players数组
JSONArray players = jsonObject.getJSONArray("players");
String playersStr = search.readPlayersInfo(players);
// 期望输出的前两个player的信息
String real =
"full_name:Radu Albot\n" +
"gender:male\n" +
"nationality:Moldova\n" +
"-----\n" +
"full_name:Marcelo Arevalo\n" +
"gender:male\n" +
"nationality:El Salvador\n" +
"-----\n";
// 实际解析得来的数据是否以期望值开头(即验证前两个player信息)
Assert.assertTrue(playersStr.startsWith(real));
} catch (IOException e) {
throw new RuntimeException(e);
}
}
测试了AOSearch类中获取球员信息readPlayersInfo方法,其中readPlayersInfo方法调用了getGender方法。调用完方法可获得全体球员的数据,由于数据过于庞大,因此只比较了前两个球员的数据。
// 测试0116的第一场比赛信息 "result 0116“
@Test
public void testReadMatch0116() {
try {
String content = FileUtils.readFileToString(new File("src/data/schedule/0116.json"));
JSONObject jsonObject = JSONObject.parseObject(content);
// 获取courts数组
JSONArray courts = jsonObject.getJSONObject("schedule").getJSONArray("courts");
// 获取输出结果
String match = search.readMatchInfo(courts, jsonObject);
// 期望的结果(仅取第一场比赛)
String real = "time:00:14\n" +
"winner:C. Gauff\n" +
"score:1:6 | 4:6\n" +
"-----\n" ;
Assert.assertTrue(match.startsWith(real));
} catch (IOException e) {
throw new RuntimeException(e);
}
}
测试了AOSearch类的获取比赛信息readMatchInfo方法。在readMatchInfo方法中,调用了getWinner方法来获取胜者名字,调用getScore方法获取比分。其中,在getWinner中调用了getNamesByteamId方法获取名字。
// 测试错误指令“player” 期望值"Error----\n"
@Test
public void testError1() {
String result = search.findErrorType("player");
String real =
"Error\n" +
"-----\n";
Assert.assertEquals(real, result);
}
测试了AOSearch类中处理错误指令的findErrorType方法,错误的类型分为"Error"和"N/A"。


本次作业是一个命令行的java程序,当在命令行窗口输入"
Java -jar AOSearch.jar input2.txt output.txt"时,若在AOSearch.jar包的同级文件目录下,不存在输入文件"input2.txt",则会进入异常处理,在命令行窗口中会输出"您的输入文件input2.txt不存在",如图所示:

通过这次作业,我接触了一个用于文件操作的第三方库commons-io,用于解析json的第三方库fastjson。本次作业锻炼了我的自学能力,过程中也遇上了不少困难,例如如何爬取数据,把程序打为jar包后无法读取到json文件等,最终通过询问同学、自行查找资料解决了问题。本次作业第一次接触到了单元测试框架junit,对单元测试有了初步的认识和了解。回顾一下感慨颇多,一开始看到作业只觉得比登天还难,但是多通读几遍要求,多找一些视频资料学习,最终还是度过难关,印证了“learning by doing”。