这个作业属于哪个课程 | 软件工程实践-2023学年-W班 |
---|---|
这个作业要求在哪里 | 软件工程实践第二次作业——个人实战 |
这个作业的目标 | 用java实现爬取数据与json文件处理的实战小作业 |
文章目录
一、Gitcode项目地址
二、PSP表格
PSP | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | 10 | 10 |
• Estimate | • 估计这个任务需要多少时间 | 3 | 3 |
Development | 开发 | 300 | 400 |
• Analysis | • 需求分析 (包括学习新技术) | 60 | 70 |
• Design Spec | • 生成设计文档 | 10 | 10 |
• Design Review | • 设计复审 | 10 | 10 |
• Coding Standard | • 代码规范 (为目前的开发制定合适的规范) | 5 | 5 |
• Design | • 具体设计 | 20 | 30 |
• Coding | • 具体编码 | 270 | 360 |
• Code Review | • 代码复审 | 5 | 5 |
• Test | • 测试(自我测试,修改代码,提交修改) | 60 | 120 |
Reporting | 报告 | 30 | 30 |
• Test Repor | • 测试报告 | 10 | 10 |
• Size Measurement | • 计算工作量 | 10 | 10 |
• Postmortem & Process Improvement Plan | • 事后总结, 并提出过程改进计划 | 10 | 10 |
合计 | 340 | 440 |
三、解题思路描述
3.1 网络爬取参赛者信息和比赛结果的解决方式
一开始是立马去学习了Jsoup工具进行爬取,爬取尝试中发现对应信息的HTML代码并不会直接生成,而是通过AJAX的异步请求生成的数据,因此直接在开发者工具中只选择网络中的XHR选项卡,直接获取到了对应的数据接口,如图。
找到数据接口后也就懒得用jsoup工具爬取了,直接访问链接后并ctrl+s将json数据保存到项目中作为静态资源。
3.2 Json文件解析的解决方式
简单搜索资料后选择了jackson工具进行学习与使用。使用jackson包的读取json文件为JsonNode树的方式,并通过不断get的方法即可得到对应的数据。值得一提的是,jackson集成了javaBean和Json数据的相互转换功能,但由于我是直接将网站原数据保存下来而不是爬取需要的数据并转换为简单的Json格式,因此我如果要从这样的Json文件转换为JavaBean的话需要的映射将会比较复杂(也有可能是我理解太浅,说不定有更简单的方式)。因此我放弃了直接read文件并自动转为JavaBean的方式,而是逐个获取需要的数据并手动在每次获取后将数据载入对应JavaBean中。比较死板但对于这个小作业来说也是足够的。
3.3 文件读写方式
我不是很喜欢用最为基础的File类的read等方法,因此在这次作业中,我用Scanner和Formatter处理文件流,直接使用平常习惯的nextLine和format等方法
3.4 打包方式
一开始我使用的是IDEA自带的工件打包方式,可以设定导出包名,包内结构,依赖提取方式等。不过在进行junit的单元测试后重新打包时,发现该打包方式不识别junit包,不知道是不是单测本就不参与打包的原因。出于谨慎,我选择了导入插件maven-shade-plugin的打包方式。
四、接口设计和实现过程
进行初步分析后,我计划只创建四个类。启动入口的DWASearch,以及实体类中的GameResult,Player,PlayerResult。其中实体类中仅包含将数据转为对应格式字符串的方法以及设置数据的方法。而DWASearch中则包含getPlayers和getResults(String fileName)这两个方法,main方法中则涉及了关于命令行参数的检测与读取以及对文件的读写操作。
设计结构如下:
五、关键代码展示
/**
* 获取所有参赛者信息
* @return {@link Player}类的List集合
*/
public static List<Player> getPlayers() {
List<Player> result = new ArrayList<>();
ObjectMapper mapper = new ObjectMapper();
try {
File jsonFile = new File(root+"/src/data/Players.json");
JsonNode root = mapper.readTree(jsonFile);
if (root.isArray()) {
for (JsonNode country : root) {
for (JsonNode player : country.get("Participations")) {
result.add(new Player(player.get("Gender").asInt(), player.get("PreferredLastName").asText(),
player.get("PreferredFirstName").asText(), player.get("NAT").asText()));
}
}
}
} catch (IOException e) {
throw new RuntimeException(e);
}
return result;
}
/**
* 获得某个比赛的结果
* @param name 比赛名字
* @return {@link GameResult}比赛结果类
*/
public static GameResult getResult(String name) throws RuntimeException{
GameResult result = new GameResult();
ObjectMapper mapper = new ObjectMapper();
try {
File jsonFile = new File(root+"/src/data/" + name + ".json");
JsonNode root = mapper.readTree(jsonFile);
for (JsonNode heat : root.get("Heats")) {
String heatName = heat.get("Name").asText();
for (JsonNode playerResult : heat.get("Results")) {
int rank = playerResult.get("Rank").asInt();
String playerName = playerResult.get("FullName").asText();
ArrayList<Double> scores = new ArrayList<>();
for (JsonNode dive : playerResult.get("Dives")) {
scores.add(dive.get("DivePoints").asDouble());
}
result.setScore(heatName, playerName, rank, scores);
}
}
} catch (IOException e) {
throw new RuntimeException(e);
}
return result;
}
public static void main(String[] args){
String pathIn = root+"/src/data/input.txt";
String pathOut = root+"/src/data/output.txt";
if(args.length >= 1){
pathIn = args[0];
}
if(args.length >= 2){
pathOut = args[1];
}
try{
Scanner in = new Scanner(new File(pathIn));
Formatter out = new Formatter(pathOut);
while (in.hasNextLine()){
String str = in.nextLine();
String[] ss = str.split(" ");
switch (ss[0]){
case "players":
for (Player player : getPlayers()) {
out.format("%s\n",player);
out.format("-----\n");
}
break;
case "result":
if(ss.length == 1){
out.format("N/A\n-----\n");
break;
}
String[] sss;
if(ss[ss.length-1].equals("detail")){
sss = Arrays.copyOfRange(ss,1,ss.length-1);
}else {
sss = Arrays.copyOfRange(ss,1,ss.length);
}
String name = String.join(" ", sss);
GameResult result = null;
try{
result = getResult(name);
}catch (RuntimeException ex){
out.format("N/A\n-----\n");
break;
}
out.format(result.getResult());
break;
default:
out.format("Error\n-----\n");
break;
}
out.flush();
}
}catch (Exception e){
System.out.println(e);
}
}
六、单元测试
采用junit的单元测试方式,编写了测试类PlayerTest,测试了getPlayer和getResults两个核心方法,如下
/**
* 测试参赛者信息获取功能
*/
@Test
public void getPlayersTest(){
List<Player> players = DWASearch.getPlayers();
for (Player player : players) {
System.out.println(player);
System.out.println("-----");
}
}
七、异常处理
7.1 junit导入后出现程序包org.junit不存在的异常
属实没见过的错误。经排查后,发现是因为本次作业需迎合作业要求,我的内容根在源文件夹下创建了测试源文件夹(也就是test文件夹在src下),导致出现嵌套,因此在构建项目,解析类的时候,输出目录target中没有出现test的目录及对应的测试类。导致的无法找到对应org.junit的包(即使已经能在外部库中看到对应的目录也报错说找不到对应的程序包一度使我崩溃)
7.2 命令行参数异常的容错性处理
本次作业要求可以使用"java -jar DWASearch.jar 输入文件路径 输出文件路径"这种形式的命令执行代码,本着不信任任何用户输入的原则,对命令行参数进行如下容错性处理(即未输入文件路径时指定默认文件目录,以及在输入的文件路径无法正常打开文件时直接抛出异常并关闭程序):
private static String root;
static {
root = System.getProperty("user.dir");
}
public static void main(String[] args){
String pathIn = root+"/src/data/input.txt";
String pathOut = root+"/src/data/output.txt";
if(args.length >= 1){
pathIn = args[0];
}
if(args.length >= 2){
pathOut = args[1];
}
try{
Scanner in = new Scanner(new File(pathIn));
Formatter out = new Formatter(pathOut);
//其它代码
}catch (Exception e){
System.out.println(e);
}
}
7.3 采用相对路径读取文件时由于打包导致的路径错误异常处理
本次作业我一开始是直接通过"src/data/players.json"这样的格式访问资源文件的,在编写代码的过程中一切正常,但到打包阶段才发现由于工作根目录的转变这样子的路径是无法找到对应的文件的。经过一番搜索后最后选用了System.getPath(“user.dir”)的方式获取实时的程序工作根目录,然后再通过相对路径的方式进行访问资源。
八、心得体会
从错误总结的角度上讲,有几个地方是我觉得下次应该改进的(基本上是膜拜了一下大佬的项目结构后的感悟):
- 应该要单独分出一个DataSource处理类来解析Json文件而不是将getPlayer和getResults这两个核心模块集成在程序入口类DWASeach.java中。
- 分完模块后应该还是要遵循之前学习Springboot时学习的规范,即先写模块接口,并在同级目录下创建impl目录存放实现类。
- 处理数据源的方法中应该将获取初赛数据,获取半决赛数据,获取决赛数据这几个功能分开,而不是结合层一个getResult方法。虽然出于节约打开文件次数的考虑我选择了后者,但现在体会下来前者的逻辑其实更合理,功能分的更开,程序粒度更细致。
新的体会:
- 之前没有接触过用java进行网络爬取的功能,虽然这次学习到的jsoup最后没有用上,但也从另一个角度加深了java对于网络请求与数据处理的理解
- 这次由于老师限定了项目的文件结构,导致我对项目模块内容根进行了一些改动和设置,从而遇到了之前没有见到过的bug(见7.1)以及充分理解了测试这一大模块的重要性,可以说收获颇丰。
- 在打包过程中发现了相对路径的严重问题(见7.3)。虽然最后我使用了System.getPath(“user.dir”)的方法暂时解决了这个问题,但是如果将jar这个包转移位置后又会导致找不到文件。尝试采用了类反射获取类路径的方式,但没有成功,希望在接下来的工作中能够解决这个问题。