新冠疫情防控指挥作战平台项目

第一章 项目介绍

1.1 项目背景

新冠疫情防控指挥作战平台项目实现了疫情态势、基层防控、物资保障、复工复产等多个专题板块,包括新冠疫情防控指挥大屏子系统和新冠疫情防控指挥平台后台管理子系统。

通过新冠疫情防控指挥作战平台的建设及实施,使得从局部作战到中心指挥,让战“疫”指挥官对疫情防控心中有“数”,科学决策,下好疫情防控、救治、复工复产“一盘棋”,更高效地帮助防疫指挥部开展统筹、协调、决策工作,尽快打赢疫情防控战役。

项目学习目标:

1.掌握使用Java爬虫爬取疫情相关数据

2.掌握使用Java代码生成部分疫情相关数据

3.掌握Kafka基本使用并接收实时疫情数据

4.掌握Spark整合Kafka,消费、处理并分析数据

5.掌握将分析结果数据存入MySQL

6.掌握搭建SpringBoot+Echarts项目对数据结果进行可视化

1.2 项目架构

image-20220314151813351

1.3 项目截图

image-20221220150351637

第二章 数据爬取和生成

2.1 数据清单

以图表的方式展示境外输入趋势、疫情新增趋势(新增确诊人数、新增疑似病例)、累计确诊趋势(累计确诊人数、累计疑似人数)、治愈出院趋势(出院人数、住院人数)、患者类型趋势(普通型、重型、危重)、患者男女比例、患者年龄分布等。横轴为日期、纵轴为人数

从丁香园网站上爬取:https://ncov.dxy.cn/ncovh5/view/pneumonia

2.2 前置知识

2.2.1 网络爬虫介绍

网络爬虫的基本概念

爬虫引入:

随着互联网的迅速发展,网络资源越来越丰富,信息需求者如何从网络中抽取信息变得至关重要。目前,有效的获取网络数据资源的重要方式,便是网络爬虫技术。简单的理解,比如您对百度贴吧的一个帖子内容特别感兴趣,而帖子的回复却有1000多页,这时采用逐条复制的方法便不可行。而采用网络爬虫便可以很轻松地采集到该帖子下的所有内容。

网络爬虫技术最广泛的应用是在搜索引擎中,如百度、Google、Bing 等,它完成了搜索过程中的最关键的步骤,即网页内容的抓取。下图为简单搜索引擎原理图。

img

什么是网络爬虫:

网络爬虫(Web Crawler),又称为网络蜘蛛(Web Spider)或 Web 信息采集器,是一种按照一定规则,自动抓取或下载网络信息的计算机程序或自动化脚本。

狭义上理解:

