基于SpringBoot的疫情信息统计平台【完整项目源码】

温馨提示:步骤讲解详细,文章内容较长,获取源码直接浏览文末

Part 1

【学习目标】

1) SpringBoot框架 2) 业务功能的开发思路 3) 爬虫的底层原理 4) E-chart图标展示

【项目的诞生】

项目成员:项目经理 PM 产品经理 PD、 UI设计师 UED、 前端工程师 FE、后端工程师 RD、 测试工程师 QA、运维工程师 OP

【功能的诞生】

产品经理 -> 需求评审会 -> UI设计师(交互) -> UI评审 -> 技术 -> 技术方案设计 -> 开发(1/3) -> 测试和修改

【爬虫的基础知识】

(一) 搜索引擎

“搜索引擎” 是一种帮助用户搜索他们需要内容的计算机程序

本质:连接人与内容

存储数据的处理方式:

如:一篇文章 -> 语义分析 -> 关键字提取 -> 根据关键字及出现次数等 -> 倒排存储

关键字搜索:

【查询分析】 关键字相关的近义词、反义词、关联信息(ip、地址、用户信息)

(二)爬虫分类

通用型爬虫(搜索引擎使用)

采集的数据是整个网站的数据,不论数据是何种形态、格式,使用通用的方法来存储和处理

垂直型爬虫(对特定内容的采集)

采集的数据是指定的,格式是指定的,使用个性化的方法来处理

网站的首页作为种子, 爬虫去采集能够分解出多少不重复的子链接,及其数据。

采集/下载页面 -> 分解为数据本身(存储) 、新的链接(依次向下爬取,类似树的深度遍历)-> 直到没有新链接代表采集完成 (链接使用队列来存储)

(三)爬虫数据的分析

1、浏览器开发者工具

分析数据源 1) 腾讯新闻出品 https://news.qq.com/zt2020/page/feiyan.htm#/?nojump=1

工具的打开方式:F12 / Ctrl + Shift + I / 更多工具 - 开发者工具 / 右键 - 检查

选中Network - Preserve log(保存持续的日志), 重新刷新页面,可以看到网页所有的请求连接和返回数据。

想拿到表格中 国内疫情的数据情况

基于SpringBoot的疫情信息统计平台【完整项目源码】_第1张图片

分析的方式:通过搜索框,搜索想要获得数据的具体数值

分析后的请求地址: https://view.inews.qq.com/g2/getOnsInfo?name=disease_h5

返回数据的格式是json

基于SpringBoot的疫情信息统计平台【完整项目源码】_第2张图片

2)丁香医生出品 https://ncov.dxy.cn/ncovh5/view/pneumonia?scene=2&clicktime=1579579384&enterid=1579579384&from=singlemessage&isappinstalled=0

基于SpringBoot的疫情信息统计平台【完整项目源码】_第3张图片

分析的方式同上

分析后的请求地址:

https://ncov.dxy.cn/ncovh5/view/pneumonia?scene=2&from=singlemessage&isappinstalled=0

返回数据的格式是html

2、postman(模拟http请求的工具)

验证分析出来的请求地址,在排除上下文环境后,是否依然能够拿到数据。

比如有的请求,依赖cookie、依赖动态的参数等等

3、爬虫破解问题

拿到数据是核心的一步。

公开数据只要是非恶意就允许采集的,非恶意是指模仿人的行为采集的行为,不会高并发或者恶意攻击。

隐私数据都是有强大的加密处理的,防爬虫的手段是安全领域内的一大问题。

Part 2

【解析数据】

(一) 认识JSON

JSON = JavaScript Object Notation (JavaScript对象表示法)

本质上,是存储和交换文本信息的语法。是一种轻量级的文本数据格式。

java领域内解析json的工具:gson 、 fastjson 、jackson

JSON 和 Java实体类

JSON JAVA实体类
string java.lang.String
number java.lang.Number (Double)
true|false java.lang.Boolean
null null
array java.util.List (ArrayList)
object java.util.Map (LinkedTreeMap)

(二) Gson

是google推出的,用来在json数据和java对象之间进行转换的类库。

Gson  gson = new Gson();
Gson  gson1 = new GsonBuilder().create();

// 将对象obj转化为json字符串
String jsonStr = gson.toJson(Obj);
// 将json字符串转化为java对象
T obj = gson.fromJson(jsonStr,class)

