巴黎奥运会的网络爬虫——软件工程实践第二次作业

这个作业属于哪个课程https://bbs.csdn.net/forums/2401_CS_SE_FZU
这个作业要求在哪里https://bbs.csdn.net/topics/619294904
这个作业的目标PSP表格,git版本控制项目开发,网络监控与爬虫数据,文件的IO读写,类和接口封装,性能优化项目,测试类
其他参考文献

Gitcode项目地址

fork仓库的项目地址:

https://devcloud.cn-north-4.huaweicloud.com/codehub/project/c0f3e49ae1304b23bf8ab43916a2a04e/codehub/2687131/home?ref=master

助教仓库地址:

https://devcloud.cn-north-4.huaweicloud.com/codehub/project/15a8ff57fe12409f9e3511d656a41e3e/codehub/2686893/home?ref=master&filePath=Java%252F222200231%25E6%25B8%25B8%25E7%25AB%25A3%25E8%25B6%2585

PSP表格

PSPPersonal Software Process Stages预估耗时(分钟)实际耗时(分钟)
Planning计划6575
• Estimate• 估计这个任务需要多少时间6575
Development开发12501300
• Analysis• 需求分析 (包括学习新技术)130160
• Design Spec• 生成设计文档7080
• Design Review• 设计复审3545
• Coding Standard• 代码规范 (为目前的开发制定合适的规范)2535
• Design• 具体设计110130
• Coding• 具体编码720760
• Code Review• 代码复审5565
• Test• 测试(自我测试,修改代码,提交修改)150170
Reporting报告170160
• Test Report• 测试报告6555
• Size Measurement• 计算工作量2525
• Postmortem & Process Improvement Plan• 事后总结, 并提出过程改进计划8080
合计14851560

解题思路描述

问题1:数据爬取

先大致阅读网站,找到需要的奖牌榜和每日赛程榜

在这里插入图片描述

点击F12打开开发者工具进行网络监控,根据网络响应查找到获取奖牌榜对应的网络请求,如图我们找到了对应的Json数据
在这里插入图片描述
在这里插入图片描述

找到对应的请求头还有请求参数

  • URL 解析
    基础 URL: https://api.cntv.cn/Olympic/getOlyMedals
    这是从 CNTV API 获取奥运会奖牌信息的端点。
  • GET: 使用的请求方法是 GET,通常用于从服务器检索数据。
  • 查询参数
    • serviceId=2024aoyun: 这个参数可能指定了服务或事件的 ID,在这里与 2024 年奥运会相关。
    • olyseason=2024S: 这个参数表示奥运会的季节,“2024S”可能指的是 2024 年夏季奥运会。
    • itemcode=GEN-------------------------------: 这个参数可能用于过滤或指定奥运会中的一般类别或事件代码。
    • t=jsonp: 这个参数表明响应格式是 JSONP,通常用于跨域请求以允许在脚本标签中获取数据。
    • cb=banomedals: 这是 JSONP 中用于包装 JSON 响应的回调函数名称,允许其作为脚本执行。
      请求方法

使用同样的方法去分析每日赛程

  • URL 解析
    基础 URL: https://api.cntv.cn/olympic/getOlyMatchList
    这是用于获取奥运会比赛列表的 API 端点。
  • 查询参数
    • itemcode=: 这个参数为空,可能用于指定特定的项目代码,但在此请求中未使用。
    • startdate=20240811: 这个参数指定了比赛的开始日期,格式为 YYYYMMDD,这里是 2024 年 8 月 11 日。
    • venue=: 这个参数为空,可能用于指定比赛场地,但在此请求中未使用。
    • medal=: 这个参数为空,可能用于过滤奖牌信息,但在此请求中未使用。
    • t=jsonp: 这个参数表明响应格式是 JSONP,通常用于跨域请求以允许在脚本标签中获取数据。
    • cb=OM: 这是 JSONP 中用于包装 JSON 响应的回调函数名称,允许其作为脚本执行。
    • serviceId=2024aoyun: 这个参数可能指定了服务或事件的 ID,在这里与 2024 年奥运会相关。
    • olyseason=2024S: 这个参数表示奥运会的季节,“2024S”可能指的是 2024 年夏季奥运会。
  • 请求方法 GET: 使用的请求方法是 GET,通常用于从服务器检索数据。

