一、效果图
二、技术栈
- 基础框架(springboot + mybatis + mybatis-plus)
- 缓存数据(redis)
- 国际化(一键切换不同语种)
- 定时任务(定时更新数据)
- 爬虫
- 加密
- 感知数据变更,进行通知推送
- 日志监控
- 数据计算和二次存储(为图表服务 - 比如趋势图)
- 数据展示和渲染 Thymeleaf Echarts
- 模拟http请求
三、项目背景
模拟疫情数据展示网站,做出一个完整的数据采集、数据存储、数据计算、数据展示的疫情数据系统。
四、搜索引擎原理
链接人和内容
网页爬取 -》 网页去重 -》 网页分析 -》 内容保存(倒排索引)
关键字查询 -》 关键字分析 -》 去匹配内容 -》 筛选出比如100条数据 -》 数据排序 (可口可乐的秘方)
分类:
通用型爬虫 和 垂直型爬虫
疫情数据系统:
数据采集 -》 数据存储 -》 数据计算 -》 数据展示
五、数据分析
5.1 分析数据源
确认能够通过代码 获取到数据 (定位到具体的http请求)
5.2 获取疫情数据
- 国内各省份表格数据-对应请求
- 对应的数据格式:json
- 国外表格数据-对应请求
- 国内趋势数据
六、数据处理
6.1 初识json
json = javascript object notation (js对象表示法)
独立于语言,具有自我描述性
Gson -> From Google
new Gson().toJson(Object obj) 将对象转化为json字符串
new Gson().fromJson(String jsonStr, T.class) 将json字符串转化为对象
6.2 数据展示
controller - service - handler
数据加载到model中 返回给html渲染
@Controller public class DataController { @Autowired private DataService dataService; @GetMapping("/") public String list(Model model) { List<DataBean> beanList = dataService.list(); model.addAttribute("beanList", beanList); return "list"; } }
public interface DataService { List<DataBean> list(); } @Service public class DataServiceImpl implements DataService { @Override public List<DataBean> list() { return MyDataHandler.getData(); } }
<!DOCTYPE html> <html lang="en" xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <h2> 国内疫情情况如下 </h2> <br> <table> <thead> <tr> <th>地区</th> <th>现有</th> <th>累计</th> <th>治愈</th> <th>死亡</th> </tr> </thead> <tbody> <tr th:each="bean:${beanList}"> <td th:text="${bean.area}">name</td> <td th:text="${bean.nowConfirm}">nowConfirm</td> <td th:text="${bean.confirm}">confirm</td> <td th:text="${bean.heal}">heal</td> <td th:text="${bean.dead}">dead</td> </tr> </tbody> </table> </body> </html>
七、网络请求
HTTP
- “应用层协议”
- 了解HTTP不同版本的演进
- 了解GET和POST请求的区别
// 需要的参数url // 创建一个远程的连接对象 设置方法类型 GET // 设置相关参数 发送请求 // 通过io接收数据后返回 public static String doGet(String urlStr) { HttpURLConnection conn = null; InputStream is = null; BufferedReader br = null; StringBuilder result = new StringBuilder(); try { // 创建远程url连接对象 URL url = new URL(urlStr); // 打开一个连接 conn = (HttpURLConnection) url.openConnection(); // 设置为GET请求 conn.setRequestMethod("GET"); // 设置重要的参数 连接超时时间 和 读取超时时间 // 超时时间 更多被距离影响 读取时间 更多被数据量影响 conn.setConnectTimeout(15000); conn.setReadTimeout(60000); // header参数设置 可以不设置 conn.setRequestProperty("Accept", "application/json"); // 发送请求 conn.connect(); // 状态码 200 302 404 500 ? // 如果比较时 可能出现空指针 把确定的值放在前面 可以避免 if (200 == conn.getResponseCode()) { is = conn.getInputStream(); br = new BufferedReader(new InputStreamReader(is, "UTF-8")); String line; while ((line = br.readLine()) != null) { result.append(line); } } else { System.out.println("error responseCode :" + conn.getResponseCode()); } } catch (Exception e) { e.printStackTrace(); } finally { try { if (br != null) { br.close(); } if (is != null) { is.close(); } } catch (Exception e) { e.printStackTrace(); } conn.disconnect(); } return result.toString(); }
八、数据存储
8.1 Mybatis整合
SSM的整合 -> 入门课的整合 -> mybatis-plus的整合
明确数据结构 -> 创建mapper文件夹 -> 调用getData方法 将数据据存储到数据库中 -> 查询时从数据库中读取
a) 引入依赖
<!-- mybatis整合 mybatis-spring-boot-starter mybatis-plus -boot-starter mysql驱动--> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.1.1</version> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.2.0</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency>
b) 配置数据库连接参数
spring.datasource.url=jdbc:mysql://localhost:3306/illness?characterEncoding=utf8 spring.datasource.username=root spring.datasource.password=123456
c) 创建数据库
CREATE DATABASE /*!32312 IF NOT EXISTS*/`illness` /*!40100 DEFAULT CHARACTER SET utf8 */; USE `illness`; /*Table structure for table `epidemic` */ DROP TABLE IF EXISTS `epidemic`; CREATE TABLE `epidemic` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `area` varchar(100) DEFAULT NULL, `confirm` int(11) DEFAULT NULL, `now_confirm` int(11) DEFAULT NULL, `dead` int(11) DEFAULT NULL, `heal` int(11) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=308 DEFAULT CHARSET=utf8;
d) 编写类及文件夹
将实体类对应到数据库的表中 属性和字段也需对应
需注意 属性的命名为驼峰命名法
如 nowConfirm 字段的命名是下划线分隔 如 now_confirm
可以使用@TableName和@TableField等注解指定对应关系
@Data @NoArgsConstructor @AllArgsConstructor @TableName("epidemic") public class DataBean implements Serializable { // Alt + Insert 调用 安装插件 private static final long serialVersionUID = 4938260405189292371L; // Alt + 7 查看类的方法 // 地区 累计确诊人数 现有确诊人数 死亡人数 治愈人数 private long id; private String area; // @TableField("all_confirm") // 如果属性名和表中字段名 不一致 可以通过 TableField注解指定 private int confirm; private int nowConfirm; private int dead; private int heal; }
在主程序类上 增加@MapperScan注解 指定mapper类所在文件夹
在此文件夹下 创建mapper类 实战中 如表名叫user mapper会命名为UserMapper
@SpringBootApplication @MapperScan("com.duing.mapper") public class DataHandlerApplication { public static void main(String[] args) { SpringApplication.run(DataHandlerApplication.class, args); } } public interface DataMapper extends BaseMapper<DataBean> { }
创建service接口及其实现类
整合mybatis-plus,用其提供的IService父接口 和 ServiceImpl实现父类
public interface DataService extends IService<DataBean> { } @Service public class DataServiceImpl extends ServiceImpl<DataMapper,DataBean> implements DataService { }
在controller中调用mybatis-plus实现的CRUD方法
@Controller public class DataController { @Autowired private DataService dataService; @GetMapping("/") public String list(Model model) { List<DataBean> beanList = dataService.list(); model.addAttribute("beanList", beanList); return "list"; } }
8.2 数据初始化及定时更新
在数据查询前,先将数据存储到数据库中,我们称之为数据初始化
可以在项目启动时,先采集一次数据存储到数据库中,然后再进行定期更新
而项目启动时执行且只执行一次的逻辑,可以使用注解 @PostConstruct
// 先将DataHandler托管到spring容器中 使用@Component // 以便于获取到 dataService对象 @Component public class DataHandler { @Autowired private DataService dataService; // 数据初始化 // 在服务器加载Servlet时运行 且 只运行一次 @PostConstruct public void saveData() { List<DataBean> dataBeans = getData(); // mybatis-plus提供了可用的方法 // 删除全部数据 批量新增数据 dataService.remove(null); dataService.saveBatch(dataBeans); } ... }
定时更新其实也是定时任务
可以通过注解@Scheduled + cron表达式来实现
Scheduled 英文原意是调度的意思 意思是我们将某段逻辑 按照指定的时间间隔 进行调度 即为定时处理
使用方式如下:
首先在入口类上 打开定时任务开关 使用注解 @EnableScheduling
@SpringBootApplication @MapperScan("com.duing.mapper") @EnableScheduling public class DataHandlerApplication { public static void main(String[] args) { SpringApplication.run(DataHandlerApplication.class, args); } }
然后在方法上 使用 @Scheduled 搭配cron表达式 决定方法多久执行一次
// 定时更新 @Scheduled(cron = "0 0 0/1 * * ? ") public void updateData(){ System.out.println("要更新数据啦"); System.out.println("当前时间:" + dateFormat.format(new Date())); List<DataBean> dataBeans = getData(); dataService.remove(null); dataService.saveBatch(dataBeans); }
其中cron表达式,是由6个表达不同时间单位的字段拼接而成
可以通过在线cron表达式生成器来生成
cron表达式几乎可以满足所有定时执行的需求
可以生成各类需求对应的表达式熟悉一下
除此之外,@Scheduled还支持固定时间间隔的参数设置
分别为 fixedRate 和 fixedDelay
// 固定频率任务 以ms为单位 10000代表每10s执行一次 // 使用时要注意间隔时间 和 任务消耗时间的 大小关系 // 如设置间隔10s 而方法需执行20s 那么方法会等待上一次执行完成才会执行 // 唤起方法真正的时间间隔为20s @Scheduled(fixedRate = 10000) public void updateData1(){} // 固定间隔任务 上一次执行结束后 再隔10s进行下一次执行 // 如设置间隔10s 而方法需执行20s 那么会从上一次执行完成后开始计算 10s后开始下一次执行 // 唤起方法真正的时间间隔为30s @Scheduled(fixedDelay = 10000) public void updateData2(){}
九、图形化数据处理
9.1 折线图
<!DOCTYPE html> <html lang="en" xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>Title</title> <script type="text/javascript" src="js/echarts.min.js"></script> </head> <body> <!-- 为ECharts准备一个具备大小(宽高)的Dom --> <div id="main" style="width: 600px;height:400px;"></div> <script th:inline="javascript"> var dateStr = [[${dateList}]]; var confirmStr = [[${confirmList}]]; var suspectStr = [[${suspectList}]]; // 基于准备好的dom 初始化实例 var mychart = echarts.init(document.getElementById("main")); var option = { // 标题 title:{ text: '全国疫情新增趋势' }, legend:{ data:['新增确诊','新增疑似'] }, // x轴的数据 xAxis:{ data: dateStr }, // y轴的数据类型 yAxis:{ type:'value' }, series:[ { name: '新增确诊', type: 'line', data: confirmStr }, { name: '新增疑似', type: 'line', data: suspectStr } ] }; // 将参数设置进去 mychart.setOption(option); </script> </body> </html>
9.2 地图
<!DOCTYPE html> <html lang="en" xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>Title</title> <script type="text/javascript" src="js/echarts.min.js"></script> <script type="text/javascript" src="js/china.js"></script> </head> <body> <!-- 为ECharts准备一个具备大小(宽高)的Dom --> <div id="main" style="width: 1000px;height:700px;"></div> <script th:inline="javascript"> var dataStr1 = [[${mapData1}]]; var dataStr2 = [[${mapData2}]]; // 基于准备好的dom 初始化实例 var mychart = echarts.init(document.getElementById("main")); var option = { title: { text: '疫情地图', subtext: '仅供参考', x: 'center' }, tooltip: { trigger: 'item' }, legend: { orient: 'vertical', left: 'left', data: ['现有确诊', '累计确诊'] }, visualMap: { type: 'piecewise', pieces: [ {min: 10000, max: 1000000, label: '10000人及以上', color: '#de1f05'}, {min: 1000, max: 9999, label: '1000-9999人', color: '#ff2736'}, {min: 500, max: 999, label: '500-999人', color: '#ff6341'}, {min: 100, max: 499, label: '100-499人', color: '#ffa577'}, {min: 10, max: 99, label: '10-99人', color: '#ffcea0'}, {min: 1, max: 9, label: '1-9人', color: '#ffe7b2'}, {min: 0, max: 0, label: '0人', color: '#e2ebf4'}, ], calculate: true }, series: [ { name: '现有确诊', type: 'map', mapType: 'china', roam: false, label: { normal: { position: 'center', show: true } }, data: JSON.parse(dataStr1) }, { name: '累计确诊', type: 'map', mapType: 'china', roam: false, label: { normal: { position: 'center', show: true } }, data: JSON.parse(dataStr2) } ] }; // 将参数设置进去 mychart.setOption(option); </script> </body> </html>
9.3 拓展部分
我们可以根据国内表格数据的处理方式、以及图形化数据的处理方式,举一反三的去处理海外数据,只要掌握了处理流程,其实是可以模拟腾讯新闻,将全部数据展示出来的,要注意的是,我们的项目是为了练习springboot+mybatis+thymeleaf+echarts的使用,了解http请求+json数据处理+定时任务等等功能是如何实现的,那么对数据采集方面要慎重,尽量只采集需要的少部分数据,并且控制请求次数和请求频率,一定要做到“友好访问”。
十、页面数据处理
Jsoup(Ji soup 鸡汤)----页面解析器
解析一个HTML文档的方式如下: String html = "<html><head><title>First parse</title></head>" + "<body><p>Parsed HTML into a doc.</p></body></html>"; Document doc = Jsoup.parse(html); 解析器能够尽最大可能从你提供的HTML文档中创建出一个干净的解析结果,无论HTML的格式是否完整。 比如它可以处理: 1)没有关闭的标签 <p>Lorem <p>Ipsum 可以解析成 <p>Lorem</p> <p>Ipsum</p> 2)隐式标签 (补充没有显示的标签) 它可以自动将 <td>Table data</td> 包装成 <table><tr><td>... 3)创建可靠的文档结构 html标签包含head 和 body,且在head只出现恰当的元素 文档的对象模型 1)文档由多个Elements和TextNodes组成 2)继承结构如下:Document继承Element继承Node. TextNode继承 Node. 3)一个Element包含一系列的子节点,并拥有一个父Element。他们还唯一提供了一个子元素过滤列表。
十一、国际化
根据不同的用户群,显示不同的语言,此功能叫做国际化,如丁香医生页面切换语种的功能。
要想实现动态显示不同语言,首先要将页面中的数据转为动态显示,放在配置文件中读取
创建list.properties
list.title=步尔斯特 list.h2=国内疫情情况如下 list.table.name1=地区 list.table.name2=现有确诊 list.table.name3=累计确诊 list.table.name4=治愈 list.table.name5=死亡
我们将list.properties放在i18n的文件夹下,然后在application.properties中设置好指定配置
spring.messages.basename=i18n.list
这样就可以再创建一个list_en_US.properties文件,内容如下
list.title=burst list.h2=As Follows list.table.name1=Area list.table.name2=Now Confirm list.table.name3=Confirm list.table.name4=Heal list.table.name5=Dead
对应的list.html页面修改如下
<!DOCTYPE html> <html lang="en" xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title th:text="#{list.title}">Title</title> <link rel="stylesheet" th:href="@{css/bootstrap.css}"> </head> <body> <h2 th:text="#{list.h2}"> 国内疫情情况如下 </h2> <br> <table class="table table-hover"> <thead> <tr> <th><p th:text="#{list.table.name1}">地区</p></th> <th><p th:text="#{list.table.name2}">现有</p></th> <th><p th:text="#{list.table.name3}">累计</p></th> <th><p th:text="#{list.table.name4}">治愈</p></th> <th><p th:text="#{list.table.name5}">死亡</p></th> </tr> </thead> <tbody> <tr th:each="bean:${beanList}"> <td th:text="${bean.area}">name</td> <td th:text="${bean.nowConfirm}">nowConfirm</td> <td th:text="${bean.confirm}">confirm</td> <td th:text="${bean.heal}">heal</td> <td th:text="${bean.dead}">dead</td> </tr> </tbody> </table> </body> </html>
当我们运行项目后,通过浏览器更改语言,就可以切换成不同语种的显示了。
以chrome为例,将中文置顶,则显示中文,将英语(美国)置顶,则显示英文,这里要注意,如果没有英语(美国)这个语言,可以在底下的添加语言中,先搜索后添加。
那是如何实现切换的呢,当我们调出控制台,比对两笔请求的http,可以发现请求头中Accept-Language参数是不同的,请求中文时以zh-CN开头,请求英文时以en-US开头,如果你对其他语种感兴趣,也可以看一下其他语种的简称。
但是通过浏览器切换仍有些麻烦,所以我们尝试在页面中添加切换按钮
当我们设置了参数lan用来区分不同语种时,需要使用一个处理器来接收语种参数
然后将处理器注入到spring容器中
同时增加一个list_zh_CN.properties文件,声明各字段的中文值
再次启动项目,就可在页面中进行中英文切换了。
十二、Redis的使用
三大客户端框架: Jedis Redission Lettuce
其中spring-data-redis 是将redis整合在spring中使用的jar包
在springboot2.0以后的版本 将原本的jedis升级成Lettuce 而本质上是一种连接池的封装
redis数据的存储,可以在项目启动时存储,另外可以在请求第一次被调用时存储(懒加载)
请求 -》 redis(存在 -》 直接返回,不存在 -》 mysql中查询 -》 存储到redis中 -》 返回)
-》 mysql(存在 ,不存在 -》 发送http请求 -》 数据清洗和计算后存储 -》 返回)
以折线图数据为例
在处理数据的过程中,有三种数据形态
1)url直接返回的原始数据
2)第一次解析后处理的 List<GraphBean>
3) echarts的直观需要的三个list, 一个对应x轴,两个对应y轴
第一种数据重要,但一般不放在redis中存储,甚至不放在mysql中。