使用方式

1)引入gson依赖

        
            com.google.code.gson
            gson
            2.8.6
        
        

2)确认要转化的数据格式

import lombok.AllArgsConstructor;
import lombok.Data;

@Data @AllArgsConstructor
public class DataBean {

    private String area;
    private int nowConfirm;
    private int confirm;
    private int heal;
    private int dead;
}

3)解析文本数据

public class DataHandler {

    public static void main(String[] args) throws Exception{
        getData();
    }


    public static List getData() throws Exception {
//        Gson gson = new Gson();
        Gson gson1 = new GsonBuilder().create();
//        Map map = gson.fromJson(testStr,Map.class);
//        System.out.println(map);


        // 读取文件中的文本内容   然后再转化为java对象
//        File file = new File("tmp.txt");

        FileReader fr = new FileReader("tmp.txt");
        char[] cBuf = new char[1024];
        int cRead = 0;
        StringBuilder builder = new StringBuilder();
        while ((cRead = fr.read(cBuf)) > 0) {
            builder.append(new String(cBuf, 0, cRead));
        }

        fr.close();

//        System.out.println(builder.toString());
        Gson gson = new Gson();
        Map map = gson.fromJson(builder.toString(), Map.class);
        System.out.println(map);

        ArrayList areaList = (ArrayList) map.get("areaTree");
        Map dataMap = (Map) areaList.get(0);
        ArrayList childrenList = (ArrayList) dataMap.get("children");

        // 遍历然后转化
        List result = new ArrayList<>();

        for (int i = 0; i < childrenList.size(); i++) {
            Map tmp = (Map) childrenList.get(i);
            String name = (String)tmp.get("name");

            Map totalMap = (Map) tmp.get("total");
            double nowConfirm = (Double)totalMap.get("nowConfirm");
            double confirm = (Double)totalMap.get("confirm");
            double heal = (Double)totalMap.get("heal");
            double dead = (Double)totalMap.get("dead");

            DataBean dataBean = new DataBean(name,(int)nowConfirm,(int)confirm,
                    (int)heal,(int)dead);

            result.add(dataBean);
        }

        System.out.println(result);

        return result;
    }
}

Part 3

(三) 将数据展示在页面中

基于SpringBoot的疫情信息统计平台【完整项目源码】_第4张图片

1、编写service和controller

public interface DataService extends IService {

    List list();

}
@Service
public class DataServiceImpl implements DataService {

    @Override
    public List list() {
        List result = null;

        try {
            result = DataHandler.getData();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return result;
    }
}
@Controller
public class DataController {

    @Autowired
    private DataService dataService;

    @GetMapping("/")
    public String list(Model model){
        List list = dataService.list();
        model.addAttribute("dataList",list);
        return "list";
    }
    
}

2、编写页面(注意已引入thymeleaf的maven依赖)




    
    Title




国内疫情情况如下


地区 现有确诊 累计确诊 治愈 死亡
name nowConfirm confirm heal dead

(四) 转为实时数据

涉及知识点:用java代码模拟http请求

1、复习get和post请求

分别在使用场景、参数传递方式、数据大小限制、安全性等方面的异同

2、HttpURLConnection

连接时间和读取时间
 连接时间:发送请求端 连接到  url目标地址端的时间
           受到距离长短和网络速度的影响
 读取时间:指连接成功后  获取数据的时间
           受到数据量和服务器处理速度的影响

1) 通过创建url打开远程链接 (HttpURLConnection) 2) 设置相关参数(超时时间和请求头等等) 3) 发送请求 4) 接收结果(使用InputStream和BufferedReader)

