目录
需求分析
项目实施
1.数据采集
2.搭建flask应用
3.可视化展示
第一板块
第二板块
第三板块
第四板块
4.添加定时任务
项目总结
本项目是基于flask+echarts搭建的全国疫情实时追踪的可视化大屏,主要涉及到的技术有爬虫,mysql数据库,flask框架,echarts图表。关于flask知识点,可学习另一篇文章Flask全套知识点从入门到精通,学完可直接做项目
最终效果如下:
从最终效果图可以看出,我们将屏幕分为4大板块(页面排布是左中右+上),第一板块是最上面的部分,包括大屏标题以及当前的实时时间;第二板块是最左边,上面的全国新增趋势折线图(新增确诊、治愈、死亡),下面是全国累计趋势折线图(累计确诊、治愈、死亡);第三板块是中间,上面是当天的一些数据,下面是全国累计确诊的疫情地图;第四板块是右边,上面是新增确诊人数Top前五的省份柱状图,下面是微博热搜话题的词云图。
开发工具:
vscode编辑器
python3.8
如果你在开发的过程中发现你编写的html或css文件在页面中没有更新,而是你上次编写的代码,也就是缓存的问题,这时候你需要在app.py中添加如下代码即可解决:
@app.after_request
def apply_caching(response):
response.headers["Cache-Control"] = "no-cache"
return response
项目的最终文件目录结构如下:
首先,我们得要有数据才能进行展示,这里我们选择用爬虫来进行数据采集并保存到mysql数据库中,考虑到平台限制,这里就不方便展示爬虫代码,需要的评论留言或私信。
这里我们先搭建一个基础的flask应用
from flask import Flask,render_template
app = Flask(__name__)
app.config['JSON_AS_ASCII'] = False
@app.route('/')
def index():
return render_template('main.html')
if __name__ == '__main__':
app.run(debug=True)
接着,需要编写main.html页面(这里我就直接放最终的代码)
全国疫情实时追踪
全国疫情实时追踪
累计确诊
新增确诊
累计治愈
累计死亡
其次,我们还需要编写css来进行板块划分
main.css
body{
margin: 0;
background-color: #333;
}
.title{
position: absolute;
width: 40%;
height: 10%;
top: 0;
left: 30%;
color: white;
font-size: 40px;
display: flex;
align-items: center;
justify-content: center;
}
.l1{
position: absolute;
width: 30%;
height: 45%;
top: 10%;
left: 0;
background-color: aquamarine;
}
.l2{
position: absolute;
width: 30%;
height: 45%;
top: 55%;
left: 0;
background-color: blue;
}
.c1{
position: absolute;
width: 40%;
height: 25%;
top: 10%;
left: 30%;
/* background-color: blue; */
}
.num{
width: 25%;
float: left;
display: flex;
align-items: center;
justify-content: center;
color: gold;
font-size: 16px;
}
.txt{
width: 25%;
float: left;
display: flex;
align-items: center;
justify-content: center;
font-family: "幼圆";
color: whitesmoke;
font-size: 14px;
}
.c2{
position: absolute;
width: 40%;
height: 65%;
top: 35%;
left: 30%;
/* background-color: whitesmoke; */
}
.r1{
position: absolute;
width: 30%;
height: 45%;
top: 10%;
right: 0;
background-color: burlywood;
}
.r2{
position: absolute;
width: 30%;
height: 45%;
top: 55%;
right: 0;
background-color: brown;
}
.tim{
position: absolute;
/* width: 30%; */
height: 10%;
top: 5%;
right: 2%;
/* background-color: blueviolet; */
font-size: 20px;
color: whitesmoke;
}
接下来我将按照4大板块进行介绍
那个大屏标题文字在上面的html页面中有,这里就不说了。还有一个就是右上角的时间显示,这里我们需要编写一个获取时间的接口,然后通过ajax来发送请求进行调用.
utils.py
import time
import pymysql
import collections
import jieba
import re
def get_time():
time_str = time.strftime('%Y{}%m{}%d{} %X ')
return time_str.format('年','月','日')
app.py
import utils
@app.route('/time')
def time():
return utils.get_time()
get_data.js
function gettime(){
$.ajax({
url:'/time',
timeout:10000,//超时时间
success:function(data){
$('.tim').html(data)
},
error:function(data){
}
});
};
第二板块是左边的两个图
流程步骤就是先从数据库中获取数据,在flask应用中编写接口,最后在页面中通过ajax来发送进行调用,这是图表展示的通用步骤,下面我就不再叙述了。
这里因为我们要多次从数据库获取数据,所以 我们先封装一下方法,便于后面获取数据
utils.py
def get_conn():
"""
:return: 连接,游标
"""
# 创建连接
conn = pymysql.connect(host="127.0.0.1",
user="xxx", # 这里写你的mysql用户名
password="xxx", # 这里写你的mysql密码
db="yiqing", # 这里写你的mysql中创建的数据库
charset="utf8")
# 创建游标
cursor = conn.cursor()# 执行完毕返回的结果集默认以元组显示
return conn, cursor
def close_conn(conn, cursor):
cursor.close()
conn.close()
def query(sql,*args):
"""
封装通用查询
:param sql:
:param args:
:return: 返回查询到的结果,((),(),)的形式
"""
conn, cursor = get_conn()
cursor.execute(sql,args)
res = cursor.fetchall()
close_conn(conn, cursor)
return res
左上的图:
utils.py
def get_l1_data():
# 因为会更新多次数据,取时间戳最新的那组数据
sql = '''
SELECT ds,confirm_add,heal_add,dead_add
FROM history
'''
res = query(sql)
return res
app.py
from flask import jsonify
@app.route('/l1')
def get_l1_data():
data = utils.get_l1_data()
day,confirm_add,heal_add,dead_add = [],[],[],[]
for item in data:
day.append(item[0].strftime('%m-%d'))
confirm_add.append(item[1])
heal_add.append(item[2])
dead_add.append(item[3])
return jsonify({'day':day,'confirm_add':confirm_add,'heal_add':heal_add,'dead_add':dead_add})
get_data.js
function get_l1_data(){
$.ajax({
url:'/l1',
success:function(data){
ec_left1_Option.xAxis[0].data=data.day
ec_left1_Option.series[0].data=data.confirm_add
ec_left1_Option.series[1].data=data.heal_add
ec_left1_Option.series[2].data=data.dead_add
ec_left1.setOption(ec_left1_Option)
},
error:function(data){
}
});
}
ec_left1.js
var ec_left1 = echarts.init(document.getElementById('l1'), "dark");
var ec_left1_Option = {
tooltip: {
trigger: 'axis',
//指示器
axisPointer: {
type: 'line',
lineStyle: {
color: '#7171C6'
}
},
},
legend: {
data: ['新增确诊', '新增治愈','新增死亡'],
left: "right"
},
//标题样式
title: {
text: "全国新增趋势",
textStyle: {
color: 'white',
},
left: 'left'
},
//图形位置
grid: {
left: '4%',
right: '6%',
bottom: '4%',
top: 50,
containLabel: true
},
xAxis: [{
type: 'category',
data: []
}],
yAxis: [{
type: 'value',
//y轴线设置显示
axisLine: {
show: true
},
position:'left',
axisLabel: {
show: true,
color: 'white',
fontSize: 12,
formatter: function(value) {
if (value >= 1000) {
value = value / 1000 + 'k';
}
return value;
}
},
//与x轴平行的线样式
splitLine: {
show: true,
lineStyle: {
width: 1,
}
}
},
{
type: 'value',
//y轴线设置显示
axisLine: {
show: true
},
position:'right',
axisLabel: {
show: true,
color: 'white',
fontSize: 12,
formatter: function(value) {
return value;
}
},
//与x轴平行的线样式
splitLine: {
show: true,
lineStyle: {
width: 1,
}
}
}
],
series: [{
name: "新增确诊",
type: 'line',
smooth: true,
yAxisIndex:0,
data: []
}, {
name: "新增治愈",
type: 'line',
smooth: true,
yAxisIndex:1,
data: []
},{
name: "新增死亡",
type: 'line',
smooth: true,
yAxisIndex:1,
data: []
}
]
};
ec_left1.setOption(ec_left1_Option)
左下:
utils.py
def get_l2_data():
sql = '''
SELECT ds,confirm,heal,dead
FROM history;
'''
res = query(sql)
return res
app.py
@app.route('/l2')
def get_l2_data():
data = utils.get_l2_data()
day,confirm,heal,dead = [],[],[],[]
for item in data:
day.append(item[0].strftime('%m-%d'))
confirm.append(item[1])
heal.append(item[2])
dead.append(item[3])
return jsonify({'day':day,'confirm':confirm,'heal':heal,'dead':dead})
get_data.js
function get_l2_data(){
$.ajax({
url:'/l2',
success:function(data){
ec_left2_Option.xAxis[0].data=data.day
ec_left2_Option.series[0].data=data.confirm
ec_left2_Option.series[1].data=data.heal
ec_left2_Option.series[2].data=data.dead
ec_left2.setOption(ec_left2_Option)
},
error:function(data){
}
});
}
ec_left2.js
var ec_left2 = echarts.init(document.getElementById('l2'), "dark");
var ec_left2_Option = {
tooltip: {
trigger: 'axis',
//指示器
axisPointer: {
type: 'line',
lineStyle: {
color: '#7171C6'
}
},
},
legend: {
data: ['累计确诊', '累计治愈','累计死亡'],
left: "right"
},
//标题样式
title: {
text: "全国累计趋势",
textStyle: {
color: 'white',
},
left: 'left'
},
//图形位置
grid: {
left: '4%',
right: '6%',
bottom: '4%',
top: 50,
containLabel: true
},
xAxis: [{
type: 'category',
data: []
}],
yAxis: [{
type: 'value',
//y轴字体设置
//y轴线设置显示
axisLine: {
show: true
},
axisLabel: {
show: true,
color: 'white',
fontSize: 12,
formatter: function(value) {
if (value >= 1000) {
value = value / 1000 + 'k';
}
return value;
}
},
//与x轴平行的线样式
splitLine: {
show: true,
lineStyle: {
// color: '#FFF',
width: 1,
// type: 'solid',
}
}
},
{
type: 'value',
//y轴线设置显示
axisLine: {
show: true
},
position:'right',
axisLabel: {
show: true,
color: 'white',
fontSize: 12,
formatter: function(value) {
if (value >= 1000) {
value = value / 1000 + 'k';
}
return value;
}
},
//与x轴平行的线样式
splitLine: {
show: true,
lineStyle: {
width: 1,
}
}
} ],
series: [{
name: "累计确诊",
type: 'line',
smooth: true,
yAxisIndex:0,
data: []
}, {
name: "累计治愈",
type: 'line',
smooth: true,
yAxisIndex:1,
data: []
},{
name: "累计死亡",
type: 'line',
smooth: true,
yAxisIndex:1,
data: []
}
]
};
ec_left2.setOption(ec_left2_Option)
第三板块是中间部分
先完成上面的数值数据填充
utils.py
def get_c1_data():
"""
:return: 返回大屏div id=c1 的数据
"""
# 因为会更新多次数据,取时间戳最新的那组数据
sql = """
SELECT confirm,confirm_add,heal,dead
FROM history
ORDER BY ds DESC LIMIT 1;
"""
res = query(sql)
return res[0]
app.py
@app.route('/c1')
def get_c1_data():
data = utils.get_c1_data()
return jsonify({'confirm':int(data[0]),'confirm_add':int(data[1]),'heal':int(data[2]),'dead':int(data[3])})
get_data.js
function get_c1_data(){
$.ajax({
url:'/c1',
success:function(data){
$(".num h1").eq(0).text(data.confirm)
$(".num h1").eq(1).text(data.confirm_add)
$(".num h1").eq(2).text(data.heal)
$(".num h1").eq(3).text(data.dead)
},
error:function(data){
}
});
}
接着完成下面的地图
utils.py
def get_c2_data():
"""
:return: 返回各省数据
"""
# 因为会更新多次数据,取时间戳最新的那组数据
sql = '''
SELECT province,sum(confirm_now)
FROM details
GROUP BY province;
'''
res = query(sql)
return res
app.py
@app.route('/c2')
def get_c2_data():
res = []
for item in utils.get_c2_data():
res.append({'name':item[0],'value':int(item[1])})
return jsonify({'data':res})
get_data.py
function get_c2_data(){
$.ajax({
url:'/c2',
success:function(data){
ec_center_option.series[0].data=data.data
ec_center_option.series[0].data.push({
name:"南海诸岛",value:0
})
ec_center.setOption(ec_center_option)
},
error:function(data){
}
});
}
ec_center.js
const ec_center_option = {
tooltip: {
trigger: 'item',
formatter: '名称:{a}
省份:{b}
确诊人数:{c}'
},
//左侧小导航图标
visualMap: {
show: true,
x: 'left',
y: 'bottom',
textStyle: {
fontSize: 8,
color:['#FFFFFF']
},
splitList: [{ start: 0,end: 9 },
{start: 10, end: 99 },
{ start: 100, end: 999 },
{ start: 1000, end: 9999 },
{ start: 10000 }],
color: ['#8A3310', '#C64918', '#E55B25', '#F2AD92', '#F9DCD1']
},
series: [
{
name: '数据',
type: 'map',
mapType: 'china',
roam: false,
itemStyle: {
normal: {
borderWidth: .5, //区域边框宽度
borderColor: '#62d3ff', //区域边框颜色
areaColor: "#b7ffe6", //区域颜色
label: { show: true }
},
emphasis: { //鼠标滑过地图高亮的相关设置
borderWidth: .5,
borderColor: '#fff',
areaColor: "#fff",
label: { show: true }
}},
data: [] // data_list
}
]
};
ec_center = echarts.init(document.getElementById('main'));
ec_center.setOption(ec_center_option)
到这里第三板块就完成了!
首先完成上面的柱状图
utils.py
def get_r1_data():
"""
:return: 返回新增确诊人数前5名的省份
"""
sql = '''
SELECT province,confirm FROM
(select province ,sum(confirm_add) as confirm from details
where update_time=(select update_time from details
order by update_time desc limit 1)
group by province) as a
ORDER BY confirm DESC LIMIT 7;
'''
res = query(sql)
return res
app.py
@app.route('/r1')
def get_r1_data():
name,value = [],[]
for n,v in utils.get_r1_data()[2:]:
name.append(n)
value.append(int(v))
return jsonify({'name':name,'value':value})
get_data.js
function get_r1_data(){
$.ajax({
url:'/r1',
success:function(data){
ec_right1_option.xAxis.data=data.name
ec_right1_option.series[0].data=data.value
ec_right1.setOption(ec_right1_option)
}
});
}
ec_right1.js
var ec_right1 = echarts.init(document.getElementById('r1'),"dark");
var ec_right1_option = {
//标题样式
title : {
text : "新增确诊人数TOP5",
textStyle : {
color : 'white',
},
left : 'left'
},
color: ['#3398DB'],
tooltip: {
trigger: 'axis',
axisPointer: { // 坐标轴指示器,坐标轴触发有效
type: 'shadow' // 默认为直线,可选为:'line' | 'shadow'
}
},
xAxis: {
type: 'category',
color : 'white',
data: []
},
yAxis: {
type: 'value',
color : 'white',
},
series: [{
data: [],
type: 'bar',
barMaxWidth:"50%"
}]
};
ec_right1.setOption(ec_right1_option)
接着完成下面的词云图
utils.py
def get_r2_data():
"""
:return: 返回最近的20条热搜
"""
sql = 'select title from focu_news order by news_time desc limit 20'
res = query(sql)
all_word = ''
for item in res:
all_word += item[0]
new_data = re.findall('[\u4e00-\u9fa5]+', all_word, re.S)
new_data = "/".join(new_data)
seg_list_exact = jieba.cut(new_data, cut_all=True)
result_list = []
with open('停用词库.txt', encoding='utf-8') as f:
con = f.readlines()
stop_words = set()
for i in con:
i = i.replace("\n", "") # 去掉读取每一行数据的\n
stop_words.add(i)
for word in seg_list_exact:
if word not in stop_words and len(word) > 1:
result_list.append(word)
word_counts = collections.Counter(result_list)
# 词频统计:获取前80最高频的词
word_counts_top = word_counts.most_common(80)
return word_counts_top
app.py
@app.route('/r2')
def get_r2_data():
data = utils.get_r2_data()
d = []
for i in data:
k = i[0]
v = int(i[1])
d.append({"name": k, "value": v})
return jsonify({"kws": d})
get_data.py
function get_r2_data() {
$.ajax({
url: "/r2",
success: function (data) {
ec_right2_option.series[0].data=data.kws;
ec_right2.setOption(ec_right2_option);
}
})
}
ec_right2.js
var ec_right2 = echarts.init(document.getElementById('r2'), "dark");
var ec_right2_option = {
// backgroundColor: '#515151',
title: {
text: "微博热搜话题",
textStyle: {
color: 'white',
},
left: 'left'
},
tooltip: {
show: false
},
series: [{
type: 'wordCloud',
// drawOutOfBound:true,
gridSize: 1,
sizeRange: [12, 55],
rotationRange: [-45, 0, 45, 90],
// maskImage: maskImage,
textStyle: {
normal: {
color: function () {
return 'rgb(' +
Math.round(Math.random() * 255) +
', ' + Math.round(Math.random() * 255) +
', ' + Math.round(Math.random() * 255) + ')'
}
}
},
// left: 'center',
// top: 'center',
// // width: '96%',
// // height: '100%',
right: null,
bottom: null,
// width: 300,
// height: 200,
// top: 20,
data: []
}]
}
ec_right2.setOption(ec_right2_option);
到这里,全部的页面及渲染就编写完成!
这里忘记说了,前面每次在get_data.js中编写的函数,最后要调用才能使用
get_c1_data()
get_c2_data()
get_l1_data()
get_l2_data()
get_r1_data()
get_r2_data()
这里我们还需要给定义发送请求的ajax函数设置定时,也就是在get_data.js里面添加
setInterval(gettime,1000) # 时间是1s获取一次
setInterval(get_c1_data,1000*60*60) # 1小时发送一次请求
setInterval(get_c2_data,1000*60*60*6)
setInterval(get_l1_data,1000*60*60*12)
setInterval(get_l2_data,1000*60*60*12)
setInterval(get_r1_data,1000*60*60*6)
setInterval(get_r2_data,1000*10)
本项目适合flask初学者来进行练手,当然前提也要会一些前端的知识,关于Echarts的使用可以去官网进行学习。关于页面的布局,可自由发挥来进行设计,或者在此基础上来进行创新,开发新的功能。