利用标准的 HTTP 协议,根据网络超链接(如https://www.baidu.com/)和 Web 文档检索的方法(如深度优先)遍历万维网信息空间的软件程序。

功能上理解:

确定待爬的 URL 队列,获取每个 URL 对应的网页内容(如 HTML/JSON),解析网页内容,并存储对应的数据。

本质:

网络爬虫实际上是通过模拟浏览器的方式获取服务器数据

img

网络爬虫的作用

我们初步认识了网络爬虫,但是为什么要学习网络爬虫呢?只有清晰地知道我们的学习目的,才能够更好地学习这一项知识。在此,总结了如下学习爬虫的原因:

  1. 可以实现搜索引擎

我们学会了爬虫编写之后,就可以利用爬虫自动地采集互联网中的信息,采集回来后进行相应的存储或处理,在需要检索某些信息的时候,只需在采集回来的信息中进行检索,即实现了私人的搜索引擎。

  1. 大数据时代,可以让我们获取更多的数据源。

在进行大数据分析或者进行数据挖掘的时候,需要有数据源进行分析。我们可以从某些提供数据统计的网站获得,也可以从某些文献或内部资料中获得,但是这些获得数据的方式,有时很难满足我们对数据的需求,而手动从互联网中去寻找这些数据,则耗费的精力过大。此时就可以利用爬虫技术,自动地从互联网中获取我们感兴趣的数据内容,并将这些数据内容爬取回来,作为我们的数据源,再进行更深层次的数据分析,并获得更多有价值的信息。如研究产品个性化推荐、文本挖掘、用户行为模式挖掘等。

  1. 可以更好地进行搜索引擎优化(SEO)。

对于很多SEO从业者来说,为了更好的完成工作,那么就必须要对搜索引擎的工作原理非常清楚,同时也需要掌握搜索引擎爬虫的工作原理。而学习爬虫,可以更深层次地理解搜索引擎爬虫的工作原理,这样在进行搜索引擎优化时,才能知己知彼,百战不殆。

  1. 有利于就业。

从就业来说,爬虫工程师方向是不错的选择之一,因为目前爬虫工程师的需求越来越大,而能够胜任这方面岗位的人员较少,所以属于一个比较紧缺的职业方向,并且随着大数据时代和人工智能的来临,爬虫技术的应用将越来越广泛,在未来会拥有很好的发展空间。

网络爬虫的分类

网络爬虫按照系统架构和实现技术,大致可以分为以下几种类型:

通用网络爬虫(General Purpose Web Crawler)、聚焦网络爬虫(Focused Web Crawler)、增量式网络爬虫(Incremental Web Crawler)、深层网络爬虫(Deep Web Crawler)。

实际的网络爬虫系统通常是几种爬虫技术相结合实现的。

  1. 通用网络爬虫:

爬行对象从一些种子 URL 扩充到整个 Web,主要为门户站点搜索引擎和大型 Web 服务提供商采集数据。通用网络爬虫的爬取范围和数量巨大,对于爬行速度和存储空间要求较高,对于爬行页面的顺序要求较低,通常采用并行工作方式,有较强的应用价值。

  1. 聚焦网络爬虫

又称为主题网络爬虫:是指选择性地爬行那些与预先定义好的主题相关的页面。和通用爬虫相比,聚焦爬虫只需要爬行与主题相关的页面,极大地节省了硬件和网络资源,保存的页面也由于数量少而更新快,可以很好地满足一些特定人群对特定领域信息的需求。通常在设计聚焦网络爬虫时,需要加入链接和内容筛选模块。

一个常见的案例是基于关键字获取符合用户需求的数据,如下图所示:

img

  1. 增量网络爬虫

对已下载网页采取增量式更新和只爬行新产生的或者已经发生变化网页的爬虫,它能够在一定程度上保证所爬行的页面是尽可能新的页面,历史已经采集过的页面不重复采集。增量网络爬虫避免了重复采集数据,可以减小时间和空间上的耗费。通常在设计网络爬虫时,需要在数据库中,加入时间戳,基于时间戳上的先后,判断程序是否继续执行。

常见的案例有:论坛帖子评论数据的采集(如下图所示论坛的帖子,它包含400多页,每次启动爬虫时,只需爬取最近几天用户所发的帖子);天气数据的采集;新闻数据的采集;股票数据的采集等。

img

  1. Deep Web 爬虫

指大部分内容不能通过静态链接获取,只有用户提交一些表单信息才能获取的 Web 页面。例如,需要模拟登陆的网络爬虫便属于这类网络爬虫。

另外,还有一些需要用户提交关键词才能获取的内容,如京东淘宝提交关键字、价格区间获取产品的相关信息。

img

网络爬虫的流程

网络爬虫基本流程可用下图描述:

img

具体流程为:

  1. 需求者选取一部分种子 URL(或初始 URL),将其放入待爬取的队列中。如在 Java 网络爬虫中,可以放入 LinkedList 或 List 中。

  2. 判断 URL 队列是否为空,如果为空则结束程序的执行,否则执行第三步骤。

  3. 从待爬取的 URL 队列中取出待爬的一个 URL,获取 URL 对应的网页内容。在此步骤需要使用响应的状态码(如200,403等)判断是否获取数据,如响应成功则执行解析操作;如响应不成功,则将其重新放入待爬取队列(注意这里需要移除无效 URL)。

  4. 针对已经响应成功后获取到的数据,执行页面解析操作。此步骤根据用户需求获取网页内容里的部分数据,如汽车论坛帖子的标题、发表的时间等。

  5. 针对3步骤已解析的数据,将其进行存储。

网络爬虫的爬行策略

一般的网络爬虫的爬行策略分为两种:深度优先搜索(Depth-First Search)策略、广度优先搜索(Breadth-First Search)策略。

  1. 深度优先搜索策略

从根节点的 URL 开始,根据优先级向下遍历该根节点对应的子节点。当访问到某一子节点 URL 时,以该子节点为入口,继续向下层遍历,直到没有新的子节点可以继续访问为止。接着使用回溯的方法,找到没有被访问到的节点,以类似的方式进行搜索。下图给出了理解深度优先搜索的一个简单案例:

img

  1. 广度优先搜索策略

也称为宽度优先,是另外一种非常有效的搜索技术,这种方法按照层进行遍历页面。下图可帮助理解广度优先搜索的遍历方式:

img

基于广度优先的爬虫是最简单的爬取网站页面的方法,也是目前使用较为广泛的方法。

2.2.2 Java爬虫入门

Java 网络爬虫具有很好的扩展性可伸缩性,其是目前搜索引擎开发的重要组成部分。例如,著名的网络爬虫工具 Nutch 便是采用 Java 开发

环境准备

pom.xml

  <dependency>
            <groupId>org.apache.httpcomponents</groupId>
            <artifactId>httpclient</artifactId>
            <version>4.5.3</version>
        </dependency>
        <dependency>
            <groupId>org.jsoup</groupId>
            <artifactId>jsoup</artifactId>
            <version>1.12.1</version>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
        </dependency>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>3.7</version>
        </dependency>
        <dependency>
            <groupId>commons-io</groupId>
            <artifactId>commons-io</artifactId>
            <version>2.6</version>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-log4j12</artifactId>
            <version>1.7.25</version>
        </dependency>

log4j.

log4j.rootLogger=DEBUG,A1
log4j.logger.cn.itcast = DEBUG

log4j.appender.A1=org.apache.log4j.ConsoleAppender
log4j.appender.A1.layout=org.apache.log4j.PatternLayout
log4j.appender.A1.layout.ConversionPattern=%-d{yyyy-MM-dd HH:mm:ss,SSS} [%t] [%c]-[%p] %m%n

URLConnection

URLConnection 是 JDK 自带的一个抽象类,其代表应用程序和 URL 之间的通信链接。在网络爬虫中,我们可以使用 URLConnection 请求一个 URL 地址,然后获取流信息,通过对流信息的操作,可获得请求到的实体内容

代码演示:

import org.junit.Test;

import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;

/**
 * 演示原生JDK-API-URLConnection发送get和post请求
 */
public class JDKAPITest {
    @Test
    public void testGet() throws Exception{
        //1.确定首页的URL
        URL url = new URL("http://www.itcast.cn/?username=zhangsan");
        //2.通过url对象获取远程连接
        HttpURLConnection urlConnection = (HttpURLConnection)url.openConnection();
        //3.设置请求方式  请求参数  请求头
        urlConnection.setRequestMethod("GET");  //设置请求方式的时候一定要大写, 默认的请求方式是GET
        urlConnection.setRequestProperty("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.108 Safari/537.36");
        urlConnection.setConnectTimeout(30000); //连接超时 单位毫秒
        urlConnection.setReadTimeout(30000);   //读取超时 单位毫秒
        //4.获取数据
        InputStream in = urlConnection.getInputStream();
        BufferedReader reader = new BufferedReader(new InputStreamReader(in));
        String line;
        String html = "";
        while ((line = reader.readLine()) != null) {
            html +=  line + "\n";
        }
        System.out.println(html);
        in.close();
        reader.close();
    }

    @Test
    public void testPost() throws Exception{
        //1.确定首页的URL
        URL url = new URL("http://www.itcast.cn");
        //2.获取远程连接
        HttpURLConnection urlConnection =(HttpURLConnection) url.openConnection();
        //3.设置请求方式  请求参数  请求头
        urlConnection.setRequestMethod("POST");
        urlConnection.setDoOutput(true); // 原生jdk默认关闭了输出流
        urlConnection.setRequestProperty("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.108 Safari/537.36");
        urlConnection.setConnectTimeout(30000); //连接超时 单位毫秒
        urlConnection.setReadTimeout(30000);   //读取超时 单位毫秒
        OutputStream out = urlConnection.getOutputStream();
        out.write("username=zhangsan&password=123".getBytes());
        //4.获取数据
        InputStream in = urlConnection.getInputStream();
        BufferedReader reader = new BufferedReader(new InputStreamReader(in));
        String line;
        String html = "";
        while ((line = reader.readLine()) != null) {
            html +=  line + "\n";
        }
        System.out.println(html);
        in.close();
        reader.close();
    }
}

HttpClient

HttpClient 是 Apache Jakarta Common 下的子项目,用来提供高效的、最新的、功能丰富的支持 HTTP 协议的客户端编程工具包,并且它支持 HTTP 协议最新的版本和建议。其相比于传统 JDK 自带的 URLConnection,增加了易用性和灵活性。其功能主要是用来向服务器发送请求,并返回相关资源。在网络爬虫实战中,经常使用 HttpClient 获取网页内容,使用 jsoup 解析网页内容

代码演示:

import org.apache.http.NameValuePair;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.util.EntityUtils;
import org.junit.Test;

import java.util.ArrayList;
import java.util.List;


public class HttpClientTest {
    /**
     * 测试HttpClient发送Get请求
     */
    @Test
    public void testGet() throws Exception {
        //0.创建配置
        RequestConfig requestConfig = RequestConfig.custom()
                .setSocketTimeout(10000)//设置连接的超时时间
                .setConnectTimeout(10000)//设置创建连接的超时时间
                .setConnectionRequestTimeout(10000)//设置请求超时时间
                //.setProxy(new HttpHost("123.207.57.145",  1080, null))//添加代理
                .build();

        //1.创建HttpClient对象
        //HttpClient httpClient = new DefaultHttpClient();//不用
        //CloseableHttpClient httpClient = HttpClients.createDefault();//简单API
        CloseableHttpClient httpClient = HttpClients.custom().setDefaultRequestConfig(requestConfig).build();//常用

        //2.创建HttpGet请求
        String uri = "http://yun.itheima.com/search?so=java";
        HttpGet httpGet = new HttpGet(uri);
        //或者单独给httpGet设置
        //httpGet.setConfig(requestConfig);

        //3.设置请求头
        httpGet.setHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.108 Safari/537.36");

        CloseableHttpResponse response = null;
        try {
            //4.使用HttpClient发起请求
            response = httpClient.execute(httpGet);
            //5.判断响应状态码是否为200
            if (response.getStatusLine().getStatusCode() == 200) {
                //6.获取响应数据
                String content = EntityUtils.toString(response.getEntity(), "UTF-8");
                System.out.println(content);
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            //6.关闭资源
            response.close();
            httpClient.close();
        }
    }

    /**
     * 测试HttpClient发送Post请求
     */
    @Test
    public void testPost() throws Exception {
        //1.创建HttpClient对象
        CloseableHttpClient httpClient = HttpClients.createDefault();
        //2.创建HttpPost请求
        HttpPost httpPost = new HttpPost("http://yun.itheima.com/search");
        //3.准备参数
        List<NameValuePair> params = new ArrayList();
        params.add(new BasicNameValuePair("so", "java"));
        UrlEncodedFormEntity formEntity = new UrlEncodedFormEntity(params, "UTF-8");
        //4.设置参数
        httpPost.setEntity(formEntity);
        //5.发起请求
        CloseableHttpResponse response = null;
        try {
            response = httpClient.execute(httpPost);
            if (response.getStatusLine().getStatusCode() == 200) {
                //6.获取响应
                String content = EntityUtils.toString(response.getEntity(), "UTF-8");
                System.out.println(content);
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            //7.关闭资源
            response.close();
            httpClient.close();
        }
    }

    /**
     * 测试HttpClient连接池
     */
    @Test
    public void testPool() throws Exception {
        PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager();
        //设置最大连接数
        cm.setMaxTotal(200);
        //设置每个主机的并发数
        cm.setDefaultMaxPerRoute(20);
        doGet(cm);
        doGet(cm);
    }

    private static void doGet(PoolingHttpClientConnectionManager cm) throws Exception {
        CloseableHttpClient httpClient = HttpClients.custom().setConnectionManager(cm).build();
        //在下一行加断点
        HttpGet httpGet = new HttpGet("http://www.itcast.cn/");
        CloseableHttpResponse response = null;
        try {
            response = httpClient.execute(httpGet);
            if (response.getStatusLine().getStatusCode() == 200) {
                String content = EntityUtils.toString(response.getEntity(), "UTF-8");
                System.out.println(content.length());
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            //释放连接
            response.close();
            //不能关闭HttpClient
            //httpClient.close();
        }
    }
}

JSoup

https://www.open-open.com/jsoup/

我们抓取到页面之后,还需要对页面进行解析。可以使用字符串处理工具解析页面,也可以使用正则表达式,但是这些方法都会带来很大的开发成本,所以我们需要使用一款专门解析html页面的技术。

jsoup 是一款基于 Java 语言的 HTML 请求及解析器,可直接请求某个 URL 地址、解析 HTML 文本内容。它提供了一套非常省力的 API,可通过 DOM、CSS 以及类似于 jQuery 的操作方法来取出和操作数据。

jsoup的主要功能如下:

  1. 从一个URL,文件或字符串中解析HTML;

  2. 使用DOM或CSS选择器来查找、取出数据;

  3. 可操作HTML元素、属性、文本;

注意:

虽然使用Jsoup可以替代HttpClient直接发起请求解析数据,但是往往不会这样用,因为实际的开发过程中,需要使用到多线程,连接池,代理等等方式,而jsoup对这些的支持并不是很好,所以我们一般把jsoup仅仅作为Html解析工具使用

jsoup 一个重要用途是解析 HTML 文件,在开始用之前,必须弄清 jsoup 中的 Node、Element、Document 的相关概念及区别,防止因概念混淆而导致乱用错用。

img

  1. Document(文档):指整个 HTML 文档的源码内容

  2. Node(节点):HTML 中所包含的内容都可以看成一个节点。节点有很多种类型:属性节点(Attribute)、注释节点(Note)、文本节点(Text)、元素节点(Element)等。解析 HTML 内容的过程,其实就是对节点操作的过程。

  3. Element(元素):元素是节点的子集,所以一个元素也是一个节点。

代码演示:

import org.apache.commons.io.FileUtils;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;
import org.junit.Test;

import java.io.File;
import java.net.URL;

/**
 * Author itcast
 * Date 2020/5/4 14:49
 * Desc
 */
public class JsopTest {
    /**
     * 测试JSoup-获取Document
     */
    @Test
    public void testDocument() throws Exception {
        Document doc1 = Jsoup.connect("http://www.itcast.cn/").get();
        Document doc2 = Jsoup.parse(new URL("http://www.itcast.cn/"), 1000);

        String html = FileUtils.readFileToString(new File("jsoup.html"), "UTF-8");
        Document doc3 = Jsoup.parse(html);

        System.out.println(doc1);
        System.out.println(doc2);
        System.out.println(doc3);
    }

    /**
     * 测试JSoup-解析html
     */
    @Test
    public void testJsoupHtml() throws Exception {
        Document doc = Jsoup.parse(new File("jsoup.html"), "UTF-8");

        //**使用dom方式遍历文档
        //1. 根据id查询元素getElementById
        Element element = doc.getElementById("city_bj");
        System.out.println(element.text());
        //2. 根据标签获取元素getElementsByTag
        element = doc.getElementsByTag("title").first();
        System.out.println(element.text());
        //3. 根据class获取元素getElementsByClass
        element = doc.getElementsByClass("s_name").last();
        System.out.println(element.text());
        //4. 根据属性获取元素getElementsByAttribute
        element = doc.getElementsByAttribute("abc").first();
        System.out.println(element.text());
        element = doc.getElementsByAttributeValue("class", "city_con").first();
        System.out.println(element.text());


        //**元素中数据获取
        //1. 从元素中获取id
        String str = element.id();
        System.out.println(str);
        //2. 从元素中获取className
        str = element.className();
        System.out.println(str);
        //3. 从元素中获取属性的值attr
        str = element.attr("id");
        System.out.println(str);
        //4. 从元素中获取所有属性attributes
        str = element.attributes().toString();
        System.out.println(str);
        //5. 从元素中获取文本内容text
        str = element.text();
        System.out.println(str);

        //**使用选择器语法查找元素
        //jsoup elements对象支持类似于CSS (或jquery)的选择器语法,来实现非常强大和灵活的查找功能。
        //select方法在Document/Element/Elements对象中都可以使用。可实现指定元素的过滤,或者链式选择访问。
        //1. tagname: 通过标签查找元素,比如:span
        Elements span = doc.select("span");
        for (Element e : span) {
            System.out.println(e.text());
        }
        //2. #id: 通过ID查找元素,比如:#city_bjj
        str = doc.select("#city_bj").text();
        System.out.println(str);
        //3. .class: 通过class名称查找元素,比如:.class_a
        str = doc.select(".class_a").text();
        System.out.println(str);
        //4. [attribute]: 利用属性查找元素,比如:[abc]
        str = doc.select("[abc]").text();
        System.out.println(str);
        //5. [attr=value]: 利用属性值来查找元素,比如:[class=s_name]
        str = doc.select("[class=s_name]").text();
        System.out.println(str);


        //**Selector选择器组合使用
        //1. el#id: 元素+ID,比如: h3#city_bj
        str = doc.select("h3#city_bj").text();
        System.out.println(str);
        //2. el.class: 元素+class,比如: li.class_a
        str = doc.select("li.class_a").text();
        System.out.println(str);
        //3. el[attr]: 元素+属性名,比如: span[abc]
        str = doc.select("span[abc]").text();
        System.out.println(str);
        //4. 任意组合,比如:span[abc].s_name
        str = doc.select("span[abc].s_name").text();
        System.out.println(str);
        //5. ancestor child: 查找某个元素下子元素,比如:.city_con li 查找"city_con"下的所有li
        str = doc.select(".city_con li").text();
        System.out.println(str);
        //6. parent > child: 查找某个父元素下的直接子元素,
        //比如:.city_con > ul > li 查找city_con第一级(直接子元素)的ul,再找所有ul下的第一级li
        str = doc.select(".city_con > ul > li").text();
        System.out.println(str);
        //7. parent > * 查找某个父元素下所有直接子元素.city_con > *
        str = doc.select(".city_con > *").text();
        System.out.println(str);
    }
}

补充知识点

正则表达式

https://www.runoob.com/regexp/regexp-syntax.html

https://deerchao.cn/tutorials/regex/regex.htm

HTTP状态码

https://www.runoob.com/http/http-status-codes.html

当浏览者访问一个网页时,浏览者的浏览器会向网页所在服务器发出请求。当浏览器接收并显示网页前,此网页所在的服务器会返回一个包含 HTTP 状态码的信息头(Server Header)用以响应浏览器的请求。在网络爬虫向后台请求一个 URL 地址时,便会返回状态码,该状态码中包含丰富的信息。

例如,

200表示请求成功,成功获取到了后台传的数据(HTML 或 JSON 等);

301资源(网页等)被永久转移到其它 URL;

404请求的资源(网页等)不存在等。以下是 HTTP 状态码的分类。

分类 分类描述
1** 信息,服务器收到请求,需要请求者继续执行操作
2** 成功,操作被成功接收并处理
3** 重定向,需要进一步的操作以完成请求
4** 客户端错误,请求包含语法错误或无法完成请求
5** 服务器错误,服务器在处理请求的过程中发生了错误

HTTP请求头

请求头 说明
Accept 浏览器可以接受的媒体类型(MIME),如 text/html 代表可以接受 HTML 文档, * 代表接受任何类型。
Accept-Encoding 浏览器声明自己接受的编码方法,通常指定压缩方法 ,是否支持压缩,支持什么格式的压缩 。
Accept-Language 浏览器可接受的语言,zh-CN,zh;q=0.9 表示浏览器支持的语言分别是中文和简体中文,优先支持简体中文。
Cache-Control 指定请求和响应遵循的缓存机制。
Connection 表示是否需要持久连接,HTTP 1.1 默认进行持久连接。
Cookie HTTP 请求发送时,会把保存在该请求域名下的所有 Cookie 值一起发送给 Web 服务器。
Host 指定请求的服务器的域名和端口号。
Upgrade-Insecure-Requests 浏览器自动升级请求(HTTP 和 HTTPS 之间)。
User-Agent 用户代理,是一个特殊字符串头,使得服务器能够识别客户端使用的操作系统及版本、CPU 类型、浏览器及版本、浏览器渲染引擎、浏览器语言、浏览器插件等。

代理服务器的使用

image-20220430111116659

代理服务器(Proxy Server)是网上提供转接功能的服务器,一般情况下,我们使用网络浏览器直接连接其他Internet站点取得网络信息。代理服务器是介于客户端和 Web 服务器之间的另一台服务器,基于代理,浏览器不再直接向 Web 服务器取数据,而是向代理服务器发出请求,信号会先送到代理服务器,由代理服务器取回浏览器所需要的信息。简单的可以理解为中介。

在网络爬虫中,使用代理服务器访问站点内容,能够隐藏爬虫的真实 IP 地址,从而防止网络爬虫被封。另外,普通网络爬虫使用固定 IP 请求时,往往需要设置随机休息时间,而通过代理却不需要,从而提高了爬虫的效率。

目前,代理可以来源于提供免费代理地址以及接口的网站(如https://proxy.mimvp.com/freeopen),但这些免费的代理 IP 都是不稳定的。另外,也可通过购买的方式使用代理,其提供的代理 IP 可用率较高,稳定性较强

2.3 疫情数据爬取

2.3.1 环境准备

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <artifactId>crawler</artifactId>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.2.7.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>cn.itcast</groupId>
    <version>0.0.1-SNAPSHOT</version>

    <properties>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
         <dependency>
             <groupId>org.springframework.kafka</groupId>
             <artifactId>spring-kafka</artifactId>
         </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.22</version>
        </dependency>
        <dependency>
            <groupId>org.apache.httpcomponents</groupId>
            <artifactId>httpclient</artifactId>
            <version>4.5.3</version>
        </dependency>
        <dependency>
            <groupId>org.jsoup</groupId>
            <artifactId>jsoup</artifactId>
            <version>1.10.3</version>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
        </dependency>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>3.7</version>
        </dependency>
        <dependency>
            <groupId>commons-io</groupId>
            <artifactId>commons-io</artifactId>
            <version>2.6</version>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-log4j12</artifactId>
            <version>1.7.25</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

application.properties

server.port=9999
#kafka
#服务器地址
kafka.bootstrap.servers=hadoop102:9092,hadoop103:9092,hadoop104:9092
#重试发送消息次数
kafka.retries_config=0
#批量发送的基本单位,默认16384Byte,即16KB
kafka.batch_size_config=4096
#批量发送延迟的上限
kafka.linger_ms_config=100
#buffer内存大小
kafka.buffer_memory_config=40960
#主题
kafka.topic=covid19

2.3.2 工具类

HttpUtils

package cn.itcast.util;

import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.apache.http.util.EntityUtils;

import java.util.ArrayList;
import java.util.List;
import java.util.Random;
// 封装HttpClient工具,方便爬取网页内容
public abstract class HttpUtils {
    private static PoolingHttpClientConnectionManager cm;
    private static List<String> userAgentList = null;

    static {
        cm = new PoolingHttpClientConnectionManager();
        //设置最大连接数
        cm.setMaxTotal(200);
        //设置每个主机的并发数
        cm.setDefaultMaxPerRoute(20);

        userAgentList = new ArrayList<>();
        userAgentList.add("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.132 Safari/537.36");
        userAgentList.add("Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:73.0) Gecko/20100101 Firefox/73.0");
        userAgentList.add("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_3) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.5 Safari/605.1.15");
        userAgentList.add("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36 Edge/16.16299");
        userAgentList.add("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36");
        userAgentList.add("Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:63.0) Gecko/20100101 Firefox/63.0");
    }



    //获取内容
    public static String getHtml(String url) {
        CloseableHttpClient httpClient = HttpClients.custom().setConnectionManager(cm).build();
        HttpGet httpGet = new HttpGet(url);
        int index = new Random().nextInt(userAgentList.size());
        httpGet.setHeader("User-Agent", userAgentList.get(index));
        httpGet.setConfig(getConfig());
        CloseableHttpResponse response = null;
        try {
            response = httpClient.execute(httpGet);
            if (response.getStatusLine().getStatusCode() == 200) {
                String html = "";
                if (response.getEntity() != null) {
                    html = EntityUtils.toString(response.getEntity(), "UTF-8");
                }
                return html;
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                if (response != null) {
                    response.close();
                }
                // httpClient.close();//不能关闭,现在使用的是连接管理器
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        return null;
    }

    //获取请求参数对象
    private static RequestConfig getConfig() {
        RequestConfig config = RequestConfig.custom().setConnectTimeout(1000)
                .setConnectionRequestTimeout(500)
                .setSocketTimeout(10000)
                .build();
        return config;
    }
}

TimeUtils

package cn.itcast.util;

import org.apache.commons.lang3.time.FastDateFormat;
// 时间工具类
public abstract class TimeUtils {
    public static String format(Long timestamp,String pattern){
        return FastDateFormat.getInstance(pattern).format(timestamp);
    }

    public static void main(String[] args) {
        String format = TimeUtils.format(System.currentTimeMillis(), "yyyy-MM-dd");
        System.out.println(format);
    }
}

KafkaProducerConfig

package cn.itcast.util;

import org.apache.kafka.clients.producer.ProducerConfig;
import org.apache.kafka.common.serialization.IntegerSerializer;
import org.apache.kafka.common.serialization.StringSerializer;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.kafka.core.DefaultKafkaProducerFactory;
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.kafka.core.ProducerFactory;

import java.util.HashMap;
import java.util.Map;
// kafkaTemplate配置类
@Configuration // 表示该类是一个配置类
public class KafkaProducerConfig {
    @Value("${kafka.bootstrap.servers}")
    private String bootstrap_servers;
    @Value("${kafka.retries_config}")
    private String retries_config;
    @Value("${kafka.batch_size_config}")
    private String batch_size_config;
    @Value("${kafka.linger_ms_config}")
    private String linger_ms_config;
    @Value("${kafka.buffer_memory_config}")
    private String buffer_memory_config;
    @Value("${kafka.topic}")
    private String topic;

    @Bean //表示方法返回值对象是受Spring所管理的一个Bean
    public KafkaTemplate kafkaTemplate() {
        // 构建工厂需要的配置
        Map<String, Object> configs = new HashMap<>();
        configs.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrap_servers);
        configs.put(ProducerConfig.RETRIES_CONFIG, retries_config);
        configs.put(ProducerConfig.BATCH_SIZE_CONFIG, batch_size_config);
        configs.put(ProducerConfig.LINGER_MS_CONFIG, linger_ms_config);
        configs.put(ProducerConfig.BUFFER_MEMORY_CONFIG, buffer_memory_config);
        configs.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, IntegerSerializer.class);
        configs.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
        // 指定自定义分区
        configs.put(ProducerConfig.PARTITIONER_CLASS_CONFIG, CustomerPartitioner.class);
        // 创建生产者工厂
        ProducerFactory<String, String> producerFactory = new DefaultKafkaProducerFactory(configs);
        // 返回KafkTemplate的对象
        KafkaTemplate kafkaTemplate = new KafkaTemplate(producerFactory);
        //System.out.println("kafkaTemplate"+kafkaTemplate);
        return kafkaTemplate;
    }

}

CustomerPartitioner

package cn.itcast.config;

import org.apache.kafka.clients.producer.Partitioner;
import org.apache.kafka.common.Cluster;
import java.util.Map;

// 自定义分区器指定分区规则(默认是按照key的hash)

public class CustomerPartitioner implements Partitioner {
    //根据参数按照指定的规则进行分区,返回分区编号即可
    @Override
    public int partition(String topic, Object key, byte[] keyBytes, Object value, byte[] valueBytes, Cluster cluster) {
        Integer k  = (Integer)key;
        Integer num = cluster.partitionCountForTopic(topic);
        int partiton = k % num;
        return partiton;
    }

    @Override
    public void close() {

    }

    @Override
    public void configure(Map<String, ?> map) {

    }
}

2.3.3 实体类

CovidBean

package cn.itcast.bean;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;


@Data
@NoArgsConstructor
@AllArgsConstructor
public class CovidBean {
    private String provinceName;//省份名称
    private String provinceShortName;//省份短名
    private String cityName; //城市名
    private Integer currentConfirmedCount;//当前确诊人数
    private Integer confirmedCount;//累记确诊人数
    private Integer suspectedCount;//疑似病例人数
    private Integer curedCount;//治愈人数
    private Integer deadCount;//死亡人数
    private Integer locationId;//位置id
    private Integer pid;// 城市的外键,参照locationId
    private String statisticsData;//每一天的统计数据  json数据链接
    private String cities;//下属城市
    private String datetime;//日期
}

MaterialBean

package cn.itcast.bean;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class MateriaBean {
    private String name;//物资名称
    private String from;//物质来源
    private Integer count;//物资数量
}

2.3.4 入口程序

package cn.itcast;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;

@SpringBootApplication
@EnableScheduling//开启定时任务
public class Covid19ProjectApplication {
    public static void main(String[] args) {
        SpringApplication.run(Covid19ProjectApplication.class, args);
    }
}

2.3.5 数据爬取

kafka集群配置

查看主题:

bin/kafka-topics.sh --list --zookeeper hadoop102:2181

删除主题:

bin/kafka-topics.sh --delete --zookeeper hadoop102:2181 --topic covid19

创建主题:

bin/kafka-topics.sh  --create --zookeeper hadoop102:2181  --replication-factor 2 --partitions 3 --topic covid19

再次查看主题:

bin/kafka-topics.sh --list --zookeeper hadoop102:2181

启动控制台消费者

bin/kafka-console-consumer.sh --bootstrap-server hadoop102:9092 --from-beginning --topic covid19

启动控制台生产者

bin/kafka-console-producer.sh --topic covid19 --broker-list hadoop102:9092

Covid19DataCrawler

package cn.itcast.crawler;

import cn.itcast.bean.CovidBean;
import cn.itcast.util.HttpUtils;
import cn.itcast.util.TimeUtils;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

@Component
public class Covid19DataCrawler {
    @Autowired
    private KafkaTemplate kafkaTemplate;
    //后续需要将该方法改为定时任务,如每天8点定时爬取疫情数据
    //@Scheduled(initialDelay = 1000,fixedDelay = 1000*60*60*24)
    //@Scheduled(cron="0/1 * * * * ?")//每隔1s执行
    //@Scheduled(cron="0 0 8 * * ?")//每天的8点定时执行
    @Scheduled(cron="0/60 * * * * ?")//每隔10s执行
    public void testCrawling() throws Exception {
        System.out.println("每隔60s执行一次");
        String datetime = TimeUtils.format(System.currentTimeMillis(), "yyyy-MM-dd");

        //1.爬取指定页面
        String html = HttpUtils.getHtml("https://ncov.dxy.cn/ncovh5/view/pneumonia");

        //2.解析页面中的指定内容-即id为getAreaStat的标签中的全国疫情数据
        Document doc = Jsoup.parse(html);
        String text = doc.select("script[id=getAreaStat]").toString();

        //3.使用正则表达式获取json格式的疫情数据
        String pattern = "\\[(.*)\\]";//定义正则规则
        Pattern reg = Pattern.compile(pattern);//编译成正则对象
        Matcher matcher = reg.matcher(text);//去text中进行匹配
        String jsonStr = "";
        if(matcher.find()){//如果text中的内容和正则规则匹配上就取出来赋值给jsonStr变量
            jsonStr = matcher.group(0);
        }else{
            System.out.println("no match");
        }

        //对json数据进行更近一步的解析
        //4.将第一层json(省份数据)解析为JavaBean
        List<CovidBean> pCovidBeans = JSON.parseArray(jsonStr, CovidBean.class);
        for (CovidBean pBean : pCovidBeans) {//pBean为省份
            //先设置时间字段
            pBean.setDatetime(datetime);
            //获取cities
            String citysStr = pBean.getCities();
            //5.将第二层json(城市数据)解析为JavaBean
            List<CovidBean> covidBeans = JSON.parseArray(citysStr, CovidBean.class);
            for (CovidBean bean : covidBeans) {//bean为城市
                //System.out.println(bean);
                bean.setDatetime(datetime);
                bean.setPid(pBean.getLocationId());//把省份的id作为城市的pid
                bean.setProvinceName(pBean.getProvinceName());
                bean.setProvinceShortName(pBean.getProvinceShortName());
                //System.out.println(bean);
                //后续需要将城市疫情数据发送给Kafka
                //将JavaBean转为jsonStr再发送给Kafka
                String beanStr = JSON.toJSONString(bean);
                kafkaTemplate.send("covid19", bean.getPid(), beanStr);
            }
            //6.获取第一层json(省份数据)中的每一天的统计数据
            String statisticsDataUrl = pBean.getStatisticsData();
            // 爬虫
            String statisticsDataStr = HttpUtils.getHtml(statisticsDataUrl);
            //获取statisticsDataStr中的data字段对应的数据
            JSONObject jsonObject = JSON.parseObject(statisticsDataStr);
            String dataStr = jsonObject.getString("data");
            //System.out.println(dataStr);
            //将爬取解析出来的每一天的数据设置回省份pBean中的StatisticsData字段中(之前该字段只是一个URL)
            pBean.setStatisticsData(dataStr);
            pBean.setCities(null);
            //System.out.println(pBean);
            //后续需要将省份疫情数据发送给Kafka
            String pBeanStr = JSON.toJSONString(pBean);
            System.out.println(pBeanStr);
            kafkaTemplate.send("covid19",pBean.getLocationId(),pBeanStr);
        }
        //Thread.sleep(10000000);
    }
}

写入kafka的两种数据格式:

城市:

{"cityName":"拉萨","confirmedCount":1,"curedCount":1,"currentConfirmedCount":0,"datetime":"2022-03-14",
"deadCount":0,"locationId":540100,"pid":540000,"provinceShortName":"西藏","suspectedCount":0}

省份:

{"confirmedCount":1,"curedCount":1,"currentConfirmedCount":0,"datetime":"2022-03-14",
 "deadCount":0,"locationId":540000,"provinceName":"西藏自治区","provinceShortName":"西藏","statisticsData":"test","suspectedCount":0}

2.4 防疫数据生成

package cn.itcast.generator;

import cn.itcast.bean.MaterialBean;
import com.alibaba.fastjson.JSON;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

import java.util.Random;

/**
 *  物资         库存   需求     消耗     捐赠
 * N95口罩       4293   9395   3254   15000
 * 医用外科口罩  9032   7425   8382   55000
 * 医用防护服   1938   2552   1396   3500
 * 内层工作服   2270   3189   1028   2800
 * 一次性手术衣  3387   1000   1413   5000
 * 84消毒液/升 9073   3746   3627   10000
 * 75%酒精/升 3753   1705   1574   8000
 * 防护目镜/个  2721   3299   1286   4500
 * 防护面屏/个  2000   1500   1567   3500
 */
@Component
public class Covid19DataGenerator {
    @Autowired
    KafkaTemplate kafkaTemplate;

     @Scheduled(initialDelay = 1000, fixedDelay = 1000 * 10)
     public void generate() {
        System.out.println("每隔10s生成10条数据");
        Random random = new Random();
        for (int i = 0; i < 10; i++) {
            MaterialBean materialBean = new MaterialBean(wzmc[random.nextInt(wzmc.length)], wzlx[random.nextInt(wzlx.length)], random.nextInt(1000));
            String jsonString = JSON.toJSONString(materialBean);
            System.out.println(materialBean);
            kafkaTemplate.send("covid19_wz", random.nextInt(4),jsonString);
        }
    }

    private static String[] wzmc = new String[]{"N95口罩/个", "医用外科口罩/个", "84消毒液/瓶", "电子体温计/个", "一次性橡胶手套/副", "防护目镜/副",  "医用防护服/套"};

    private static String[] wzlx = new String[]{"采购", "下拨", "捐赠", "消耗","需求"};
}

第三章 实时数据处理和分析

3.1 环境准备

pom.xml

  <properties>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
        <encoding>UTF-8</encoding>
        <scala.version>2.11.8</scala.version>
        <spark.version>2.2.0</spark.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.scala-lang</groupId>
            <artifactId>scala-library</artifactId>
            <version>${scala.version}</version>
        </dependency>
        <dependency>
            <groupId>org.apache.spark</groupId>
            <artifactId>spark-core_2.11</artifactId>
            <version>${spark.version}</version>
        </dependency>
        <dependency>
            <groupId>org.apache.spark</groupId>
            <artifactId>spark-sql_2.11</artifactId>
            <version>${spark.version}</version>
        </dependency>
        <dependency>
            <groupId>org.apache.spark</groupId>
            <artifactId>spark-streaming_2.11</artifactId>
            <version>${spark.version}</version>
        </dependency>
        <dependency>
            <groupId>org.apache.spark</groupId>
            <artifactId>spark-streaming-kafka-0-10_2.11</artifactId>
            <version>${spark.version}</version>
        </dependency>
        <dependency>
            <groupId>org.apache.spark</groupId>
            <artifactId>spark-sql-kafka-0-10_2.11</artifactId>
            <version>${spark.version}</version>
        </dependency>
        <dependency>
            <groupId>com.typesafe</groupId>
            <artifactId>config</artifactId>
            <version>1.3.3</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.38</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.44</version>
        </dependency>
    </dependencies>
    <build>
        <sourceDirectory>src/main/scala</sourceDirectory>
        <plugins>
            <!-- 指定编译java的插件 -->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.5.1</version>
            </plugin>
            <!-- 指定编译scala的插件 -->
            <plugin>
                <groupId>net.alchim31.maven</groupId>
                <artifactId>scala-maven-plugin</artifactId>
                <version>3.2.2</version>
                <executions>
                    <execution>
                        <goals>
                            <goal>compile</goal>
                            <goal>testCompile</goal>
                        </goals>
                        <configuration>
                            <args>
                                <arg>-dependencyfile</arg>
                                <arg>${project.build.directory}/.scala_dependencies</arg>
                            </args>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>2.22.2</version>
                <configuration>
                    <useFile>false</useFile>
                    <disableXmlReport>true</disableXmlReport>
                    <includes>
                        <include>**/*Test.*</include>
                        <include>**/*Suite.*</include>
                    </includes>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-shade-plugin</artifactId>
                <version>3.2.1</version>
                <executions>
                    <execution>
                        <phase>package</phase>
                        <goals>
                            <goal>shade</goal>
                        </goals>
                        <configuration>
                            <filters>
                                <filter>
                                    <artifact>*:*</artifact>
                                    <excludes>
                                        <exclude>META-INF/*.SF</exclude>
                                        <exclude>META-INF/*.DSA</exclude>
                                        <exclude>META-INF/*.RSA</exclude>
                                    </excludes>
                                </filter>
                            </filters>
                            <transformers>
                                <transformer             implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
                                    <mainClass></mainClass>
                                </transformer>
                            </transformers>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>

3.2 工具类

OffsetUtil

package cn.itcast.util

import java.sql.{DriverManager, ResultSet}

import org.apache.kafka.common.TopicPartition
import org.apache.spark.streaming.kafka010.OffsetRange

import scala.collection.mutable

/*
手动维护offset的工具类
首先在MySQL创建如下表
 CREATE TABLE `t_offset` (
   `topic` varchar(255) NOT NULL,
   `partition` int(11) NOT NULL,
   `groupid` varchar(255) NOT NULL,
   `offset` bigint(20) DEFAULT NULL,
   PRIMARY KEY (`topic`,`partition`,`groupid`)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
*/
object OffsetUtil {
  //从数据库读取偏移量
  def getOffsetMap(groupid: String, topic: String):mutable.Map[TopicPartition, Long] = {
    val connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/bigdata?characterEncoding=UTF-8", "root", "root")
    val pstmt = connection.prepareStatement("select * from t_offset where groupid=? and topic=?")
    pstmt.setString(1, groupid)
    pstmt.setString(2, topic)
    val rs: ResultSet = pstmt.executeQuery()
    val offsetMap: mutable.Map[TopicPartition, Long] = mutable.Map[TopicPartition, Long]()
    while (rs.next()) {
      offsetMap += new TopicPartition(rs.getString("topic"), rs.getInt("partition")) -> rs.getLong("offset")
    }
    rs.close()
    pstmt.close()
    connection.close()
    offsetMap
  }

  //将偏移量保存到数据库
  def saveOffsetRanges(groupid: String, offsetRange: Array[OffsetRange]) = {
    val connection = DriverManager
      .getConnection("jdbc:mysql://localhost:3306/bigdata?characterEncoding=UTF-8", "root", "root")
    //replace into表示之前有就替换,没有就插入
    val pstmt = connection.prepareStatement("replace into t_offset (`topic`, `partition`, `groupid`, `offset`) values(?,?,?,?)")
    for (o <- offsetRange) {
      pstmt.setString(1, o.topic)
      pstmt.setInt(2, o.partition)
      pstmt.setString(3, groupid)
      pstmt.setLong(4, o.untilOffset)
      pstmt.executeUpdate()
    }
    pstmt.close()
    connection.close()
  }
}

BaseJdbcSink

package cn.itcast.process

import java.sql.{Connection, DriverManager, PreparedStatement}

import org.apache.spark.sql.{ForeachWriter, Row}
abstract class BaseJdbcSink(sql:String) extends ForeachWriter[Row] {
  var conn: Connection = _
  var ps: PreparedStatement = _
  override def open(partitionId: Long, version: Long): Boolean = {
    conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/bigdata?characterEncoding=UTF-8","root","root")
    true
  }

  override def process(value: Row): Unit = {
    realProcess(sql,value)
  }

  def realProcess(sql:String,value: Row)

  override def close(errorOrNull: Throwable): Unit = {
    if (conn != null) {
      conn.close
    }
    if (ps != null) {
      ps.close()
    }
  }
}

3.3 物资数据实时处理与分析

3.3.1 MySQL中的表

CREATE TABLE `covid19_wz` (
`name` varchar(12) NOT NULL DEFAULT '',
`cg` int(11) DEFAULT '0',
`xb` int(11) DEFAULT '0',
`jz` int(11) DEFAULT '0',
`xh` int(11) DEFAULT '0',
`xq` int(11) DEFAULT '0',
`kc` int(11) DEFAULT '0',
PRIMARY KEY (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

3.3.2 SparkStreaming实时处理代码

package cn.itcast.process

import java.sql.{Connection, DriverManager, PreparedStatement}
import cn.itcast.util.OffsetUtil
import com.alibaba.fastjson.{JSON, JSONObject}
import org.apache.kafka.clients.consumer.ConsumerRecord
import org.apache.kafka.common.TopicPartition
import org.apache.kafka.common.serialization.StringDeserializer
import org.apache.spark.{SparkConf, SparkContext, streaming}
import org.apache.spark.streaming.StreamingContext
import org.apache.spark.streaming.dstream.{DStream, InputDStream}
import org.apache.spark.streaming.kafka010.{ConsumerStrategies, HasOffsetRanges, KafkaUtils, LocationStrategies, OffsetRange}

import scala.collection.mutable


object Covid19WZDataProcessTask {
  def main(args: Array[String]): Unit = {
    //1.创建ssc
    val conf: SparkConf = new SparkConf().setAppName("WordCount").setMaster("local[*]")
    val sc: SparkContext = new SparkContext(conf)
    sc.setLogLevel("WARN")
    val ssc: StreamingContext = new StreamingContext(sc, streaming.Seconds(5))
    ssc.checkpoint("./sscckp")
    //2.准备Kafka的连接参数
    val kafkaParams: Map[String, Object] = Map[String, Object](
      "bootstrap.servers" -> "node01:9092,node02:9092,node03:9092", //kafka集群地址
      "key.deserializer" -> classOf[StringDeserializer], //key的反序列化类型
      "value.deserializer" -> classOf[StringDeserializer], //value的反序列化类型
      //消费发给Kafka需要经过网络传输,而经过网络传输都需要进行序列化,即消息发给kafka需要序列化,那么从kafka消费完就得反序列化
      "group.id" -> "SparkKafka", //消费者组名称
      //earliest:当各分区下有已提交的offset时,从提交的offset开始消费;无提交的offset时,从头开始消费
      //latest:当各分区下有已提交的offset时,从提交的offset开始消费;无提交的offset时,消费新产生的该分区下的数据
      //none:当各分区都存在已提交的offset时,从offset后开始消费;只要有一个分区不存在已提交的offset,则抛出异常
      //这里配置latest自动重置偏移量为最新的偏移量,即如果有偏移量从偏移量位置开始消费,没有偏移量从新来的数据开始消费
      "auto.offset.reset" -> "latest",
      //使用手动提交offset
      "enable.auto.commit" -> (false: java.lang.Boolean)
    )
    val topics = Array("covid19_wz")

    //3.使用KafkaUtils.createDirectStream连接Kafka
    //根据消费者组id和主题,查询该消费者组接下来应该从主题的哪个分区的哪个偏移量开始接着消费
    val map: mutable.Map[TopicPartition, Long] = OffsetUtil.getOffsetMap("SparkKafka", "covid19_wz")
    val recordDStream: InputDStream[ConsumerRecord[String, String]] = if (map.size > 0) { //表示MySQL中存储了偏移量,那么应该从偏移量位置开始消费
      println("MySQL中存储了偏移量,从偏移量位置开始消费")
      KafkaUtils.createDirectStream[String, String](
        ssc,
        LocationStrategies.PreferConsistent,
        ConsumerStrategies.Subscribe[String, String](topics, kafkaParams, map))
    } else { //表示MySQL中没有存储偏移量,应该从"auto.offset.reset" -> "latest"开始消费
      println("MySQL中没有存储偏移量,从latest开始消费")
      KafkaUtils.createDirectStream[String, String](
        ssc,
        LocationStrategies.PreferConsistent,
        ConsumerStrategies.Subscribe[String, String](topics, kafkaParams))
    }
      
    //4.实时处理分析数据
    //val valueDS: DStream[String] = recordDStream.map(_.value())//_表示从Kafka中消费出来的每一条消息
    //valueDS.print()
    //需求分析:
    //{"count":673,"from":"消耗","name":"N95口罩/个"}
    //{"count":207,"from":"下拨","name":"医用防护服/套"}
    //{"count":802,"from":"捐赠","name":"医用防护服/套"}
    //{"count":870,"from":"需求","name":"护目镜/副"}
    //{"count":774,"from":"消耗","name":"医用防护服/套"}
    //{"count":439,"from":"下拨","name":"电子体温计/个"}
    //{"count":642,"from":"捐赠","name":"医用防护服/套"}
    //{"count":105,"from":"采购","name":"医用外科口罩/个"}
    //{"count":829,"from":"消耗","name":"84消毒液/瓶"}
    //{"count":254,"from":"捐赠","name":"一次性手套/副"}
    //我们从Kafka中消费的数据为如上格式的jsonStr,需要解析为json对象(或者是样例类)
    //目标是:将数据转为如下格式:
    //名称,采购,下拨,捐赠,消耗,需求,库存
    //N95口罩/个,1000,1000,500,-1000,-1000,500
    //护目镜/副,500,300,100,-400,-100,400
    //为了达成目标结果格式,我们需要对每一条数据进行处理,得出如下格式:
    //(name,(采购,下拨,捐赠,消耗,需求,库存))
    //如:接收到一条数据为:
    //{"count":673,"from":"消耗","name":"N95口罩/个"}
    //应该记为:
    //(N95口罩/个,(采购0,下拨0,捐赠0,消耗-673,需求0,库存-673))
    //再来一条数据:
    //{"count":500,"from":"采购","name":"N95口罩/个"}
    //应该记为:
    //(N95口罩/个,(采购500,下拨0,捐赠0,消耗0,需求0,库存500))
    //最后聚合结果:
    //(N95口罩/个,(采购500,下拨0,捐赠0,消耗-673,需求0,库存-173))
    
    //4.1将接收到的数据转换为需要的元组格式:(name,(采购,下拨,捐赠,消耗,需求,库存))
    val tupleDS: DStream[(String, (Int, Int, Int, Int, Int, Int))] = recordDStream.map(r => {
      val jsonStr: String = r.value()
      val jsonObj: JSONObject = JSON.parseObject(jsonStr)
      val name: String = jsonObj.getString("name")
      val from: String = jsonObj.getString("from") //"采购","下拨", "捐赠", "消耗","需求"
      val count: Int = jsonObj.getInteger("count")
      from match {
        //"采购","下拨", "捐赠", "消耗","需求","库存"
        case "采购" => (name, (count, 0, 0, 0, 0, count))
        case "下拨" => (name, (0, count, 0, 0, 0, count))
        case "捐赠" => (name, (0, 0, count, 0, 0, count))
        case "消耗" => (name, (0, 0, 0, -count, 0, -count))
        case "需求" => (name, (0, 0, 0, 0, count, 0))
      }
    })
    //tupleDS.print()
    //(N95口罩/个,(0,0,0,0,-784,-784))
    //(N95口罩/个,(0,0,0,0,-755,-755))
    //(医用防护服/套,(0,0,0,0,-899,-899))
    //(护目镜/副,(0,0,0,0,-154,-154))
    //(电子体温计/个,(0,0,0,0,-658,-658))
    //(护目镜/副,(230,0,0,0,0,230))
    //(84消毒液/瓶,(0,0,0,0,-274,-274))
    //(N95口罩/个,(0,752,0,0,0,752))
    //(电子体温计/个,(0,0,0,-240,0,-240))
    //(电子体温计/个,(0,0,0,0,-980,-980))
      
    //4.2将上述格式的数据按照key进行聚合(有状态的计算)--使用updateStateBykey
    //定义一个函数,用来将当前批次的数据和历史数据进行聚合
    val updateFunc = (currentValues: Seq[(Int, Int, Int, Int, Int, Int)], historyValue: Option[(Int, Int, Int, Int, Int, Int)]) => {
      var current_cg: Int = 0
      var current_xb: Int = 0
      var current_jz: Int = 0
      var current_xh: Int = 0
      var current_xq: Int = 0
      var current_kc: Int = 0
      if (currentValues.size > 0) {
        //循环当前批次的数据
        for (i <- 0 until currentValues.size) {
          current_cg += currentValues(i)._1
          current_xb += currentValues(i)._2
          current_jz += currentValues(i)._3
          current_xh += currentValues(i)._4
          current_xq += currentValues(i)._5
          current_kc += currentValues(i)._6
        }
        //获取以前批次值
        val history_cg: Int = historyValue.getOrElse((0, 0, 0, 0, 0, 0))._1
        val history_xb: Int = historyValue.getOrElse((0, 0, 0, 0, 0, 0))._2
        val history_jz: Int = historyValue.getOrElse((0, 0, 0, 0, 0, 0))._3
        val history_xh: Int = historyValue.getOrElse((0, 0, 0, 0, 0, 0))._4
        val history_xq: Int = historyValue.getOrElse((0, 0, 0, 0, 0, 0))._5
        val history_kc: Int = historyValue.getOrElse((0, 0, 0, 0, 0, 0))._6

        Option((
          current_cg + history_cg,
          current_xb + history_xb,
          current_jz + history_jz,
          current_xh + history_xh,
          current_xq + history_xq,
          current_kc+history_kc
        ))
      } else {
        historyValue //如果当前批次没有数据直接返回之前的值即可
      }
    }

    val result: DStream[(String, (Int, Int, Int, Int, Int, Int))] = tupleDS.updateStateByKey(updateFunc)
    //result.print()
    /*
    "采购","下拨", "捐赠", "消耗","需求","库存"
    (防护目镜/副,(0,0,0,0,859,0))
    (医用外科口罩/个,(725,0,0,0,0,725))
    (防护面屏/个,(0,0,795,0,0,795))
    (电子体温计/个,(0,0,947,0,0,947))
    (N95口罩/个,(0,723,743,0,0,1466))
    (手持式喷壶/个,(0,0,0,0,415,0))
    (洗手液/瓶,(0,0,377,0,0,377))
    (一次性橡胶手套/副,(0,1187,0,0,0,1187))
     */
    // 5.将处理分析的结果存入到MySQL
    result.foreachRDD(rdd=>{
      rdd.foreachPartition(lines=>{
        val conn: Connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/bigdata?characterEncoding=UTF-8","root","root")
        val sql: String = "replace into covid19_wz(name,cg,xb,jz,xh,xq,kc) values(?,?,?,?,?,?,?)"
        val ps: PreparedStatement = conn.prepareStatement(sql)
        try {
          for (row <- lines) {
            ps.setString(1,row._1)
            ps.setInt(2,row._2._1)
            ps.setInt(3,row._2._2)
            ps.setInt(4,row._2._3)
            ps.setInt(5,row._2._4)
            ps.setInt(6,row._2._5)
            ps.setInt(7,row._2._6)
            ps.executeUpdate()
          }
        } finally {
          ps.close()
          conn.close()
        }
      })
    })

      
     //6.手动提交偏移量
    //我们要手动提交偏移量,那么就意味着,消费了一批数据就应该提交一次偏移量
    //在SparkStreaming中数据抽象为DStream,DStream的底层其实也就是RDD,也就是每一批次的数据
    //所以接下来我们应该对DStream中的RDD进行处理
    recordDStream.foreachRDD(rdd => {
      if (rdd.count() > 0) { //如果该rdd中有数据则处理
        //rdd.foreach(record=>println("从Kafka中消费到的每一条消息:"+record))
        //从Kafka中消费到的每一条消息:ConsumerRecord(topic = covid19_wz, partition = 2, offset = 19, CreateTime = 1590634048100, checksum = 1933585894, serialized key size = -1, serialized value size = 3, key = null, value = 789)
        //获取偏移量
        //使用Spark-streaming-kafka-0-10中封装好的API来存放偏移量并提交
        val offsets: Array[OffsetRange] = rdd.asInstanceOf[HasOffsetRanges].offsetRanges
        //for (o <-offsets){
        //println(s"topic=${o.topic},partition=${o.partition},fromOffset=${o.fromOffset},until=${o.untilOffset}")
        //topic=covid19_wz,partition=1,fromOffset=12,until=14
        //topic=covid19_wz,partition=0,fromOffset=16,until=18
        //topic=covid19_wz,partition=2,fromOffset=19,until=21
        // }
        //手动提交偏移量到Kafka的默认主题:__consumer__offsets中,如果开启了Checkpoint还会提交到Checkpoint中
        //kafkaDS.asInstanceOf[CanCommitOffset].commitAsync(offsets)
        OffsetUtils.saveOffsets("SparkKafka", offsets)
      }
    })

    //7.开启SparkStreaming任务并等待结束
    ssc.start()
    ssc.awaitTermination()
  }
}

补充知识点

SparkStreaming整合Kafka的两种方式?

Receiver模式

KafkaUtils.creatDStream--API创建

会有一个Receiver作为常驻Task运行在Executor进程中,一直等待数据的到来

一个Receiver效率会比较低,那么可以使用多个Receiver,但是多个Receiver中的数据又需要手动进行Union(合并)很麻烦且其中某个Receiver挂了,会导致数据丢失,需要开启WAL预写日志来保证数据安全,但是效率又低了

Receiver模式使用Zookeeper来连接Kafka(Kafka的新版本中已经不推荐使用该方式了)

Receiver模式使用的是Kafka的高阶API(高度封装的),offset由Receiver提交到ZK中(Kafka的新版本中offset默认存储在默认主题
__consumer__offset中的,不推荐存入到ZK中了),容易和Spark维护在Checkpoint中的offset不一致

所以不管从何种角度去说Receiver模式都已经不再适合现如今的Kafka版本了,面试的时候要说出以上的原因!

Direct模式
KafkaUtils.createDirectStream--API创建

Direct模式是直接连接到Kafka的各个分区,并拉取数据,提高了数据读取的并发能力

Direct模式使用的是Kafka低阶API(底层API),可以自己维护偏移量到任何地方(默认是由Spark提交到默认主题/Checkpoint)

Direct模式+手动操作可以保证数据的Exactly-Once精准一次(数据仅会被处理一次)

SparkStreaming整合Kafka的两个版本的API?

  • Spark-streaming-kafka-0-8
    支持Receiver模式和Direct模式,但是不支持offset维护API,不支持动态分区订阅..

  • Spark-streaming-kafka-0-10
    支持Direct,不支持Receiver模式,支持offset维护API,支持动态分区订阅..

结论:使用Spark-streaming-kafka-0-10版本即可

消费者重置offset

由于 consumer 在消费过程中可能会出现断电宕机等故障,consumer 恢复后,需要从故障前的位置的继续消费,所以 consumer 需要实时记录自己消费到了哪个 offset,以便故障恢复后继续消费。

所以 offset 的维护是 Consumer 消费数据是必须考虑的问题。

enable.auto.commit=true即自动提交offset。默认是自动提交的。

自动提交offset十分便利,但是由于其实基于时间提交的,开发人员难以把握offset提交的时机,因此kafka提供了手动提交offset的API。

Kafka0.9版本之前,offset存储在zookeeper中,0.9版本及之后的版本,默认将offset存储在Kafka的一个内置的topic中,除此之外,Kafka还可以选择自定义存储offset数据。这里是将offset存储到MySQL中了。

3.4 疫情数据实时处理与分析

3.4.1 MySQL中的表

全国疫情汇总信息

现有确诊,累计确诊,现有疑似,累计治愈,累计死亡

CREATE TABLE `covid19_1` (
  `datetime` varchar(20) NOT NULL DEFAULT '',
  `currentConfirmedCount` bigint(20) DEFAULT '0',
  `confirmedCount` bigint(20) DEFAULT '0',
  `suspectedCount` bigint(20) DEFAULT '0',
  `curedCount` bigint(20) DEFAULT '0',
  `deadCount` bigint(20) DEFAULT '0',
  PRIMARY KEY (`datetime`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+----------+---------------------+--------------+--------------+----------+---------+
|datetime  |currentConfirmedCount|confirmedCount|suspectedCount|curedCount|deadCount|
+----------+---------------------+--------------+--------------+----------+---------+
|2020-05-28|111                  |84547         |1476          |79791     |4645     |
+----------+---------------------+--------------+--------------+----------+---------+

全国各省份累计确诊数

注意:按照日期-省份分组

CREATE TABLE `covid19_2` (
  `datetime` varchar(20) NOT NULL DEFAULT '',
  `locationId` int(11) NOT NULL DEFAULT '0',
  `provinceShortName` varchar(20) DEFAULT '',
  `currentConfirmedCount` int(11) DEFAULT '0',
  `confirmedCount` int(11) DEFAULT '0',
  `suspectedCount` int(11) DEFAULT '0',
  `curedCount` int(11) DEFAULT '0',
  `deadCount` int(11) DEFAULT '0',
  PRIMARY KEY (`datetime`,`locationId`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+----------+----------+-----------------+---------------------+--------------+--------------+----------+---------+
|datetime  |locationId|provinceShortName|currentConfirmedCount|confirmedCount|suspectedCount|curedCount|deadCount|
+----------+----------+-----------------+---------------------+--------------+--------------+----------+---------+
|2020-05-28|810000    |香港               |27                   |1066          |47            |1035      |4        |
|2020-05-28|630000    |青海               |0                    |18            |0             |18        |0        |
|2020-05-28|540000    |西藏               |0                    |1             |0             |1         |0        |
|2020-05-28|820000    |澳门               |0                    |45            |9             |45        |0        |

全国疫情趋势

注意:按照日期分组聚合

CREATE TABLE `covid19_3` (
  `dateId` varchar(20) NOT NULL DEFAULT '',
  `confirmedIncr` bigint(20) DEFAULT '0',
  `confirmedCount` bigint(20) DEFAULT '0',
  `suspectedCount` bigint(20) DEFAULT '0',
  `curedCount` bigint(20) DEFAULT '0',
  `deadCount` bigint(20) DEFAULT '0',
  PRIMARY KEY (`dateId`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+--------+-------------+--------------+--------------+----------+---------+
|dateId  |confirmedIncr|confirmedCount|suspectedCount|curedCount|deadCount|
+--------+-------------+--------------+--------------+----------+---------+
|20200413|99           |83696         |415           |78262     |3351     |
|20200218|1749         |74277         |0             |14378     |2006     |
|20200314|27           |81048         |0             |67022     |3204     |
|20200311|25           |80980         |0             |62874     |3173     |
|20200202|2830         |17238         |0             |478       |361      |
|20200217|1896         |72528         |0             |12557     |1870     |

境外输入排行

注意:按照日期-省份分组聚合

CREATE TABLE `covid19_4` (
  `datetime` varchar(20) NOT NULL DEFAULT '',
  `provinceShortName` varchar(20) NOT NULL DEFAULT '',
  `pid` int(11) DEFAULT '0',
  `confirmedCount` bigint(20) DEFAULT '0',
  PRIMARY KEY (`datetime`,`provinceShortName`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+----------+-----------------+------+--------------+
|datetime  |provinceShortName|pid   |confirmedCount|
+----------+-----------------+------+--------------+
|2020-05-28|黑龙江              |230000|386           |
|2020-05-28|上海               |310000|330           |
|2020-05-28|北京               |110000|174           |
|2020-05-28|内蒙古              |150000|155           |
|2020-05-28|山西               |140000|64            |
|2020-05-28|陕西               |610000|63            |

3.4.2 StructuredStreaming实时处理代码

package cn.itcast.process

import cn.itcast.bean.{CovidBean, StatisticsDataBean}
import cn.itcast.util.BaseJdbcSink
import com.alibaba.fastjson.JSON
import org.apache.spark.SparkContext
import org.apache.spark.sql.streaming.Trigger
import org.apache.spark.sql.{DataFrame, Dataset, ForeachWriter, Row, SparkSession}

import scala.collection.immutable.StringOps
import scala.collection.mutable

/**
 * Desc 全国各省市疫情数据实时处理统计分析
 */
object Covid19_Data_Process {
  def main(args: Array[String]): Unit = {
    //1.创建StructuredStreaming执行环境
    //StructuredStreaming支持使用SQL来处理实时流数据,数据抽象和SparkSQL一样,也是DataFrame和DataSet
    //所以这里创建StructuredStreamin执行环境就直接创建SparkSession即可
    val spark: SparkSession = SparkSession.builder().master("local[*]").appName("Covid19_Data_Process").getOrCreate()
    val sc: SparkContext = spark.sparkContext
    sc.setLogLevel("WARN")
    //导入隐式转换方便后续使用
    import spark.implicits._
    import org.apache.spark.sql.functions._
    import scala.collection.JavaConversions._

    //2.连接Kafka
    //从kafka接收消息
    val kafkaDF: DataFrame = spark.readStream
      .format("kafka")
      .option("kafka.bootstrap.servers", "hadoop102:9092,hadoop103:9092,hadoop104:9092")
      .option("subscribe", "covid19")
      .load()
    //取出消息中的value
    val jsonStrDS: Dataset[String] = kafkaDF.selectExpr("CAST(value AS STRING)").as[String]
    //3.处理数据
    //将jsonStr转为样例类
    val covidBeanDS: Dataset[CovidBean] = jsonStrDS.map(jsonStr => {
      //注意:Scala中获取class对象使用classOf[类名]
      //Java中使用类名.class/Class.forName(全类路径)/对象.getClass()
      JSON.parseObject(jsonStr, classOf[CovidBean])
    })

    //分离出省份数据
    //statisticsData 不为空的是省份数据
    val provinceDS: Dataset[CovidBean] = covidBeanDS.filter(_.statisticsData != null)

    // 分离出城市数据
    // 城市拥有的属性 datetime pid provinceShortName
    val cityDS: Dataset[CovidBean] = covidBeanDS.filter(_.statisticsData == null)

    //分离出各省份每一天的统计数据
    val statisticsDataDS: Dataset[StatisticsDataBean] = provinceDS.flatMap(p => {
      val jsonStr: StringOps = p.statisticsData //获取到的是该省份每一天的统计数据组成的jsonStr数组
      val list: mutable.Buffer[StatisticsDataBean] = JSON.parseArray(jsonStr, classOf[StatisticsDataBean])
      list.map(s => {
        s.provinceShortName = p.provinceShortName
        s.locationId = p.locationId
        s
      })
    })

    /*statisticsDataDS.writeStream
      .format("console")//输出目的地
      .outputMode("append")//输出模式,默认就是append表示显示新增行
      .trigger(Trigger.ProcessingTime(0))//触发间隔,0表示尽可能快的执行
      .option("truncate",false)//表示如果列名过长不进行截断
      .start()
      .awaitTermination()*/
    /*
+--------+-----------------+----------+--------------+---------------------+--------------------+-------------+----------+---------+------------------+--------------+---------+--------+
|dateId  |provinceShortName|locationId|confirmedCount|currentConfirmedCount|currentConfirmedIncr|confirmedIncr|curedCount|curedIncr|suspectedCountIncr|suspectedCount|deadCount|deadIncr|
+--------+-----------------+----------+--------------+---------------------+--------------------+-------------+----------+---------+------------------+--------------+---------+--------+
|20200122|广西               |450000    |2             |2                    |2                   |2            |0         |0        |0                 |0             |0        |0       |
|20200123|广西               |450000    |13            |13                   |11                  |11           |0         |0        |0                 |0             |0        |0       |
|20200124|广西               |450000    |23            |23                   |10                  |10           |0         |0        |0                 |0             |0        |0       |
|20200125|广西               |450000    |33            |33                   |10                  |10           |0         |0        |0                 |0             |0        |0       |
     */

    //4.统计分析
    //4.1.全国疫情汇总信息:现有确诊,累计确诊,现有疑似,累计治愈,累计死亡--注意:按照日期分组统计
    val result1: DataFrame = provinceDS.groupBy('datetime)
      .agg(sum('currentConfirmedCount) as "currentConfirmedCount", //现有确诊
        sum('confirmedCount) as "confirmedCount", //累计确诊
        sum('suspectedCount) as "suspectedCount", //现有疑似
        sum('curedCount) as "curedCount", //累计治愈
        sum('deadCount) as "deadCount" //累计死亡
      )

    //4.2.全国各省份累计确诊数地图--注意:按照日期-省份分组
    /*cityDS.groupBy('datetime,'provinceShortName)
      .agg(sum('confirmedCount) as "confirmedCount")*/
    val result2: DataFrame = provinceDS.select('datetime,'locationId,'provinceShortName,'currentConfirmedCount,'confirmedCount,'suspectedCount,'curedCount,'deadCount)

    //4.3.全国疫情趋势--注意:按照日期分组聚合
    val result3: DataFrame = statisticsDataDS.groupBy('dateId)
      .agg(
        sum('confirmedIncr) as "confirmedIncr", //新增确诊
        sum('confirmedCount) as "confirmedCount", //累计确诊
        sum('suspectedCount) as "suspectedCount", //累计疑似
        sum('curedCount) as "curedCount", //累计治愈
        sum('deadCount) as "deadCount" //累计死亡
      )

    //4.4.境外输入排行--注意:按照日期-城市分组聚合
    val result4: Dataset[Row] = cityDS.filter(_.cityName.contains("境外输入"))
      .groupBy('datetime, 'provinceShortName, 'pid)
      .agg(sum('confirmedCount) as "confirmedCount")
      .sort('confirmedCount.desc)

    //5.结果输出--先输出到控制台观察,最终输出到MySQL
    result1.writeStream
      .format("console")
      //输出模式:
      //1.append:默认的,表示只输出新增的数据,只支持简单的查询,不支持聚合
      //2.complete:表示完整模式,所有数据都会输出,必须包含聚合操作
      //3.update:表示更新模式,只输出有变化的数据,不支持排序
      .outputMode("complete")
      .trigger(Trigger.ProcessingTime(0))
      .option("truncate",false)
      .start()
      //.awaitTermination()
    /*
+----------+---------------------+--------------+--------------+----------+---------+
|datetime  |currentConfirmedCount|confirmedCount|suspectedCount|curedCount|deadCount|
+----------+---------------------+--------------+--------------+----------+---------+
|2020-05-28|111                  |84547         |1476          |79791     |4645     |
+----------+---------------------+--------------+--------------+----------+---------+
CREATE TABLE `covid19_1` (
  `datetime` varchar(20) NOT NULL DEFAULT '',
  `currentConfirmedCount` bigint(20) DEFAULT '0',
  `confirmedCount` bigint(20) DEFAULT '0',
  `suspectedCount` bigint(20) DEFAULT '0',
  `curedCount` bigint(20) DEFAULT '0',
  `deadCount` bigint(20) DEFAULT '0',
  PRIMARY KEY (`datetime`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

     */
    result1.writeStream
        .foreach(new BaseJdbcSink("replace into covid19_1 (datetime,currentConfirmedCount,confirmedCount,suspectedCount,curedCount,deadCount) values(?,?,?,?,?,?)") {
          override def realProcess(sql: String, row: Row): Unit = {
            //取出row中的数据
            val datetime: String = row.getAs[String]("datetime")
            val currentConfirmedCount: Long = row.getAs[Long]("currentConfirmedCount")
            val confirmedCount: Long = row.getAs[Long]("confirmedCount")
            val suspectedCount: Long = row.getAs[Long]("suspectedCount")
            val curedCount: Long = row.getAs[Long]("curedCount")
            val deadCount: Long = row.getAs[Long]("deadCount")
            //获取预编译语句对象
            ps = conn.prepareStatement(sql)
            //给sql设置参数值
            ps.setString(1,datetime)
            ps.setLong(2,currentConfirmedCount)
            ps.setLong(3,confirmedCount)
            ps.setLong(4,suspectedCount)
            ps.setLong(5,curedCount)
            ps.setLong(6,deadCount)
            ps.executeUpdate()
          }
        })
      .outputMode("complete")
      .trigger(Trigger.ProcessingTime(0))
      .option("truncate",false)
      .start()


    result2.writeStream
      .format("console")
      .outputMode("append")
      .trigger(Trigger.ProcessingTime(0))
      .option("truncate",false)
      .start()
      //.awaitTermination()
    /*
+----------+----------+-----------------+---------------------+--------------+--------------+----------+---------+
|datetime  |locationId|provinceShortName|currentConfirmedCount|confirmedCount|suspectedCount|curedCount|deadCount|
+----------+----------+-----------------+---------------------+--------------+--------------+----------+---------+
|2020-05-28|810000    |香港               |27                   |1066          |47            |1035      |4        |
|2020-05-28|630000    |青海               |0                    |18            |0             |18        |0        |
|2020-05-28|540000    |西藏               |0                    |1             |0             |1         |0        |
|2020-05-28|820000    |澳门               |0                    |45            |9             |45        |0        |
  ...
CREATE TABLE `covid19_2` (
  `datetime` varchar(20) NOT NULL DEFAULT '',
  `locationId` int(11) NOT NULL DEFAULT '0',
  `provinceShortName` varchar(20) DEFAULT '',
  `currentConfirmedCount` int(11) DEFAULT '0',
  `confirmedCount` int(11) DEFAULT '0',
  `suspectedCount` int(11) DEFAULT '0',
  `curedCount` int(11) DEFAULT '0',
  `deadCount` int(11) DEFAULT '0',
  PRIMARY KEY (`datetime`,`locationId`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
     */
    result2.writeStream
      .foreach(new BaseJdbcSink("replace into covid19_2 (datetime,locationId,provinceShortName,currentConfirmedCount,confirmedCount,suspectedCount,curedCount,deadCount) values(?,?,?,?,?,?,?,?)") {
        override def realProcess(sql: String, row: Row): Unit = {
          val datetime: String = row.getAs[String]("datetime")
          val locationId: Int = row.getAs[Int]("locationId")
          val provinceShortName: String = row.getAs[String]("provinceShortName")
          val currentConfirmedCount: Int = row.getAs[Int]("currentConfirmedCount")
          val confirmedCount: Int = row.getAs[Int]("confirmedCount")
          val suspectedCount: Int = row.getAs[Int]("suspectedCount")
          val curedCount: Int = row.getAs[Int]("curedCount")
          val deadCount: Int = row.getAs[Int]("deadCount")
          ps = conn.prepareStatement(sql)
          ps.setString(1,datetime)
          ps.setInt(2,locationId)
          ps.setString(3,provinceShortName)
          ps.setInt(4,currentConfirmedCount)
          ps.setInt(5,confirmedCount)
          ps.setInt(6,suspectedCount)
          ps.setInt(7,curedCount)
          ps.setInt(8,deadCount)
          ps.executeUpdate()
        }
      })
      .outputMode("append")
      .trigger(Trigger.ProcessingTime(0))
      .option("truncate",false)
      .start()

    result3.writeStream
      .format("console")
      .outputMode("complete")
      .trigger(Trigger.ProcessingTime(0))
      .option("truncate",false)
      .start()
      //.awaitTermination()
    /*
  +--------+-------------+--------------+--------------+----------+---------+
|dateId  |confirmedIncr|confirmedCount|suspectedCount|curedCount|deadCount|
+--------+-------------+--------------+--------------+----------+---------+
|20200413|99           |83696         |415           |78262     |3351     |
|20200218|1749         |74277         |0             |14378     |2006     |
|20200314|27           |81048         |0             |67022     |3204     |
|20200311|25           |80980         |0             |62874     |3173     |
|20200202|2830         |17238         |0             |478       |361      |
|20200217|1896         |72528         |0             |12557     |1870     |
  ...
CREATE TABLE `covid19_3` (
  `dateId` varchar(20) NOT NULL DEFAULT '',
  `confirmedIncr` bigint(20) DEFAULT '0',
  `confirmedCount` bigint(20) DEFAULT '0',
  `suspectedCount` bigint(20) DEFAULT '0',
  `curedCount` bigint(20) DEFAULT '0',
  `deadCount` bigint(20) DEFAULT '0',
  PRIMARY KEY (`dateId`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
     */
    result3.writeStream
      .foreach(new BaseJdbcSink("replace into covid19_3 (dateId,confirmedIncr,confirmedCount,suspectedCount,curedCount,deadCount) values(?,?,?,?,?,?)") {
        override def realProcess(sql: String, row: Row): Unit = {
          //取出row中的数据
          val dateId: String = row.getAs[String]("dateId")
          val confirmedIncr: Long = row.getAs[Long]("confirmedIncr")
          val confirmedCount: Long = row.getAs[Long]("confirmedCount")
          val suspectedCount: Long = row.getAs[Long]("suspectedCount")
          val curedCount: Long = row.getAs[Long]("curedCount")
          val deadCount: Long = row.getAs[Long]("deadCount")
          //获取预编译语句对象
          ps = conn.prepareStatement(sql)
          //给sql设置参数值
          ps.setString(1,dateId)
          ps.setLong(2,confirmedIncr)
          ps.setLong(3,confirmedCount)
          ps.setLong(4,suspectedCount)
          ps.setLong(5,curedCount)
          ps.setLong(6,deadCount)
          ps.executeUpdate()
        }
      })
      .outputMode("complete")
      .trigger(Trigger.ProcessingTime(0))
      .option("truncate",false)
      .start()

    result4.writeStream
      .format("console")
      .outputMode("complete")
      .trigger(Trigger.ProcessingTime(0))
      .option("truncate",false)
      .start()
      //.awaitTermination()
    /*
  +----------+-----------------+------+--------------+
|datetime  |provinceShortName|pid   |confirmedCount|
+----------+-----------------+------+--------------+
|2020-05-28|黑龙江              |230000|386           |
|2020-05-28|上海               |310000|330           |
|2020-05-28|北京               |110000|174           |
|2020-05-28|内蒙古              |150000|155           |
|2020-05-28|山西               |140000|64            |
|2020-05-28|陕西               |610000|63            |
  ....
CREATE TABLE `covid19_4` (
  `datetime` varchar(20) NOT NULL DEFAULT '',
  `provinceShortName` varchar(20) NOT NULL DEFAULT '',
  `pid` int(11) DEFAULT '0',
  `confirmedCount` bigint(20) DEFAULT '0',
  PRIMARY KEY (`datetime`,`provinceShortName`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
     */
    result4.writeStream
      .foreach(new BaseJdbcSink("replace into covid19_4 (datetime,provinceShortName,pid,confirmedCount) values(?,?,?,?)") {
        override def realProcess(sql: String, row: Row): Unit = {
          //取出row中的数据
          val datetime: String = row.getAs[String]("datetime")
          val provinceShortName: String = row.getAs[String]("provinceShortName")
          val pid: Int = row.getAs[Int]("pid")
          val confirmedCount: Long = row.getAs[Long]("confirmedCount")
          //获取预编译语句对象
          ps = conn.prepareStatement(sql)
          //给sql设置参数值
          ps.setString(1,datetime)
          ps.setString(2,provinceShortName)
          ps.setInt(3,pid)
          ps.setLong(4,confirmedCount)
          ps.executeUpdate()
        }
      })
      .outputMode("complete")
      .trigger(Trigger.ProcessingTime(0))
      .option("truncate",false)
      .start()
  }
}

第四章 实时数据展示

4.1 环境准备

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.3.0.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>cn.itcast</groupId>
    <artifactId>webui</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>webui</name>
    <description>Demo project for Spring Boot</description>

    <properties>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.1.2</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>3.7</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

application.properties

spring.datasource.driverClassName = com.mysql.jdbc.Driver
spring.datasource.url = jdbc:mysql://localhost:3306/bigdata?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8
spring.datasource.username = root
spring.datasource.password = root

静态资源

导入资料中的static文件夹中的js和css文件夹中的文件

4.2 Echarts入门

4.2.1 介绍

ECharts开源来自百度商业前端数据可视化团队,基于html5 Canvas,是一个纯Javascript图表库,提供直观,生动,可交互,可个性化定制的数据可视化图表。创新的拖拽重计算、数据视图、值域漫游等特性大大增强了用户体验,赋予了用户对数据进行挖掘、整合的能力。

https://echarts.apache.org/zh/index.html

https://www.runoob.com/w3cnote/html5-canvas-eccharts.html

https://www.runoob.com/echarts/echarts-tutorial.html

4.2.2 入门案例

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>第一个 ECharts 实例</title>
    <!-- 引入 echarts.js -->
    <script src="https://cdn.staticfile.org/echarts/4.3.0/echarts.min.js"></script>
</head>
<body>
<!-- 为ECharts准备一个具备大小(宽高)的Dom -->
<div id="main" style="width: 600px;height:400px;"></div>
<script type="text/javascript">
    // 基于准备好的dom,初始化echarts实例
    var myChart = echarts.init(document.getElementById('main'));

    // 指定图表的配置项和数据
    var option = {
        title: {
            text: '第一个 ECharts 实例'
        },
        tooltip: {},
        legend: {
            data:['销量']
        },
        xAxis: {
            data: ["衬衫","羊毛衫","雪纺衫","裤子","高跟鞋","袜子"]
        },
        yAxis: {},
        series: [{
            name: '销量',
            type: 'bar',
            data: [5, 20, 36, 10, 10, 20]
        }]
    };

    // 使用刚指定的配置项和数据显示图表。
    myChart.setOption(option);
</script>
</body>
</html>

4.3 SpringBoot+Echarts实现数据可视化

4.3.1 实体类

package cn.itcast.bean;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class Result {
    private Object data;
    private Integer code;
    private String message;

    public static Result success(Object data){
        Result result = new Result();
        result.setCode(200);
        result.setMessage("success");
        result.setData(data);
        return result;
    }

    public static Result fail(){
        Result result = new Result();
        result.setCode(500);
        result.setMessage("fail");
        result.setData(null);
        return result;
    }
}

4.3.2 Mapper

package cn.itcast.mapper;

import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import org.springframework.stereotype.Component;

import java.util.List;
import java.util.Map;

@Mapper
@Component
public interface CovidMapper {

    /**
     * 查询全国疫情汇总数据
     * @param datetime
     * @return
     */
    @Select("select `datetime`, `currentConfirmedCount`, `confirmedCount`, `suspectedCount`, `curedCount`, `deadCount` from covid19_1 where datetime = #{datetime}")
    List<Map<String,Object>> getNationalData(String datetime);

    /**
     * 查询山东省内疫情数据
     * @param datetime
     * @return
     */
    @Select("select `currentConfirmedCount`, `confirmedCount`, `suspectedCount`, `curedCount`, `deadCount` from covid19_2 where provinceShortName = '山东' and datetime = #{datetime}")
    List<Map<String,Object>> getShandongData(String datetime);


    /**
     * 查询全国各省份疫情累计确诊数据
     * @param datetime
     * @return 省份名称,累计确诊数
     */
    @Select("select provinceShortName as name ,confirmedCount as value from covid19_2 where datetime = #{datetime}")
    List<Map<String, Object>> getNationalMapData(String datetime);


    /**
     * 查询全国每一天的疫情数据
      * @return
     */
    //'新增确诊', '累计确诊', '疑似病例', '累计治愈', '累计死亡'
    @Select("select dateId,confirmedIncr as `新增确诊`,confirmedCount as `累计确诊`,suspectedCount as `疑似病例`,curedCount as `累计治愈`,deadCount as `累计死亡` from covid19_3")
    List<Map<String, Object>> getCovidTimeData();

    /**
     * 查询全国各省份境外输入病例数量
     * @param datetime
     * @return
     */
    @Select("select provinceShortName as `name`,confirmedCount as `value` from covid19_4 where datetime = #{datetime} order by `value` desc limit 10")
    List<Map<String, Object>> getCovidImportData(String datetime);


    /**
     * 查询防疫物资使用情况
     * @return
     */
    //INSERT INTO `bigdata`.`covid19_wz` (`name`, `cg`, `xb`, `jz`, `xh`, `xq`, `kc`)
    //'name', '采购', '下拨', '捐赠', '消耗', '需求', '库存'
    @Select("select name , `cg` as `采购`, `xb` as `下拨`, `jz` as `捐赠`, `xh` as `消耗`, `xq` as `需求`, `kc` as `库存` from covid19_wz")
    List<Map<String, Object>> getCovidWz();

}

4.3.3 Controller

package cn.itcast.controller;

import cn.itcast.bean.Result;
import cn.itcast.mapper.CovidMapper;
import org.apache.commons.lang3.time.FastDateFormat;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;
import java.util.Map;

@RestController
//=@Controller+@ResponseBody //表示该类是SpringBoot的一个Controller,且返回的数据为Json格式
//@Controller 将当前修饰的类注入SpringBoot IOC容器,使得从该类所在的项目跑起来的过程中,这个类就被实例化。
//@ResponseBody 它的作用简短截说就是指该类中所有的API接口返回的数据,甭管你对应的方法返回Map或是其他Object,它会以Json字符串的形式返回给客户端
@RequestMapping("covid")
public class Covid19Controller {

    @Autowired
    private CovidMapper covidMapper;

    /**
     * 接收前端请求返回全国疫情汇总数据
     */
    @RequestMapping("getNationalData")
    public Result getNationalData(){
        String datetime = FastDateFormat.getInstance("yyyy-MM-dd").format(System.currentTimeMillis());
        Map<String, Object> data = covidMapper.getNationalData(datetime).get(0);
        Result result = Result.success(data);
        return result;
    }

    /**
     * 接收前端请求返回山东疫情数据
     */
    @RequestMapping("getShandongData")
    public Result getShandongData(){
        String datetime = FastDateFormat.getInstance("yyyy-MM-dd").format(System.currentTimeMillis());
        Map<String, Object> data = covidMapper.getShandongData(datetime).get(0);
        Result result = Result.success(data);
        return result;
    }

    /**
     * 查询全国各省份累计确诊数据并返回
     */
    @RequestMapping("getNationalMapData")
    public Result getNationalMapData(){
        String datetime = FastDateFormat.getInstance("yyyy-MM-dd").format(System.currentTimeMillis());
        List<Map<String, Object>> data =  covidMapper.getNationalMapData(datetime);
        return Result.success(data);
    }

    /**
     * 查询全国每一天的疫情数据并返回
     */
    @RequestMapping("getCovidTimeData")
    public Result getCovidTimeData(){
        List<Map<String, Object>> data =  covidMapper.getCovidTimeData();
        return Result.success(data);
    }

    /**
     * 查询各省份境外输入病例数量
     *
     */
    @RequestMapping("getCovidImportData")
    public Result getCovidImportData(){
        String datetime = FastDateFormat.getInstance("yyyy-MM-dd").format(System.currentTimeMillis());
        List<Map<String, Object>> data = covidMapper.getCovidImportData(datetime);
        return  Result.success(data);
    }

    /**
     * 查询各物资使用情况
     */
    @RequestMapping("getCovidWz")
    public Result getCovidWz(){
        List<Map<String, Object>> data = covidMapper.getCovidWz();
        return Result.success(data);
    }

}

4.3.4 index.html

可参考:https://www.cnblogs.com/wkfvawl/p/15851283.html

一个简单的版本

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>疫情大数据</title>
    <script src="js/echarts.js"></script>
    <script src="js/china.js"></script>
    <script src="js/jquery.js"></script>
    <link href="css/bootstrap.css" rel="stylesheet"/>
    <style>
        * {
            margin: 0;
            padding: 0
        }

        html, body {
            width: 100%;
            height: 100%;
        }

        #main {
            width: 600px;
            height: 450px;
            margin: 150px auto;
            border: 1px solid #ddd;
        }

        /*默认长宽比0.75*/
        .center {
            margin: auto;
            width: 70%;
        }
    </style>


</head>
<body>
<div>
    <h2 align="center">实时疫情大数据平台</h2>
</div>
    <br>
    <hr>
<div id="covid_all" class="center">
    <table class="table table-bordered" bgcolor="#b0e0e6">
        <thead>
        <tr>
            <th>
                时间
            </th>
            <th>
                现存确诊
            </th>
            <th>
                累计确诊
            </th>
            <th>
                现存疑似
            </th>
            <th>
                累计治愈
            </th>
            <th>
                累计死亡
            </th>
        </tr>
        </thead>
        <tbody>
        <tr>
            <td id="datetime">1</td>
            <td id="currentConfirmedCount">1</td>
            <td id="confirmedCount">1</td>
            <td id="suspectedCount">1</td>
            <td id="curedCount">1</td>
            <td id="deadCount">1</td>
        </tr>
        </tbody>
    </table>
</div>
    <br>
    <hr>
<div id="map_all" style="width: 1500px;height:600px;" class="center"></div>
    <br>
    <hr>
<div id="time_line" style="width: 1500px;height:600px;" class="center"></div>
    <br>
    <hr>
<div id="import_pie" style="width: 1500px;height:600px;" class="center"></div>
    <br>
    <hr>
<div id="covid19_wz" style="width: 1500px;height:600px;" class="center"></div>
</body>

<script type="text/javascript">

    /*--------------------全国统计数据-----------------------------*/
    $.getJSON("http://localhost:8080/covid/getNationalData", function (data) {
        var map = data.data
        $("#datetime").html(map.datetime)
        $("#currentConfirmedCount").html(map.currentConfirmedCount)
        $("#confirmedCount").html(map.confirmedCount)
        $("#suspectedCount").html(map.suspectedCount)
        $("#curedCount").html(map.curedCount)
        $("#deadCount").html(map.deadCount)
    })


    /*--------------------全国地图-----------------------------*/
    var dataList=[
        {name: '北京', value: 100},
        {name: '上海', value: randomValue()}
    ]
    var myMapChart = echarts.init(document.getElementById('map_all'));
    function randomValue() {
        return Math.round(Math.random()*1000);
    }
    myMapChart.setOption({
        title: {
            text: '全国疫情地图'
        },
        tooltip: {
            formatter:function(params,ticket, callback){
                return params.seriesName+'<br />'+params.name+':'+params.value
            }
        },
        visualMap: {
            min: 0,
            max: 1500,
            left: 'left',
            top: 'bottom',
            text: ['多','少'],
            inRange: {
                color: ['#ffe5bf', '#ffa372', '#ff7e86','#ee1216','#B22222']
            },
            show:true
        },
        geo: {
            map: 'china',
            roam: true,//不开启缩放和平移
            zoom:1.23,//视角缩放比例
            label: {
                normal: {
                    show: true,
                    fontSize:'10',
                    color: 'rgba(0,0,0,0.7)'
                }
            },
            itemStyle: {
                normal:{
                    borderColor: 'rgba(0, 0, 0, 0.2)'
                },
                emphasis:{
                    areaColor: '#AEEEEE',//鼠标悬停区域颜色
                    shadowOffsetX: 0,
                    shadowOffsetY: 0,
                    shadowBlur: 20,
                    borderWidth: 0,
                    shadowColor: 'rgba(0, 0, 0, 0.5)'
                }
            }
        },
        series : [
            {
                name: '累计确诊',
                type: 'map',
                geoIndex: 0,
                data:dataList

            }
        ]
    });

    myMapChart.on('click', function (params) {
        alert(params.name);
    });

    setTimeout(function () {
        // 异步加载json格式数据
        $.getJSON('http://localhost:8080/covid/getNationalMapData', function(data) {
            myMapChart.setOption({
                series: [{
                    data: data.data
                }]
            });
        });
    }, 1000)


    /*--------------------时间趋势折线图-----------------------------*/
    var myLineChart = echarts.init(document.getElementById("time_line"));
    myLineChart.setOption({
        title: {
            text: '疫情趋势'
        },
        tooltip: {
            trigger: 'axis'
        },
        legend: {
            data: ['新增确诊', '累计确诊', '疑似病例', '累计治愈', '累计死亡']
        },
        dataset: {
            // 这里指定了维度名的顺序,从而可以利用默认的维度到坐标轴的映射。
            dimensions: ['dateId', '新增确诊', '累计确诊', '疑似病例', '累计治愈', '累计死亡'],
            source: []
        },
        grid: {
            left: '3%',
            right: '4%',
            bottom: '3%',
            containLabel: true
        },
        toolbox: {
            feature: {
                saveAsImage: {}
            }
        },
        xAxis: {
            type: 'category',
            boundaryGap: false,
            data: []
        },
        yAxis: {
            type: 'value'
        },
        series: [
            {type: 'line'},
            {type: 'line'},
            {type: 'line'},
            {type: 'line'},
            {type: 'line'}
        ]
    });


    var xdata2 = [];//x轴
    $.getJSON('http://localhost:8080/covid/getCovidTimeData', function (data) {
        var arr = data.data
        for (var i = 0; i < arr.length; i++) {
            xdata2.push(arr[i].dateId)
        }
        myLineChart.setOption({
            dataset: {
                source: data.data
            },
            xAxis: {
                data: xdata2
            }
        })
    })



    /*--------------------境外输入饼图-----------------------------*/
    var myPieChart = echarts.init(document.getElementById("import_pie"));
    myPieChart.setOption({
        title: {
            text: '境外输入统计'
        },
        tooltip: {
            trigger: 'item',
            formatter: '{a} <br/>{b} : {c} ({d}%)'
        },
        legend: {
            type: 'scroll',
            orient: 'vertical',
            right: 10,
            top: 20,
            bottom: 20,
        },
        series : [
            {
                name: '境外输入',
                type: 'pie',    // 设置图表类型为饼图
                radius: '70%',  // 饼图的半径
                emphasis: {
                    itemStyle: {
                        shadowBlur: 10,
                        shadowOffsetX: 0,
                        shadowColor: 'rgba(0, 0, 0, 0.5)'
                    }
                },
                data:[          // 数据数组,name 为数据项名称,value 为数据项值
                    {value:235, name:'视频广告'},
                    {value:274, name:'联盟广告'},
                    {value:310, name:'邮件营销'},
                    {value:335, name:'直接访问'},
                    {value:400, name:'搜索引擎'}
                ]
            }
        ]
    })
    $.getJSON('http://localhost:8080/covid/getCovidImportData', function (data) {
        myPieChart.setOption({
            series:[{
                data: data.data
            }]
        })
    })

    /*--------------------救援物资-----------------------------*/
    // 基于准备好的dom,初始化echarts实例
    var myChart = echarts.init(document.getElementById('covid19_wz'));
    myChart.setOption({
        title: {
            text: '救援物资'
        },
        legend: {},
        tooltip: {},
        dataset: {
            // 这里指定了维度名的顺序,从而可以利用默认的维度到坐标轴的映射。
            dimensions: ['name', '采购', '下拨', '捐赠', '消耗', '需求', '库存'],
            source: []
        },
        xAxis: {
            type: 'category',
            data: []
        },
        yAxis: {},
        series: [
            {type: 'bar'},
            {type: 'bar'},
            {type: 'bar'},
            {type: 'bar'},
            {type: 'bar'},
            {type: 'bar'}
        ]
    });

    var xdata = [];//x轴
    $.getJSON("http://localhost:8080/covid/getCovidWz", function (data) {
        var arr = data.data
        for (var i = 0; i < arr.length; i++) {
            xdata.push(arr[i].name)
        }
        myChart.setOption({
            dataset: {
                source: data.data
            },
            xAxis: {
                data: xdata
            }
        })
    })

</script>
</html>
posted @ 2022-07-31 18:31  王陸  阅读(299)  评论(0编辑  收藏  举报