public static String doGet(String urlStr) {
        HttpURLConnection conn = null;
        InputStream is = null;
        BufferedReader br = null;
        StringBuilder result = new StringBuilder();

        try {
            URL url = new URL(urlStr);
            // 通过url打开一个远程连接  强转类型
            conn = (HttpURLConnection) url.openConnection();
            conn.setRequestMethod("GET");

            // 连接时间和读取时间
            //  连接时间:发送请求端 连接到  url目标地址端的时间
            //            受到距离长短和网络速度的影响
            //  读取时间:指连接成功后  获取数据的时间
            //            受到数据量和服务器处理速度的影响
            conn.setConnectTimeout(15000);
            conn.setReadTimeout(60000);

            // 设定请求头参数的方式:如指定接收json数据   服务端的key值为content-type
//            conn.setRequestProperty("Accept", "application/json");

            // 发送请求
            conn.connect();

            if (conn.getResponseCode() != 200) {
                // TODO 此处应该增加异常处理
                return "error code";
            }

            is = conn.getInputStream();
            br = new BufferedReader(new InputStreamReader(is, "UTF-8"));
            String line;
            // 逐行读取  不为空就继续
            while ((line = br.readLine()) != null) {
                result.append(line);
                System.out.print(line);
            }


        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                if (br != null) br.close();
                if (is != null) is.close();

            } catch (Exception e) {
                e.printStackTrace();
            }
        }

        return result.toString();
    }

Part 4

(五) 使用Jsoup解析html格式数据

1、Jsoup 是html的解析器,可以解析html文本和直接解析URL地址。本质上是通过DOM、CSS以及类似jQuery的方法来取出和操作数据。

        Document document = Jsoup.parse(htmlStr);
        System.out.println(document);

        //通过标签名找到元素
        Elements elements = document.getElementsByTag("p");
        System.out.println(elements);

        document.getElementsById    //通过id找到元素
        Element element = document.select("a[href]");  
        //还支持使用正则表达式查找元素

2、提供不同数据源的切换查询

1)增加了controller方法

    @GetMapping("/list/{id}")
    public String listById(Model model, @PathVariable String id) {
        List list = dataService.listById(Integer.parseInt(id));
        model.addAttribute("dataList", list);
        return "list";
    }

@PathVariavle 将接收到的地址数据,映射到方法的参数中

2)完善service

    @Override
    public List listById(int id) {
        if (id == 2) {
            return JsoupHandler.getData();
        }
        return list();
    }
  1. 处理数据的方法

public static String urlStr = "https://ncov.dxy.cn/ncovh5/view/pneumonia?" +
            "scene=2&from=singlemessage&isappinstalled=0";

    public static ArrayList getData() {

        ArrayList result = new ArrayList<>();
        try {
            Document doc = Jsoup.connect(urlStr).get();
//            Elements scripts = doc.select("script");
            // 找到指定的标签数据
            Element oneScript = doc.getElementById("getAreaStat");

            String data = oneScript.data();
            // 字符串截取出json格式的数据
            String subData = data.substring(data.indexOf("["),
                    data.lastIndexOf("]") + 1);

//            System.out.println(subData);

            Gson gson = new Gson();
            ArrayList list = gson.fromJson(subData, ArrayList.class);

            for (int i = 0; i < list.size(); i++) {
                Map map = (Map) list.get(i);
                String name = (String) map.get("provinceName");
                double nowConfirm = (Double) map.get("currentConfirmedCount");
                double confirm = (Double) map.get("confirmedCount");
                double heal = (Double) map.get("curedCount");
                double dead = (Double) map.get("deadCount");

                DataBean dataBean = new DataBean(name, (int) nowConfirm, (int) confirm
                        , (int) heal, (int) dead);
                result.add(dataBean);
            }

        } catch (Exception e) {
            e.printStackTrace();
        }

        return result;
    }

(六) 增加数据存储逻辑

1、引入相关的依赖

        
            org.mybatis.spring.boot
            mybatis-spring-boot-starter
            2.1.0
        

        
            mysql
            mysql-connector-java
            runtime
        

        
            com.baomidou
            mybatis-plus-boot-starter
            3.2.0
        

2、配置数据库

spring.datasource.url=jdbc:mysql://localhost:3366/epidemic?serverTimezone=UTC&useUnicode=true&characterEncoding=utf8 //3366是MySQL数据库端口号
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.username=root //MySQL数据库管理用户名
spring.datasource.password=root //MySQL数据库管理用户密码

3、使用mybatis-plus进行增删改查的操作

1) 创建mapper

public interface DataMapper extends BaseMapper {

}
  1. 扫描mapper的注解 在主程序入口类中添加

@MapperScan("com.duing.mapper")
  1. 创建service及其实现类

注意泛型是要处理的实体类

public interface DataService extends IService {

}
// 泛型 分别是mapper 以及 实体类


@Service
public class DataServiceImpl extends ServiceImpl
        implements DataService {
        
}

