可视化练习二
4.1新建Flask可视化项目
项目命名为ViewData, 项目中新建文件夹app/models存放数据库映射类,app/static/js存放js的外部工具包,app/templates/main存放html前端页面,app/templates/errors存放404之后的html前端页面,app/views存放SQLAlchemy的语法的数据库访问类。
4.2 新建app/init.py
新建flask的初始化类,自定义函数def create_app(config_name)封装一个方法,专门用于创建Flask实例;自定义函数def config_errorhandler(app)编写访问失败之后的404页面的跳转设置。
from flask import Flask, render_template
from app.config import config
from app.extensions import config_extensions
from app.views import config_blueprint
# 封装一个方法,专门用于创建Flask实例
def create_app(config_name): # development
# 创建应用实例
app = Flask(__name__)
# 初始化配置
app.config.from_object(config.get(config_name) or config['default'])
# 调用初始化函数
config[config_name].init_app(app)
# 配置扩展
config_extensions(app)
# 配置蓝本
config_blueprint(app)
# 错误页面定制
config_errorhandler(app)
# 返回应用实例
return app
def config_errorhandler(app):
# 如果在蓝本定制,只针对本蓝本的错误有效,
# 可以使用app_errorhandler定制全局有效的错误显示
@app.errorhandler(404)
def page_not_found(e):
return render_template('errors/404.html')
4.3新建app/config.py
新建flask连接数据库的配置类,将flask框架连接数据库的配置编写成函数,包括数据库的ip、端口、用户名、密码等。
import os
base_dir = os.path.abspath(os.path.dirname(__file__))
# 通用配置
class Config:
# 秘钥
SECRET_KEY = os.environ.get('SECRET_KEY') or '123456'
# 数据库
SQLALCHEMY_COMMIT_ON_TEARDOWN = True
SQLALCHEMY_TRACK_MODIFICATIONS = False
# 额外的初始化操作,即使什么内容都没有写,也是有意义的
@staticmethod
def init_app(app):
pass
# 开发环境 语法:mysql+pymysql://用户名:密码@ip:端口/数据库名
class DevelopmentConfig(Config):
SQLALCHEMY_DATABASE_URI = 'mysql+pymysql://root:root@localhost:3306/visiondata'
# 测试环境
class TestingConfig(Config):
SQLALCHEMY_DATABASE_URI = 'mysql+pymysql://root:root@localhost:3306/visiondata'
# 生产环境
class ProductionConfig(Config):
SQLALCHEMY_DATABASE_URI = 'mysql+pymysql://root:root@localhost:3306/visiondata'
# 配置字典
config = {
'development': DevelopmentConfig,
'testing': TestingConfig,
'production': ProductionConfig,
'default': DevelopmentConfig
}
4.4 新建app/ extensions.py
导入flask相关模块,创建SQLAlchemy对象并初始化Bootstrap。自定义函数def config_extensions(app)完成SQLAlchemy和Bootstrap的初始化
# 导入类库
from flask_bootstrap import Bootstrap
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate
from flask_moment import Moment
# 创建对象
bootstrap = Bootstrap()
db = SQLAlchemy()
moment = Moment()
migrate = Migrate(db=db)
# 初始化
def config_extensions(app):
bootstrap.init_app(app)
db.init_app(app)
moment.init_app(app)
migrate.init_app(app)
4.5 编辑app/models/表名.py
编写flask的数据库表的映射类,每一个表格对应一个文件,映射类命名与表名一致。
# bigdata_work.oy
from app.extensions import db
class BigDataWork(db.Model):
__tablename__ = 'bigdata_work'
id = db.Column(db.Integer, primary_key=True)
job_name = db.Column(db.String(255))
company_name = db.Column(db.String(255))
city = db.Column(db.String(255))
job_require = db.Column(db.Text)
recruit_number = db.Column(db.String(255))
money = db.Column(db.String(255))
skill_require = db.Column(db.String(255))
release_date = db.Column(db.String(255))
sex = db.Column(db.String(255))
company_detail = db.Column(db.String(255))
education = db.Column(db.String(255))
# skill_require.py
from app.extensions import db
class SkillRequire(db.Model):
__tablename__ = 'skill_require'
id = db.Column(db.Integer, primary_key=True)
skill = db.Column(db.String(255))
frequency = db.Column(db.Integer)
4.6编辑app/models/init.py
编写flask的访问数据库的初始化文件,导入数据库的映射表类
from app.extensions import db
from .bigdata_work import BigDataWork
from .skill_require import SkillRequire
4.7 新建app/static/js
将html前段需要用到的js工具包存放在js文件夹下。
4.8 新建ViewData/mamage.py
新建flask启动类,创建flask实例和数据库迁移命令,以及程序的入口。
import os
from app import create_app
from flask_script import Manager, Server
from flask_migrate import MigrateCommand
# 获取配置
config_name = os.environ.get('FLASK_CONFIG') or 'default'
# 创建Flask实例
app = create_app(config_name)
# 创建命令行启动控制对象
manager = Manager(app)
manager.add_command("runserver", Server(use_debugger=True))
# 添加数据库迁移命令
manager.add_command('db', MigrateCommand)
# 启动项目
if __name__ == '__main__':
manager.run()
4.9 新建app/views/init.py
新建flask的蓝本配置初始化类,进行蓝本的配置,自定义函数def config_blueprint(app)以及封装函数完成蓝本的注册。
from .main import main
# 蓝本配置
DEFAULT_BLUEPRINT = (
# 蓝本,前缀
(main, ''),
)
# 封装函数,完成蓝本注册
def config_blueprint(app):
for blueprint, prefix in DEFAULT_BLUEPRINT:
app.register_blueprint(blueprint, url_prefix=prefix)
4.10 新建app/views的main.py
(1)分析并统计招聘数量"大数据"相关职位招聘数量,分别使用柱状图表达,同时在网页后台输出相关数据打印语句。
数据源:数据库visiondata中bigdata_work
在app/views下的main.py添加函数def get_big_data_work(),使用SQLAlchemy语法编写sql的查询语句,进行相同职位进行数量汇总,"大数据"相关职位招聘数量比较
(2)请根据数据库表格bigdata_work,以招聘大数据相关岗位数量为依据绘制散点热度地图。同时在网页后台输出相关数据打印语句。
数据源:数据库visiondata中bigdata_work表
在app/views下的main.py添加函数def get_city_work(),使用SQLAlchemy语法编写sql的查询语句,对相同工作地点进行数量汇总,各城市"大数据"相关职位招聘数量。
(3)请统计2018年,2019年各月份大数据相关职位的招聘数量,以左右分布的(左侧2018年度,右侧2019年度)饼图进行表达
数据源:数据库visiondata中avg_money_city和bigdata_work
在app/views下的main.py添加函数get_month_work(),使用SQLAlchemy语法编写sql的查询语句,查询大数据相关职位招聘数量(按月分组)
(4)请根据指定表中的数据,分析各知识技能在某个招聘岗位能力需求中的占比情况,通过词云图例进行呈现。
数据源:数据库visiondata中skill_require
在app/views下的main.py添加函数def get_skill_require(),使用SQLAlchemy语法编写sql的查询语句,查询大数据相关职位技能标签词云。
import json
from flask import Blueprint, render_template, jsonify
from app.models import BigDataWork, SkillRequire
from app.extensions import db
from sqlalchemy import *
main = Blueprint('main', __name__) # 实例化路由
@main.route('/')
def index():
return render_template('/main/echarts.html') # 当url访问 / 直接跳转到主页
@main.route('/index/') # url访问 /index/ 跳转主页 调用display方法 将查询结果通过render_template()传入html页面
def display():
rs_gmw = get_month_work()
rs_gsr = get_skill_require()
rs_gcw = get_city_work()
return render_template('/main/echarts.html', rs_gmw=rs_gmw, rs_gsr=rs_gsr, rs_gcw=rs_gcw)
# 对相同工作地点进行数量汇总,各城市"大数据"相关职位招聘数量 获取的结果 ('上海', 2239)
def get_city_work():
# sql: select job_name,city,count(job_name) from bigdata_work GROUP BY job_name,city
gcw_rs = db.session.query(BigDataWork.city, func.count('*').label('job_count')).group_by(BigDataWork.city).all()
print('各城市"大数据"相关职位招聘数量:' + str(gcw_rs))
return gcw_rs
# "大数据"相关职位招聘数量(按月分组)
def get_month_work():
# sql: select release_date,count(id),DATE_FORMAT(release_date,'%m月') from bigdata_work group by DATE_FORMAT(release_date,'%Y%m') HAVING year(release_date)=2018
# func.count(BigDataWork.id).label('job_count') 调用count行数 统计 id的总数 映射名为 job_name
# func.DATE_FORMAT(BigDataWork.release_date, '%m月').label('month')) date_format函数 获取月份 映射名为 month
gmw_rs_2018 = db.session.query(BigDataWork.release_date, func.count(BigDataWork.id).label('job_count'),
func.DATE_FORMAT(BigDataWork.release_date, '%m月').label('month'))\
.group_by(func.DATE_FORMAT(BigDataWork.release_date, '%Y%m'))\
.having(func.year(BigDataWork.release_date) == 2018).all()
gmw_rs_2019 = db.session.query(BigDataWork.release_date, func.count(BigDataWork.id).label('job_count'),
func.DATE_FORMAT(BigDataWork.release_date, '%m月').label('month'))\
.group_by(func.DATE_FORMAT(BigDataWork.release_date, '%Y%m'))\
.having(func.year(BigDataWork.release_date) == 2019).all()
# 将两个查询结果赋值给一个新的数据返回到前端
gmw_rs = [gmw_rs_2018, gmw_rs_2019]
print('"大数据"相关职位招聘数量(按月分组):' + str(gmw_rs))
return gmw_rs
# "大数据"相关职位技能标签词云 获取结果
def get_skill_require():
# sql: select * from skill_require
gsr_rs = SkillRequire.query.all()
print('"大数据"相关职位技能标签词云 获取结果成功')
return gsr_rs
4.11 新建app/templates/errors/404.html
编写url访问失败之后的提示页面。
4.12 新建app/templates/main/echarts.html
新建/编辑app/templates/main/目录下的ehcarts.html文件,编写js获取到flask传过来的数据,通过echarts组件进行结果的展示。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>招聘信息统计</title>
<!-- 警告:这里一定要先引入ehcarts.js再引入china.js 不然会报错:ECharts is not Loaded -->
<script type="text/javascript" src="/static/js/echarts.js"></script>
<script type="text/javascript" src="/static/js/china.js"></script>
<script type="text/javascript" src="/static/js/jquery.min.js"></script>
<script type="text/javascript" src="/static/js/js2wordcloud.js"></script>
</head>
<style>
html , body , .content {
width:100%;
height:100%;
padding: 0;
margin: 0;
box-sizing: border-box;
background-color: #ccc;
}
.content {
padding: 40px;
}
.header {
height: 10%;
width: 100%;
font-size: 24px;
font-weight: 700;
line-height: 60px;
text-align: center;
}
.body {
height: 100%;
width: 100%;
text-align: center;
}
.chartBox {
width: 100%;
height: 60%;
margin-bottom:40px;
}
</style>
<body>
<div class="content">
<div class="header"></div>
<div class="body">
<div class="chartBox" id="heatmap"></div>
<div class="chartBox" id="mothWork"></div>
<p>
大数据相关职位技能</p>
<div class="chartBox" id="wordCloud"></div>
</div>
</div>
</body>
<script>
//散点地图
var heatMap = echarts.init(document.getElementById('heatmap'));
var city_data = [{% for r in rs_gcw %}{name:"{{r.city}}", value:"{{r.job_count}}"},{% endfor %}]
var max_data = [{% for r in rs_gcw %}"{{r.job_count}}",{% endfor %}]
var dataIntArr_max = []; ///下面的函数用于将元素为字符串的数组转成元素为整型的数组
max_data.forEach(function(data){
dataIntArr_max.push(+parseInt(data)); ///遍历数组 将每个元素变成整型
});
var max = Math.max.apply(null, dataIntArr_max); ////求得数组中最大的值 然后赋值给热力图作为热力图的最大值
console.log("招聘“大数据”相关职位数量最大为" + max);
//各大城市的地点坐标
var geoCoordMap = {
"海门":[121.15,31.89],"鄂尔多斯":[109.781327,39.608266],"招远":[120.38,37.35],"舟山":[122.207216,29.985295],
"齐齐哈尔":[123.97,47.33],"盐城":[120.13,33.38],"赤峰":[118.87,42.28],"乳山":[121.52,36.89],
"金昌":[102.188043,38.520089],"泉州":[118.58,24.93],"莱西":[120.53,36.86],"日照":[119.46,35.42],
"胶南":[119.97,35.88],"南通":[121.05,32.08],"拉萨":[91.11,29.97],"云浮":[112.02,22.93],
"梅州":[116.1,24.55],"文登":[122.05,37.2],"上海":[121.48,31.22],"攀枝花":[101.718637,26.582347],
"威海":[122.1,37.5],"承德":[117.93,40.97],"厦门":[118.1,24.46],"汕尾":[115.375279,22.786211],
"潮州":[116.63,23.68],"丹东":[124.37,40.13],"太仓":[121.1,31.45],"曲靖":[103.79,25.51],
"烟台":[121.39,37.52],"福州":[119.3,26.08],"瓦房店":[121.979603,39.627114],"即墨":[120.45,36.38],
"抚顺":[123.97,41.97],"玉溪":[102.52,24.35],"张家口":[114.87,40.82],"阳泉":[113.57,37.85],
"莱州":[119.942327,37.177017],"湖州":[120.1,30.86],"汕头":[116.69,23.39],"昆山":[120.95,31.39],
"宁波":[121.56,29.86],"湛江":[110.359377,21.270708],"揭阳":[116.35,23.55],"荣成":[122.41,37.16],
"连云港":[119.16,34.59],"葫芦岛":[120.836932,40.711052],"常熟":[120.74,31.64],"东莞":[113.75,23.04],
"河源":[114.68,23.73],"淮安":[119.15,33.5],"泰州":[119.9,32.49],"南宁":[108.33,22.84],
"营口":[122.18,40.65], "惠州":[114.4,23.09],"江阴":[120.26,31.91],"蓬莱":[120.75,37.8],
"韶关":[113.62,24.84],"嘉峪关":[98.289152,39.77313],"广州":[113.23,23.16],"延安":[109.47,36.6],
"太原":[112.53,37.87],"清远":[113.01,23.7],"中山":[113.38,22.52],"昆明":[102.73,25.04],
"寿光":[118.73,36.86],"盘锦":[122.070714,41.119997],"长治":[113.08,36.18],"深圳":[114.07,22.62],
"珠海":[113.52,22.3],"宿迁":[118.3,33.96],"咸阳":[108.72,34.36],"铜川":[109.11,35.09],
"平度":[119.97,36.77],"佛山":[113.11,23.05],"海口":[110.35,20.02],"江门":[113.06,22.61],
"章丘":[117.53,36.72],"肇庆":[112.44,23.05],"大连":[121.62,38.92],"临汾":[111.5,36.08],
"吴江":[120.63,31.16],"石嘴山":[106.39,39.04],"沈阳":[123.38,41.8],"苏州":[120.62,31.32],
"茂名":[110.88,21.68],"嘉兴":[120.76,30.77],"长春":[125.35,43.88],"胶州":[120.03336,36.264622],
"银川":[106.27,38.47],"张家港":[120.555821,31.875428],"三门峡":[111.19,34.76],"锦州":[121.15,41.13],
"南昌":[115.89,28.68],"柳州":[109.4,24.33],"三亚":[109.511909,18.252847],"自贡":[104.778442,29.33903],
"吉林":[126.57,43.87],"阳江":[111.95,21.85],"泸州":[105.39,28.91],"西宁":[101.74,36.56],
"宜宾":[104.56,29.77],"呼和浩特":[111.65,40.82],"成都":[104.06,30.67],"大同":[113.3,40.12],
"镇江":[119.44,32.2],"桂林":[110.28,25.29],"张家界":[110.479191,29.117096],"宜兴":[119.82,31.36],
"北海":[109.12,21.49],"西安":[108.95,34.27],"金坛":[119.56,31.74],"东营":[118.49,37.46],
"牡丹江":[129.58,44.6],"遵义":[106.9,27.7],"绍兴":[120.58,30.01], "扬州":[119.42,32.39],
"常州":[119.95,31.79],"潍坊":[119.1,36.62],"重庆":[106.54,29.59],"台州":[121.420757,28.656386],
"南京":[118.78,32.04],"滨州":[118.03,37.36],"贵阳":[106.71,26.57],"无锡":[120.29,31.59],
"本溪":[123.73,41.3],"克拉玛依":[84.77,45.59],"渭南":[109.5,34.52],"马鞍山":[118.48,31.56],
"宝鸡":[107.15,34.38],"焦作":[113.21,35.24],"句容":[119.16,31.95],"北京":[116.46,39.92],
"徐州":[117.2,34.26],"衡水":[115.72,37.72],"包头":[110,40.58],"绵阳":[104.73,31.48],
"乌鲁木齐":[87.68,43.77],"枣庄":[117.57,34.86],"杭州":[120.19,30.26],"淄博":[118.05,36.78],
"鞍山":[122.85,41.12],"溧阳":[119.48,31.43],"库尔勒":[86.06,41.68],"安阳":[114.35,36.1],
"开封":[114.35,34.79],"济南":[117,36.65],"德阳":[104.37,31.13],"温州":[120.65,28.01],
"九江":[115.97,29.71], "邯郸":[114.47,36.6],"临安":[119.72,30.23],"兰州":[103.73,36.03],
"沧州":[116.83,38.33],"临沂":[118.35,35.05],"南充":[106.110698,30.837793],"天津":[117.2,39.13],
"富阳":[119.95,30.07],"泰安":[117.13,36.18],"诸暨":[120.23,29.71],"郑州":[113.65,34.76],
"哈尔滨":[126.63,45.75],"聊城":[115.97,36.45],"芜湖":[118.38,31.33],"唐山":[118.02,39.63],
"平顶山":[113.29,33.75],"邢台":[114.48,37.05],"德州":[116.29,37.45],"济宁":[116.59,35.38],
"荆州":[112.239741,30.335165],"宜昌":[111.3,30.7],"义乌":[120.06,29.32],"丽水":[119.92,28.45],
"洛阳":[112.44,34.7],"秦皇岛":[119.57,39.95],"株洲":[113.16,27.83],"石家庄":[114.48,38.03],
"莱芜":[117.67,36.19],"常德":[111.69,29.05],"保定":[115.48,38.85],"湘潭":[112.91,27.87],
"金华":[119.64,29.12], "岳阳":[113.09,29.37],"长沙":[113,28.21],"衢州":[118.88,28.97],
"廊坊":[116.7,39.53],"菏泽":[115.480656,35.23375],"合肥":[117.27,31.86],"武汉":[114.31,30.52],
"大庆":[125.03,46.58]
};
//函数用于将数据保存成ehcarts地图需要的数据格式 [name:'地点',value:[地图坐标,数据]] [121.48, 31.22, "2239"]
var convertData = function (data) {
var res = [];
for (var i = 0; i < data.length; i++) {
var geoCoord = geoCoordMap[data[i].name];
if (geoCoord) {
res.push({
name: data[i].name,
value: geoCoord.concat(data[i].value)
});
}
}
return res;
};
mapOption = {
title: {
text:'各城市"大数据"相关职位招聘数量',
x:'center',
//modified 0523
subtextStyle:{
//样式与主标题 方式指定就可以
color:'#f30008',
//字体风格,'normal','italic','oblique'
fontStyle:'oblique',
//字体粗细 'normal','bold','bolder','lighter',100 | 200 | 300 | 400...
fontWeight:'bold',
//字体系列
fontFamily:'FangSong'
},
textStyle:{
//文字颜色
color:'#f30008',
//字体风格,'normal','italic','oblique'
fontStyle:'oblique',
//字体粗细 'normal','bold','bolder','lighter',100 | 200 | 300 | 400...
fontWeight:'bold',
//字体系列
fontFamily:'FangSong'
//fontFamily: 'KaiTi'
//字体大小
//fontSize:18
}
//modiefied end
},
tooltip: {
trigger: 'item',
formatter: function (params) { // 鼠标移动到地图 显示: 地点:数值
return params.name + ' : ' + params.value[2];
}
},
visualMap: {
min: 0,
max: max, //设置热力图的最小最大热力值
calculable: true,
inRange: {
color: ['#50a3ba', '#eac736', '#d94e5d'] //随着热力值变化 颜色的变化
}
},
geo: {
map: 'china',
label: {
emphasis: {
show: true
}
}
},
series: [
{
type: 'scatter',
coordinateSystem: 'geo', //加载地图参数
data: convertData(city_data)
}
]
};
heatMap.setOption(mapOption);
//双饼图
var mothWork = echarts.init(document.getElementById('mothWork'));
var data_2018 = [{% for r in rs_gmw[0] %}{name:"{{r.month}}", value:"{{r.job_count}}"},{% endfor %}]
var data_2019 = [{% for r in rs_gmw[1] %}{name:"{{r.month}}", value:"{{r.job_count}}"},{% endfor %}]
var array_2018 = [{% for r in rs_gmw[0] %}"{{r.job_count}}",{% endfor %}];
var array_2019 = [{% for r in rs_gmw[1] %}"{{r.job_count}}",{% endfor %}];
var max_2018 = [];
var max_2019 = [];
array_2018.forEach(function(data){
max_2018.push(+parseInt(data)); ///遍历数组 将每个元素变成整型
});
max_2019.forEach(function(data){
max_2019.push(+parseInt(data)); ///遍历数组 将每个元素变成整型
});
console.log('2018年最多职位数量为:' + Math.max.apply(null, max_2018));
console.log('2018年最多职位数量为:' + Math.max.apply(null, array_2019));
mothWorkOption = {
title : {
text: '大数据"相关职位招聘数量(按月份分组)',
subtext: '双饼图',
x:'center',
//modified 0523
textStyle:{
//文字颜色
color:'#f30008',
//字体风格,'normal','italic','oblique'
fontStyle:'oblique',
//字体粗细 'normal','bold','bolder','lighter',100 | 200 | 300 | 400...
fontWeight:'bold',
//字体系列
fontFamily:'FangSong'
//fontFamily: 'KaiTi'
//字体大小
//fontSize:18
}
//modified end
},
tooltip : {
trigger: 'item',
formatter: "{b}:{c}({d}%)" //当鼠标移动到图形 显示数据(格式): 佛山(1111) 10%
},
legend: {
x : 'center',
y : '30%',
data:['2018年(左图)', '2019年(右图)']
},
series : [
{
name: '2018年(左图)',
type:'pie',
radius : [0, 110], //图像的大小
center : ['25%', '50%'], //图形的位置
data:data_2018
},
{
name: '2019年(右图)',
type:'pie',
radius : [0, 110],
center : ['75%', '50%'],
data:data_2019
}
]
};
mothWork.setOption(mothWorkOption);
//词云
var wordCloud = new Js2WordCloud(document.getElementById('wordCloud'));
var word_cloud = [{% for r in rs_gsr %}["{{r.skill}}","{{r.frequency}}"],{% endfor %}]
console.log('词云数据:' + word_cloud);
wordCloudoPtion = {
tooltip: {
show: true
},
list: word_cloud,
color: '#15a4fa', //字体颜色
backgroundColor: '#ccc',
shape: 'circle',
ellipticity: 1
};
wordCloud.setOption(wordCloudoPtion);
</script>
</html>
4.13 运行Flask程序
(1)分析并统计招聘数量"大数据"相关职位招聘数量,分别使用柱状图表达,同时在网页后台输出相关数据打印语句。
(2)以招聘大数据相关岗位数量为依据绘制散点热度地图。同时在网页后台输出相关数据打印语句
(3)请统计2018年,2019年各月份大数据相关职位的招聘数量,以左右分布的(左侧2018年度,右侧2019年度)饼图进行表达
(4)分析各知识技能在某个招聘岗位能力需求中的占比情况,通过词云图例进行呈现。