这个作业属于哪个课程 | 福州大学 2024 秋软件工程实践 |
---|---|
这个作业要求在哪里 | 软件工程实践第二次作业——个人实战 |
这个作业的目标 | 爬取数据、编写代码、性能分析、单元测试 |
其他参考文献 | 如何使用 Python 提取 JSON 中的数据、 Gson的基本使用、IDEA中添加junit4的三种方法(详细步骤操作) |
文章目录
1.Gitcode项目地址
2.PSP表格
PSP | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | 20 | 40 |
• Estimate | • 估计这个任务需要多少时间 | 20 | 40 |
Development | 开发 | 960 | 1255 |
• Analysis | • 需求分析 (包括学习新技术) | 120 | 480 |
• Design Spec | • 生成设计文档 | 20 | 20 |
• Design Review | • 设计复审 | 20 | 60 |
• Coding Standard | • 代码规范 (为目前的开发制定合适的规范) | 10 | 5 |
• Design | • 具体设计 | 30 | 90 |
• Coding | • 具体编码 | 700 | 420 |
• Code Review | • 代码复审 | 30 | 90 |
• Test | • 测试(自我测试,修改代码,提交修改) | 30 | 90 |
Reporting | 报告 | 60 | 70 |
• Test Repor | • 测试报告 | 20 | 35 |
• Size Measurement | • 计算工作量 | 20 | 15 |
• Postmortem & Process Improvement Plan | • 事后总结, 并提出过程改进计划 | 20 | 20 |
合计 | 1040 | 1365 |
3.解题思路描述
3.1数据爬取
拿到项目第一步,数据在哪?我根据提供的网址,打开开发者工具看源码后,找到medals文件里面的script,包含所有需要的数据,然后爬下来。
然后就得到了包含所有数据(也有冗余数据)的olympics_medals_data.json文件。
(话说我开学刚卸的PyCharm和IDLE,又火急火燎下回来)
3.2 需求分析
根据作业要求里面的三个功能,首先要确定需要的数据结构,便于后面项目进行,比如对json数据的筛选,使用Gson进行数据转换,都有帮助。
所需功能:
1.实现奖牌排行榜上的所有国家(按奖牌排序)的奖牌数输出。
2.实现输出指定日期当天的运动员(按奖牌排序)的奖牌数输出。
3.实现两个国家指定比赛项目奖牌数的比较。
由于功能1和3可以放在一起实现,更方便处理以及节省空间。
所以我决定使用两种结构,一种放某一国家的奖牌数和获得奖牌的项目的信息,一种放某一日期,所有获奖人员的姓名和奖牌数。
3.3 分析JSON结构并筛选有效数据
一开始我想直接使用olympics_medals_data.json文件的,但发现无效数据太多,而且会影响最后的效率。所以决定先对其进行有效数据的筛选。
1.首先把JSON大致看一遍,发现url放在"urls",国家对应的名称代码放在"nocList"等,发现数据在"initialMedals"里,但其实再看看就发现里面只需要"medalStandings",还有一个没用的"competitionCode": ““也不知道写那干嘛,然后"medalStandings"也只需要"medalsTable”,还有一个"eventInfo"计算总共的项目->没啥用。所以最后只需要"medalsTable”。所以直接把"medalsTable"里面的数据都放到另外一个文件filtered_medals_table.json里。
(项目写完了,我都不知道写个空的"competitionCode":有啥用)
2.随后用python提取功能需要的数据分别放在两个json文件里。
import json# 加载JSON数据
with open('D:/desktop/filtered_medals_table.json', 'r', encoding='utf-8') as file:
data = json.load(file) # data 是一个列表,而不是字典
countries = []
date_events = {}
for country_data in data: # 不再使用 'medalsTable' 作为键,因为 data 是一个列表
country_code = country_data['description']
country_name = country_data['longDescription']
rank = country_data['rank']
# 获取国家总奖牌数
total_medals = None
for medals_info in country_data['medalsNumber']:
if medals_info['type'] == 'Total':
total_medals = medals_info
break
gold = total_medals['gold']
silver = total_medals['silver']
bronze = total_medals['bronze']
total = total_medals['total']
# 创建 Country 对象结构
country = {
'countryCode': country_code,
'countryName': country_name,
'gold': gold,
'silver': silver,
'bronze': bronze,
'total': total,
'rank': rank,
'disciplines': []
}
# 遍历国家的项目 (disciplines)
for discipline_data in country_data['disciplines']:
discipline_name = discipline_data['name']
discipline_gold = discipline_data['gold']
discipline_silver = discipline_data['silver']
discipline_bronze = discipline_data['bronze']
discipline_total = discipline_data['total']
# 创建 Discipline 对象
discipline = {
'name': discipline_name,
'gold': discipline_gold,
'silver': discipline_silver,
'bronze': discipline_bronze,
'total': discipline_total
}
# 将 Discipline 添加到 Country
country['disciplines'].append(discipline)
# 遍历项目的获奖者并将信息按日期存储
for winner_data in discipline_data['medalWinners']:
event_description = winner_data['eventDescription']
winner_name = winner_data['competitorDisplayName']
date = winner_data['date']
# 创建 Event 对象
event = {
'eventName': event_description,
'winnerName': winner_name,
'nationality': country_code,
'gold': discipline_gold,
'silver': discipline_silver,
'bronze': discipline_bronze,
'total': discipline_total
}
# 按日期存储事件
if date not in date_events:
date_events[date] = {
'date': date,
'events': []
}
date_events[date]['events'].append(event)
# 将 Country 对象添加到 countries 列表
countries.append(country)
with open('D:/desktop/countries.json', 'w', encoding='utf-8') as outfile:
json.dump(countries, outfile, ensure_ascii=False, indent=4)
with open('D:/desktop/dates.json', 'w', encoding='utf-8') as outfile:
json.dump(list(date_events.values()), outfile, ensure_ascii=False, indent=4)
print("数据已成功处理并保存为 countries.json 和 dates.json")
import json
# 读取原始 JSON 文件
with open('D:/desktop/filtered_medals_table.json', 'r', encoding='utf-8') as file:
data = json.load(file)
date_events = {}
# 遍历每个国家的数据
for country_data in data:
country_code = country_data['description']
# 遍历国家的项目 (disciplines)
for discipline_data in country_data['disciplines']:
# 遍历项目的获奖者
for winner_data in discipline_data['medalWinners']:
competitor_type = winner_data['competitorType'] # 判断是否为团体赛
if competitor_type == "T":
continue # 跳过团体赛
winner_name = winner_data['competitorDisplayName']
date = winner_data['date']
medal_type = winner_data['medalType']
# 初始化当天的日期结构
if date not in date_events:
date_events[date] = {
'date': date,
'competitors': {}
}
# 如果该选手已经在日期里,累加奖牌数
if winner_name not in date_events[date]['competitors']:
date_events[date]['competitors'][winner_name] = {
'name': winner_name,
'nationality': country_code,
'medalNumber': {
'gold': 0,
'silver': 0,
'bronze': 0,
'total': 0
}
}
# 根据奖牌类型累加奖牌
if medal_type == "ME_GOLD":
date_events[date]['competitors'][winner_name]['medalNumber']['gold'] += 1
elif medal_type == "ME_SILVER":
date_events[date]['competitors'][winner_name]['medalNumber']['silver'] += 1
elif medal_type == "ME_BRONZE":
date_events[date]['competitors'][winner_name]['medalNumber']['bronze'] += 1
# 更新奖牌总数
date_events[date]['competitors'][winner_name]['medalNumber']['total'] += 1
# 对每个日期下的选手按金牌、银牌、铜牌排序,并为他们添加排名
for date, event_data in date_events.items():
competitors_list = list(event_data['competitors'].values())
competitors_list.sort(key=lambda x: (-x['medalNumber']['gold'], -x['medalNumber']['silver'], -x['medalNumber']['bronze']))
# 添加排名
for rank, competitor in enumerate(competitors_list, start=1):
competitor['rank'] = rank
# 更新 competitors 数据
date_events[date]['competitors'] = competitors_list
# 将处理好的 DateEvents 数据保存为 JSON 文件
with open('D:/desktop/dates.json', 'w', encoding='utf-8') as outfile:
json.dump(list(date_events.values()), outfile, ensure_ascii=False, indent=4)
print("数据已成功处理并保存为 dates.json")
不要问为什么dates.json处理了两次,因为需求看错了(白花两小时我真的哭死)
3.最后得到两个处理完的json文件–countries.json和dates.json。结构如下
3.4项目技术
其实原本想尝试c++的,但是由于没什么c++的项目经验就用Java了。
项目用maven引用Gson工具来处理json数据转换。
只需要在项目里的pop.xml里面加入依赖就好了。
<dependencies>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.8.9</version> <!-- 版本可以根据需要调整 -->
</dependency>
</dependencies>
4.接口设计和实现过程
4.1代码中类的组织和关系
1.首先两个类使用Gson对两个json文件进行数据转换。
2.使用三个类(其实还有一个内置类),对应所需要的数据结构。
3.由于作业要求接口封装,我就把两个基本功能放在两个类中了 。
4.加上一个主程序OlympicSearch类
4.2 代码中的函数组织
有了数据和类的结构,函数就非常好处理了,在封装的地方安安分分写函数就好了。
比如在DailyResults类里面writeDailyResults传入List<DateWinners.Competitor>比较好而不是直接传List< DateWinners >.
5.关键代码展示
5.1 对json进行数据转化成List
Gson gson = new Gson();
FileReader reader = new FileReader("D:/desktop/countries.json");
// 反序列化到 Country 对象列表
Type countryListType = new TypeToken<List<Country>>() {}.getType();
List<Country> countries = gson.fromJson(reader, countryListType);
// 输出所有国家的奖牌信息
for (Country country : countries) {
System.out.println(country);
}
Gson gson = new Gson();
FileReader reader = new FileReader("D:/desktop/dates.json");
// 反序列化到 DateWinners 对象列表
Type datewinnersListType = new TypeToken<List<DateWinners>>() {}.getType();
List<DateWinners> datewinnerss = gson.fromJson(reader, datewinnersListType);
// 输出所有国家的奖牌信息
for (DateWinners datewinners : datewinnerss) {
System.out.println(datewinners);
}
5.2主函数中得到数据组成的列表
public class OlympicSearch {
static String countriesfile="D:/desktop/countries.json";
static String datesfile="D:/desktop/dates.json";
private static List<Country> loadCountries() throws Exception {
Gson gson = new Gson();
FileReader reader = new FileReader(countriesfile);
Type countryListType = new TypeToken<List<Country>>() {}.getType();
return gson.fromJson(reader, countryListType);
}
private static List<DateWinners> loadDateWinners()throws Exception{
Gson gson = new Gson();
FileReader reader = new FileReader(datesfile);
Type dateListType = new TypeToken<List<DateWinners>>() {}.getType();
return gson.fromJson(reader, dateListType);
}
public static List<Country> sortCountriesByMedals(List<Country> countries) {
countries.sort((c1, c2) -> {
if (c1.getGold() != c2.getGold()) return Integer.compare(c2.getGold(), c1.getGold());
if (c1.getSilver() != c2.getSilver()) return Integer.compare(c2.getSilver(), c1.getSilver());
return Integer.compare(c2.getBronze(), c1.getBronze());
});
return countries;
}
5.3 实现命令行程序
实际上就是对主程序的args进行处理
if (args.length != 2) {
System.err.println("Usage: java -jar OlympicSearch.jar <inputFile> <outputFile>");
System.exit(1);
}
String inputFile = args[0];
String outputFile = args[1];
除了这三个,其他的函数就是正常的逻辑关系处理了。
6.性能改进
6.1去除冗余数据
其实最大的改进就是把json文件里面的冗余数据去除,详细的请看 3.3 分析JSON结构并筛选有效数据。
6.2 数据结构的处理
前面说过我重新处理了一遍dates.json文件,因为在最开始处理的时候不知道怎么样的数据结构最合适,在真正写到输出的时候,我确定了更优良的数据结构,防止了多余的数据冗余。
6.3传入参数的差异
前面在4.2 代码中的函数组织中,我传入的列表选择了List<DateWinners.Competitor> competitors,这样只需要在主程序提前判断处理了输入的日期,然后只需要传需要日期的DateWinners数据就好了,不然传过去的是所有日期的全部数据,效率就会很低。
String specifiedDate = input[1];
boolean found = false;
DailyResults dailyResults = new DailyResults();
for (DateWinners dateWinners : dateWinnersList) {
String t=dateWinners.getDate();
t=""+t.charAt(5)+t.charAt(6)+t.charAt(8)+t.charAt(9);
if (t.equals(specifiedDate)){
found = true;
dailyResults.writeDailyResults(specifiedDate,dateWinners.getCompetitors(),outputFile);
break;
}
}
7.单元测试
使用 了JUnit 进行单元测试。
这里导入依赖库
package junit.test;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
import java.io.*;
import java.util.List;
class CountryStatsTest {
private List<Country> countries;
@BeforeEach
void setUp() {
// 模拟国家数据
Country country1 = new Country("USA", 10, 5, 3, 18, 1);
Country country2 = new Country("China", 9, 6, 4, 19, 2);
countries = List.of(country1, country2);
}
@Test
void testOutputCountryMedals() throws IOException {
String outputPath = "country_output.txt";
CountryStats countryStats = new CountryStats();
countryStats.outputCountryMedals(countries, outputPath);
BufferedReader reader = new BufferedReader(new FileReader(outputPath));
assertTrue(reader.readLine().contains("rank1:USA"));
assertTrue(reader.readLine().contains("gold:10"));
reader.close();
new File(outputPath).delete();
}
@Test
void testEmptyCountryList() throws IOException {
String outputPath = "empty_country_output.txt";
CountryStats countryStats = new CountryStats();
countryStats.outputCountryMedals(List.of(), outputPath);
BufferedReader reader = new BufferedReader(new FileReader(outputPath));
assertNull(reader.readLine()); // 应该没有输出
reader.close();
new File(outputPath).delete();
}
}
package junit.test;
//import Olympic.src.DailyResults.java;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
import java.io.*;
import java.util.List;
class DailyResultsTest {
private List<DateWinners.Competitor> competitors;
@BeforeEach
void setUp() {
// 模拟竞赛者数据
DateWinners.Competitor competitor1 = new DateWinners.Competitor("C.Gauff", "Moldova", new DateWinners.Competitor.MedalNumber(2, 0, 0, 2), 1);
DateWinners.Competitor competitor2 = new DateWinners.Competitor("PAN Zhanle", "China", new DateWinners.Competitor.MedalNumber(1, 0, 0, 1), 2);
competitors = List.of(competitor1, competitor2);
}
@Test
void testWriteDailyResults() throws IOException {
String outputPath = "test_output.txt";
DailyResults dailyResults=new DailyResults();
DailyResults.writeDailyResults("2024-08-09", competitors, outputPath);
BufferedReader reader = new BufferedReader(new FileReader(outputPath));
assertTrue(reader.readLine().contains("winner:C.Gauff"));
assertTrue(reader.readLine().contains("nationality:Moldova"));
reader.close();
// 清理测试文件
new File(outputPath).delete();
}
@Test
void testEmptyCompetitors() throws IOException {
String outputPath = "test_empty_output.txt";
DailyResults.writeDailyResults("2024-08-09", List.of(), outputPath);
BufferedReader reader = new BufferedReader(new FileReader(outputPath));
assertNull(reader.readLine()); // 应该没有输出
reader.close();
new File(outputPath).delete();
}
}
8.异常处理
对于可能的异常,我对函数都进行了异常捕获,并将对应的提示内容输出,在抛出异常后,由外层函数处理并提示输出。
9.心得体会
本次设计体验了一整个项目的设计编写和调试,包括爬数据,处理数据,编写代码,测试等,工作量还是很大的。虽然单人项目开发颇具挑战,但也给我带来了全方位的能力提升,之前从来没有完整的掌握整个项目流程,都是和同学合作,还是收获很多的。就是有点累hh,收工睡觉。