4)改造实体类databean

此时要满足,存在无参构造器以及可被序列化 同时指定具体映射的表名 通过@TableName

@Data @AllArgsConstructor
@NoArgsConstructor
@TableName("illness")
public class DataBean implements Serializable {

    private String area;
    private int nowConfirm;
    private int confirm;
    private int heal;
    private int dead;
}

4、初始化数据存储的逻辑

@PostConstruct 修饰的方法,在服务器加载Servlet时运行,而且只执行一次

改造逻辑:首先将DataHandler声明为组件 @Component

    @Autowired
    private DataService dataService;
   
    @PostConstruct
    public void saveData() {
        try {
            List dataBeans = getData();
            // 先将数据清空  然后存储数据
            dataService.remove(null);
            dataService.saveBatch(dataBeans);

        } catch (Exception e) {
            e.printStackTrace();
        }

    }

@Scheduled

使用前需要在主程序入口类上打开开关

@EnableScheduling

在方法上使用注解的参数

1) @Scheduled(fixedRate = 10000) 指定频率的执行任务 从方法执行开始就计时 假设方法执行5s 那么第一次执行开始过了10s后,开始第二次执行

2) @Scheduled(fixedDelay = 10000) 指定间隔的执行任务 从方法执行完成开始计时 假设方法执行5s 那么第一次执行完成过了10s后,开始第二次执行

3) cron表达式 —— 计划执行的表达式 把六个位置用空格分隔,指代不同单位的时间,执行的规律 秒、分钟、小时、日期、月份、星期、(年,可选)

    // 配置定时执行的注解  支持cron表达式  
    // 每分钟执行一次  更改可参考cron表达式生成网站
    @Scheduled(cron = "0 0/1 * * * ?")
    public void updateData() {
        System.out.println("更新数据");
        saveData();
    }

Part 5

【展示数据Echarts】

是由百度前端技术部开发,基于js的数据可视化图表库。

https://echarts.apache.org/examples/zh/index.html#chart-type-line

基于SpringBoot的疫情信息统计平台【完整项目源码】_第5张图片

分析图形展示的数据来源,然后请求数据后转化成我们需要的格式,传递给页面,通过Echarts渲染出来。

(一)折线图

基于SpringBoot的疫情信息统计平台【完整项目源码】_第6张图片

1)分析的请求地址

https://view.inews.qq.com/g2/getOnsInfo?name=disease_other

可以获得json格式的数据,数据的key是chinaDayList

基于SpringBoot的疫情信息统计平台【完整项目源码】_第7张图片

2)模拟请求

HttpClient使用,应用最广泛的处理http请求的工具。


    org.apache.httpcomponents
    httpclient
    4.5.12
public static String doGet(String urlStr) {

        // 提供了 闭合的httpclient对象
        CloseableHttpClient httpClient = null;
        // 也提供了  闭合的响应对象
        CloseableHttpResponse response = null;

        String result = null;

        try {
            // 使用默认创建方式
            httpClient = HttpClients.createDefault();
            // 创建一个get请求  传入url
            HttpGet httpGet = new HttpGet(urlStr);
            // 设置请求头的方式
            httpGet.addHeader("Accept", "application/json");

            // 设置请求参数  连接时间、数据读取时间(socketTimeOut)等  单位是ms
            //   ConnectionRequestTimeout  指从共享连接池中取出连接的超时时间
            RequestConfig requestConfig = RequestConfig.custom()
                    .setConnectTimeout(35000)
                    .setConnectionRequestTimeout(35000)
                    .setSocketTimeout(60000)
                    .build();

            // 设置配置参数
            httpGet.setConfig(requestConfig);
            // 执行请求
            response = httpClient.execute(httpGet);
            // 从返回对象中获取返回数据
            HttpEntity entity = response.getEntity();

            result = EntityUtils.toString(entity);

        }catch (Exception e){
            e.printStackTrace();
        }

        return result;
    }
  1. 解析出数据

public class GraphHandler {

    public static String urlStr = "https://view.inews.qq.com/g2/getOnsInfo?name=disease_other";