Apifox 是一个集成了多种功能的 API 工具,它结合了 Postman、Swagger、Mock 和 JMeter 的功能。在这里我使用apifox进行网络请求,发现只要不传入查询参数(为默认)则能获取到我们想要的json数据,显示中文而不是字符码。

最后我们把奖牌榜的数据下载到本地中

问题2:功能实现

项目构建

使用maven项目

Apache Maven是一个软件项目管理和理解工具。它基于项目对象模型(POM)的概念,可以从一个中心信息点管理项目的构建、报告和文档。
Maven的一些主要好处:一致的项目结构,声明式配置,广泛的插件范围,中心化的仓库,项目管理,依赖管理,所以此项目使用maven进行构建
我的maven学习笔记

依赖注入
  • fastjson
    来自阿里巴巴的Java库,用于高效地解析和生成JSON数据。快速、易用,支持Java对象与JSON字符串的相互转换。
  • com.fasterxml.jackson.databind
    Liferay定制版本的Jackson库,用于处理JSON数据。功能强大,支持复杂的数据绑定和转换,兼容性强。
  • junit (版本4.12 和 4.13)
    流行的Java单元测试框架。广泛使用,易于编写测试用例,提供断言和测试运行的功能。
  • j- unit-jupiter
    JUnit 5中的一个模块,提供对JUnit 5的支持。更现代化,支持新的特性如嵌套测试和动态测试。
  • com.squareup.okhttp3:okhttp:4.9.3 是一个功能强大的 HTTP 客户端库,适用于 Java 和 Android 开发。通过使用 OkHttp,开发者可以轻松实现高效的网络请求和响应处理。
设计数据获取方式
  • 因为奖牌榜已经固定下来不会再发生改变,所以直接把奖牌榜的Json数据下载到本地
  • 因为每日赛程和日期有关,我们观察发现查询参数startdate即是日期,可以采用字符串拼接进行网络请求

划分功能模块

我将主要功能封装成了几个功能单元,将主要功能封装成几个功能单元的好处主要有以下几点:

  • 模块化:每个功能单元都可以独立开发和测试,这使得开发过程更加灵活和高效。
  • 可重用性:功能单元可以在多个地方重复使用,这可以减少代码的冗余,提高代码的可重用性。
  • 易于维护:当需要修改或修复某个功能时,只需要关注相关的功能单元,而不需要修改整个应用,这使得代码更易于维护。
  • 易于理解:将复杂的功能分解为几个简单的功能单元,可以使得代码更易于理解和学习。
划分单元Personal Software Process Stages
• OlympicSearch• 主类,接受两个参数inputFile和outputFile
• HttpUtils• 进行对指定日期赛程的网络请求
• DateUtils• 有两个函数,一个是检验日期是否合法,一个是检验日期是否在巴黎奥运会的期间(0724-0811)
• CmdUtils• 正则表达式检验是否是schedule命令
• ErrorUtils• 错误处理,writeError和writeNA,在出现错误时向目标文件夹输出
• JsonUtils• nioMethod从json文件中读取json数据,JsonParser把json对象转化一个jsonobject
• FileUtils• 主要按照指定格式负责写入文件,writeMatchList和writeMedalList分别写入奖牌榜和赛程表

接口设计和实现过程