    public static List getGraphData() {
        List result = new ArrayList<>();

        String str = HttpClientUtil.doGet(urlStr);

        Gson gson = new Gson();
        Map map = gson.fromJson(str, Map.class);

        String subStr = (String) map.get("data");
        Map subMap = gson.fromJson(subStr, Map.class);

        ArrayList list = (ArrayList) subMap.get("chinaDayList");

        for (int i = 0; i < list.size(); i++) {
            Map tmp = (Map)list.get(i);

            String date = (String)tmp.get("date");
            double nowConfirm = (Double)tmp.get("nowConfirm");
            GraphBean graphBean = new GraphBean(date,(int)nowConfirm);
            result.add(graphBean);
        }

        return result;
    }

}

数据结构

@Data
@AllArgsConstructor
public class GraphBean {

    private String date;
    private int nowConfirm;
}

4)返回给页面渲染

    @GetMapping("/graph")
    public String graph(Model model) {
        List list = GraphHandler.getGraphData();
        //  进一步改造数据格式
        //  因为前端需要的数据是  x轴所有数据的数组和y轴所有数据的数组

        ArrayList dateList = new ArrayList<>();
        ArrayList nowConfirmList = new ArrayList<>();

        for (int i = 0; i < list.size(); i++) {
            GraphBean graphBean = list.get(i);
            dateList.add(graphBean.getDate());
            nowConfirmList.add(graphBean.getNowConfirm());
        }

        model.addAttribute("dateList", new Gson().toJson(dateList));
        model.addAttribute("nowConfirmList", new Gson().toJson(nowConfirmList));
        return "graph";
    }



    
    Title
    





Echars教程地址:[教程地址](https://echarts.apache.org/zh/tutorial.html#5 分钟上手 ECharts)

准备dom -> 通过js渲染数据 -> 使用[[${ sth }]] 接收服务端数据
-> 使用JSON.parse()解析json字符串 -> 获得渲染结果

Part 6

(二)折线图2

基于SpringBoot的疫情信息统计平台【完整项目源码】_第8张图片

相关逻辑在 GraphAddBean对应的代码中

处理数据 -> 转化格式 -> 返回数据给echarts渲染

GraphHandler DataController *.html

1)GraphHandler

public static List getGraphAddData(String str) {

        List result = new ArrayList<>();

        Gson gson = new Gson();
        Map map = gson.fromJson(str, Map.class);

        String subStr = (String) map.get("data");
        Map subMap = gson.fromJson(subStr, Map.class);

        ArrayList list = (ArrayList) subMap.get("chinaDayAddList");

        for (int i = 0; i < list.size(); i++) {
            Map tmp = (Map) list.get(i);
            String date = (String) tmp.get("date");
            double addConfirm = (Double) tmp.get("confirm");
            double addSuspect = (Double) tmp.get("suspect");

            GraphAddBean graphAddBean = new GraphAddBean(date,
                    (int) addConfirm, (int) addSuspect);
            result.add(graphAddBean);
        }

        return result;
    }

2)DataController

 @GetMapping("/graphAdd")
    public String graphAdd(Model model) {
        List list = GraphHandler.getGraphAddData();

        ArrayList dateList = new ArrayList<>();
        ArrayList addConfirmList = new ArrayList<>();
        ArrayList addSuspectList = new ArrayList<>();

        for (int i = 0; i < list.size(); i++) {
            GraphAddBean graphAddBean = list.get(i);
            dateList.add(graphAddBean.getDate());
            addConfirmList.add(graphAddBean.getAddConfirm());
            addSuspectList.add(graphAddBean.getAddSuspect());
        }

        model.addAttribute("dateList", new Gson().toJson(dateList));
        model.addAttribute("addConfirmList", new Gson().toJson(addConfirmList));
        model.addAttribute("addSuspectList", new Gson().toJson(addSuspectList));
        return "graphAdd";
    }

3)HTML

增加折线时,主要在legend和series中增加对应元素




    
    Title
    





(三)柱状图

基于SpringBoot的疫情信息统计平台【完整项目源码】_第9张图片

先分析数据的来源 -> 经过对数据的处理和计算 -> 发送给前端组件进行渲染

对应在GraphColumnarBean相关的逻辑中

特别之处在于 拿到数据之后需要排序

@Data
@AllArgsConstructor
public class GraphColumnarBean implements Comparable {

    private String area;
    private int fromAbroad;

    @Override
    public int compareTo(GraphColumnarBean o) {
        return o.getFromAbroad() - this.getFromAbroad();
    }
}

排序之后将前十的数据返回

    @GetMapping("/graphColumnar")
    public String graphColumnar(Model model) {
        List list = GraphHandler.getGraphColumnarData();
        Collections.sort(list);

        ArrayList nameList = new ArrayList<>();
        ArrayList fromAbroadList = new ArrayList<>();

        for (int i = 0; i < 10; i++) {
            GraphColumnarBean bean = list.get(i);
            nameList.add(bean.getArea());
            fromAbroadList.add(bean.getFromAbroad());
        }

        model.addAttribute("nameList", new Gson().toJson(nameList));
        model.addAttribute("fromAbroadList", new Gson().toJson(fromAbroadList));
        return "graphColumnar";
    }

柱状图的数据示例

// 指定图表的配置项和数据
    var option = {
        title: {  // 标题组件
            text: '境外输入省市TOP10'
        },
        tooltip: {  // 提示框组件
            trigger: 'axis'
        },
        xAxis: {
            // 转化为json对象
            data: JSON.parse(nameStr)
        },
        yAxis: {
            type: 'value'
        },
        series: [
            {
                name: '境外输入',
                type: 'bar',
                barWidth: '60%',
                data: JSON.parse(fromAbroadStr)
            }
        ]
    };

(四)饼状图

基于SpringBoot的疫情信息统计平台【完整项目源码】_第10张图片

对应在GraphPieBean相关的逻辑中

graphPie.html

    // 指定图表的配置项和数据
    var option = {
        title: {  // 标题组件
            text: '全国现有确诊构成'
        },
        tooltip: {  // 提示框组件
            trigger: 'axis'
        },
        series: [
            {
                type: 'pie',
                radius: '55%',
                center: ['50%', '60%'],
                data: JSON.parse(str)
            }
        ]
    };

Part 7

(五) 中国地图

基于SpringBoot的疫情信息统计平台【完整项目源码】_第11张图片

引入新的js 【china.js】 数据来源已存储到表格中

map.html




    
    Title

    
    




【国际化】

是对thymeleaf中消息表达式的一种应用, #{} , 提供的是对配置文件中信息的读取。

(一) 使用浏览器识别语种

1) 在resources目录下,创建mg文件夹,再继续创建 *.properties文件。

设置其中的key和value (注意:此时的编码格式需要在idea的设置中确认)

list.title = 疫情最新动态
list.h2 = 国内疫情情况如下
list.table.name1 = 地区
list.table.name2 = 现有确诊
list.table.name3 = 累计确诊
list.table.name4 = 治愈
list.table.name5 = 死亡
  1. 创建其他语种对应的配置文件,如 *_en_US.properties 和 *_zh_CN.properties

3)在html中更改对key值的引用方式,使用消息表达式

4)让spring找到国际化文件对应的位置,在application.properties中

spring.messages.basename = mg.list
  1. 验证方式 通过更改浏览器中 【设置】 - 【高级】 - 【语言】 - 【找到英语(美国)】,可以通过置顶,切换中英文显示。

本质原因是,因为请求的请求头中,会设置不同的Accept-Language的值。

(二)自定义切换语种

使用spring提供的国际化使用类 LocaleResolver

1) 页面中增加按钮

    
  1. 自定义LocaleResolver类的实现

public class MyLocaleResolver implements LocaleResolver {

    @Override
    public Locale resolveLocale(HttpServletRequest httpServletRequest) {
        String lan = httpServletRequest.getParameter("lan");
        Locale locale = Locale.getDefault();
        if (!StringUtils.isEmpty(lan)) {
            String[] split = lan.split("_");
            locale = new Locale(split[0], split[1]);
        }
        return locale;
    }

    @Override
    public void setLocale(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Locale locale) {

    }
}

3)注入spring

@Configuration
public class MyConfig {

    @Bean
    public LocaleResolver localeResolver() {
        return new MyLocaleResolver();
    }
}
  1. 验证 此时切换按钮时,可以看到不同语种的显示

    基于SpringBoot的疫情信息统计平台【完整项目源码】_第12张图片

点赞评论+关注再获取源码也不晚!

源码地址仓库:文件 · master · JavaNoteany / Epidemics Over · GitCode

你可能感兴趣的:(项目,爬虫,疫情,springboot,项目源码)