工具类设计

  • CmdUtils,判断是否是schedule指令
  • DateUtils,其中包含两个静态方法:isValidDate和isWithinOlympics。isValidDate方法接收一个日期字符串和一个日期格式字符串,然后尝试将日期字符串按照给定的格式解析为LocalDate对象。如果解析成功,那么返回true,表示日期字符串是有效的;如果解析失败(抛出DateTimeParseException异常),那么返回false,表示日期字符串是无效的
  • ErrorUtils,其中包含两个静态方法:writeError和writeNA。这两个方法都接收一个输出文件名作为参数,并向该文件写入特定的字符串。
  • FileUtils,按照指定格式写入文件
  • HttpUtils,主要功能是发起一个HTTP GET请求到指定的URL(这里是一个获取2024年奥运会比赛列表的API),并获取请求的响应。
  • JsonUtils,JsonParser方法接收一个JSON字符串,解析该字符串为一个JSONObject对象,并返回该对象中名为"data"的子对象。

实体类设计

  • OlympicSearch

实现过程流程图

关键代码展示

HttpUtils

public class HttpUtils {
    /**
     * 发起HTTP GET请求以获取奥运会比赛列表
     *
     * @param line 包含日期信息的输入字符串,格式为"command YYYY"
     * @return 包含HTTP响应内容的StringBuilder对象
     * @throws RuntimeException 如果在建立连接或读取响应时发生异常
     */
    public static StringBuilder HttpRequest(String line) {
        StringBuilder response = new StringBuilder();
        String datePart = line.split(" ")[1];
        String startdate = "2024" + datePart;

        String urlString = "https://api.cntv.cn/olympic/getOlyMatchList?itemcode=&startdate=" + startdate + "&venue=&medal=&serviceId=2024aoyun&olyseason=2024S";
        try {
            URL url = new URL(urlString);
            HttpURLConnection connection = (HttpURLConnection) url.openConnection();
            connection.setRequestMethod("GET");
            connection.setRequestProperty("User-Agent", "Apifox/1.0.0 (https://apifox.com)");
            connection.setRequestProperty("Accept", "*/*");
            connection.setRequestProperty("Host", "api.cntv.cn");
            connection.setRequestProperty("Connection", "keep-alive");
            connection.setConnectTimeout(10000);
            connection.setReadTimeout(10000);
            int responseCode = connection.getResponseCode();
            if (responseCode == HttpURLConnection.HTTP_OK) {
                BufferedReader in = new BufferedReader(new InputStreamReader(connection.getInputStream()));
                String inputLine;
                while ((inputLine = in.readLine()) != null) {
                    response.append(inputLine);
                }
                in.close();
            } else {
                System.out.println("GET request failed");
            }
            return response;
        } catch (ProtocolException e) {
            throw new RuntimeException(e);
        } catch (MalformedURLException e) {
            throw new RuntimeException(e);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
}

这段代码定义了一个名为HttpRequest的静态方法,该方法接收一个字符串参数line,并返回一个StringBuilder对象。这个方法的主要功能是发起一个HTTP GET请求到指定的URL(这里是一个获取2024年奥运会比赛列表的API),并获取请求的响应。这个方法的好处在于它封装了HTTP请求的细节,使得获取网络数据变得简单。只需提供一个包含日期的字符串,就可以获取到对应日期的奥运会比赛列表。此外,这个方法还处理了各种可能的异常,提高了代码的健壮性。

JsonUtils

public class JsonUtils {
    /**
     * 解析JSON字符串并返回其中的"data"对象
     *
     * @param json 需要解析的JSON字符串
     * @return 包含"data"对象的JSONObject
     */
    public static JSONObject JsonParser(String json) {
        JSONObject dataObject = JSONObject.parseObject(json).getJSONObject("data");
        return dataObject;
    }

    /**
     * 从文件中读取JSON内容并解析为JSONObject
     *
     * @param file 包含JSON数据的文件
     * @return 解析后的JSONObject
     * @throws IOException 如果在读取文件时发生I/O错误
     */
    public static JSONObject nioMethod(File file) throws IOException {
        String jsonString = new String(Files.readAllBytes(Paths.get(file.getPath())));
        System.out.println(JSONObject.parseObject(jsonString));
        JSONObject jsonObject = JSONObject.parseObject(jsonString);
        return jsonObject;
    }
}

这段代码定义了一个名为JsonUtils的类,其中包含两个静态方法:JsonParser和nioMethod。
JsonParser方法接收一个JSON字符串,解析该字符串为一个JSONObject对象,并返回该对象中名为"data"的子对象。
nioMethod方法接收一个文件对象,读取该文件的所有内容为一个字符串,然后解析该字符串为一个JSONObject对象,并返回该对象。
这段代码的好处在于它提供了一种简单的方式来解析JSON数据。JsonParser方法可以用来从JSON字符串中提取特定的数据,而nioMethod方法可以用来从文件中读取并解析JSON数据。这两个方法都可以帮助简化处理JSON数据的代码,提高代码的可读性和可维护性。

ErrorUtils

public class ErrorUtils {
    /**
     * 向指定的输出文件中写入错误信息
     *
     * @param outputFile 要写入错误信息的输出文件路径
     * @throws IOException 如果在写入文件时发生I/O错误
     */
    public static void writeError(String outputFile) {
        try (FileWriter file1 = new FileWriter(outputFile, true)) {
            file1.write("Error\n");
            file1.write("-----\n");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    /**
     * 向指定的输出文件中写入"N/A"信息
     *
     * @param outputFile 要写入"N/A"信息的输出文件路径
     * @throws IOException 如果在写入文件时发生I/O错误
     */
    public static void writeNA(String outputFile) {
        try (FileWriter file1 = new FileWriter(outputFile, true)) {
            file1.write("N/A\n");
            file1.write("-----\n");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

这段代码定义了一个名为ErrorUtils的类,其中包含两个静态方法:writeError和writeNA。这两个方法都接收一个输出文件名作为参数,并向该文件写入特定的字符串。
writeError方法向文件写入"Error\n-----\n",而writeNA方法向文件写入"N/A\n-----\n"。这两个方法都使用了FileWriter类,它是Java I/O库中的一个类,用于写入字符文件。这两个方法的主要用途是记录错误信息和不可用的数据。
这段代码的好处在于它提供了一种简单的方式来记录错误和不可用的数据。当程序在运行过程中遇到错误或无法获取到预期的数据时,可以调用这两个方法将相关信息写入到文件中,这对于后续的错误分析和数据清理非常有用.

DateUtils

public class DateUtils {
    /**
     * 验证给定的日期字符串是否为有效日期
     *
     * @param dateStr   需要验证的日期字符串
     * @param dateFormat 日期格式,用于解析日期字符串
     * @return 如果日期字符串符合指定格式并且是有效日期,则返回true;否则返回false
     */
    public static boolean isValidDate(String dateStr, String dateFormat) {
        DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern("yyyy" + dateFormat);
        try {
            LocalDate.parse("2024" + dateStr, dateFormatter);
        } catch (DateTimeParseException e) {
            return false;
        }
        return true;
    }

    /**
     * 检查输入日期是否在2024年奥运会期间
     *
     * @param input      包含日期的输入字符串
     * @param dateFormat 日期格式,用于解析输入字符串中的日期
     * @return 如果日期在2024年奥运会期间(2024年7月24日至8月11日之间),则返回true;否则返回false
     */
    public static boolean isWithinOlympics(String input, String dateFormat) {
        String dateStr = input.split(" ")[1];
        DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern("yyyy" + dateFormat);
        LocalDate date = LocalDate.parse("2024" + dateStr, dateFormatter);

        LocalDate olympicsStart = LocalDate.of(2024, 7, 24);
        LocalDate olympicsEnd = LocalDate.of(2024, 8, 11);

        return !date.isBefore(olympicsStart) && !date.isAfter(olympicsEnd);
    }
}

这段代码定义了一个名为DateUtils的类,其中包含两个静态方法:isValidDate和isWithinOlympics。
isValidDate方法接收一个日期字符串和一个日期格式字符串,然后尝试将日期字符串按照给定的格式解析为LocalDate对象。如果解析成功,那么返回true,表示日期字符串是有效的;如果解析失败(抛出DateTimeParseException异常),那么返回false,表示日期字符串是无效的。
isWithinOlympics方法接收一个输入字符串和一个日期格式字符串,然后提取输入字符串中的日期部分,并将其解析为LocalDate对象。然后,该方法检查解析出的日期是否在2024年奥运会的开始日期(2024年7月24日)和结束日期(2024年8月11日)之间。如果是,那么返回true;否则,返回false。
这两个方法的主要用途是验证和处理日期数据。isValidDate方法可以用来检查用户输入的日期是否符合预期的格式,而isWithinOlympics方法可以用来检查一个事件是否在2024年奥运会期间发生。这两个方法都可以帮助提高代码的健壮性,防止因为无效的日期数据而导致的错误。

性能改进

缓存

为了在Java中实现本地缓存优化,我使用一个简单的内存缓存机制。
通过使用HashMap来存储响应,其中请求URL作为键,响应内容作为值

实现步骤
  • 添加缓存映射:使用HashMap存储响应,键为URL,值为响应体。
    // 创建一个简单的缓存
    private static final Map<String, String> cache = new HashMap<>();
  • 在发起请求前检查缓存:在发起HTTP请求之前,检查响应是否已经在缓存中。
        // 检查缓存中是否已有响应
        if (cache.containsKey(urlString)) {
            System.out.println("Cache hit for URL: " + urlString);
            response.append(cache.get(urlString));
            return response;
        }
  • 将响应存储到缓存中:在接收到响应后,将其存储到缓存中。
                // 将响应存储到缓存中
                cache.put(urlString, responseBody);

这种简单的缓存机制可以减少网络请求的次数,并提高性能,特别是在数据不经常变化的情况下

优化效果

对于一个反复查询同一个日期的单元测试,优化速度达到了 60% !!

  • 优化前

  • 优化后

OkHttpClient

优化说明
连接池复用: 使用 OkHttpClient 的全局静态实例,确保连接池的复用,避免每次请求都创建新的连接。
简化代码: OkHttp3 提供了更简洁的 API,减少了手动管理连接和读取响应的复杂性。
自动处理资源: 使用 try-with-resources 语句自动关闭响应体,确保资源得到妥善管理。
更好的错误处理: OkHttp3 提供了更丰富的错误处理机制,可以更方便地获取响应状态码和错误信息。
通过这些优化,你的代码将更加高效和易于维护。

public class HttpUtils {
    // 创建一个全局静态的 OkHttpClient 实例
    private static final OkHttpClient client = new OkHttpClient();
                                    ...
                                    ...
        try (Response httpResponse = client.newCall(request).execute()) {
                                    ...

单元测试

测试方法

依赖注入引入 junit4 作为单元测试工具,

测试的覆盖率和通过率

  • 覆盖率高:所有类、方法和大部分行都达到了100%的覆盖率,说明测试用例涵盖了代码的几乎所有执行路径。

  • 测试多样性:测试用例包括了参数不足、参数过多、文件不存在、日期格式错误、日期范围错误等多种情况。这确保了代码在各种情况下的表现都得到了验证。

  • 测试通过率:所有测试用例都通过了,意味着代码在每种测试情况下都表现如预期,没有出现错误。

测试的设计分析

这个测试类OlympicSearchTest设计了多个测试用例,覆盖了OlympicSearch类的main方法可能遇到的各种情况。这些测试用例包括:

  • 参数数量不足或过多的情况(insufficientArgsTest, singleArgTest, excessArgsTest)。
  • 输入文件不存在的情况(nonExistentFileTest)。
  • 输入数据的格式错误或者不符合预期的情况(singleLineTotalTest, missingDateParamTest, incorrectDateFormatTest1, incorrectDateFormatTest2, outOfRangeDateTest1, outOfRangeDateTest2, incorrectCommandTest1, incorrectCommandTest2, incorrectCommandTest3)。
  • 输入数据符合预期的情况(inRangeDateTest, mixedScheduleTotalTest)。
    每个测试用例都通过调用OlympicSearch.main方法并传入特定的参数来进行测试。这种测试方法的设计使得测试用例能够覆盖main方法的各种可能的执行路径,从而提高了测试的覆盖率。
public class OlympicSearchTest {
    //  0 arguments
    @Test
    public void insufficientArgsTest() throws IOException {
        OlympicSearch.main(new String[]{""});
    }
    // 1 argument
    @Test
    public void singleArgTest() throws IOException {
        OlympicSearch.main(new String[]{"\"input.txt\""});
    }
    //  3 arguments
    @Test
    public void excessArgsTest() throws IOException {
        OlympicSearch.main(new String[]{"\"input.txt\",\"input.txt\"", "output.txt"});
    }
    //  non-existent input file
    @Test
    public void nonExistentFileTest() throws IOException {
        OlympicSearch.main(new String[]{"\"input_test.txt\",\"output.txt\""});
    }
    @Test
    public void singleLineTotalTest() throws IOException {
        //total
        OlympicSearch.main(new String[]{"input1.txt", "output1.txt"});
    }
    @Test
    public void missingDateParamTest() throws IOException {
        // schedule
        OlympicSearch.main(new String[]{"input2.txt", "output2.txt"});
    }
    @Test
    public void incorrectDateFormatTest1() throws IOException {
        //  schedule 7814
        OlympicSearch.main(new String[]{"input3.txt", "output3.txt"});
    }
    @Test
    public void incorrectDateFormatTest2() throws IOException {
        // schedule 1301
        OlympicSearch.main(new String[]{"input4.txt", "output4.txt"});
    }
    @Test
    public void outOfRangeDateTest1() throws IOException {
        // schedule 0723
        OlympicSearch.main(new String[]{"input5.txt", "output5.txt"});
    }
    @Test
    public void outOfRangeDateTest2() throws IOException {
        //  schedule 0812
        OlympicSearch.main(new String[]{"input6.txt", "output6.txt"});
    }
    @Test
    public void inRangeDateTest() throws IOException {
        // schedule 0724
        OlympicSearch.main(new String[]{"input7.txt", "output7.txt"});
    }
    @Test
    public void incorrectCommandTest1() throws IOException {
        //  command
        OlympicSearch.main(new String[]{"input8.txt", "output8.txt"});
    }

    @Test
    public void incorrectCommandTest2() throws IOException {
        // schedule 0812 acasnci
        OlympicSearch.main(new String[]{"input9.txt", "output9.txt"});
    }

    @Test
    public void incorrectCommandTest3() throws IOException {
        // total total
        OlympicSearch.main(new String[]{"input10.txt", "output10.txt"});
    }

    // mixed schedule and total
    @Test
    public void mixedScheduleTotalTest() throws IOException {
        OlympicSearch.main(new String[]{"input11.txt", "output11.txt"});
    }
}

心得体会

  • 需求分析的关键性: 在项目的初期阶段,极其体会到需求分析尤为重要,最好是对整个项目有一个整体的看法和想法的构建,这样才能提高自己的开发速度
  • 遵循代码规范: 在实际开发中,遵循统一的代码规范对提高代码的可读性和可维护性至关重要。
  • 重视测试与反馈: 在项目中,频繁进行迭代和测试。这让我认识到及时的测试能够帮助我们迅速发现问题,这个也很重要。
  • 收获很大,大大提高了自己的开发速度,并且边做变学完成自己的文档笔记很有成就感
  • 更熟悉了整个项目开发的过程,体会还是比较深的

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值