Python疫情实时可视化(Flask)
背景:学校选修课的大作业,参考了一下b站视频:BV177411j7qJ
技术架构选用了Flask框架、Selenium工具以及pyecharts库作为后端开发工具;前端则采用html、css和js进行构建。考虑到数据更新频率相对较高,并未接入数据库系统以避免潜在性能瓶颈问题。因课程要求必须使用python进行数据可视化工作,在原本可以直接使用echarts.js实现前端图表展示功能的基础上绕了一番弯路后决定改用pyecharts库完成相关图表开发工作。
数据api:①新浪: https://interface.sina.cn/news/wap/fymap2020_data.d.json
实现过程(由于论文贼多,没什么空,就草草写一下):
最终成果:

建立Flask项目无需多言。使用的是PyCharm(免费版本),由于该版本操作相对繁琐,需进行手动操作生成;而企业版则可以直接建立Flask项目。

目录结构是这样滴
前端页面原型设计:
在PyCharm中完成数据爬取、清洗、整理与开发逻辑构建及可视化展示
由于社区版本界面体验较差,在此选择使用更加友好的VSCode环境进行调试
根据个人审美理念进行设计,在遵循非对称布局原则的基础上(即倾向于减少图形元素数量)

html:
> 1. <body>
>
> 2. <div class="title">全国疫情实时显示</div>
>
> 3. <div class="clock">time display</div>
>
> 4. <div class="data_display">
>
> 5. <div class="num"><h1></h1></div>
>
> 6. <div class="num"><h1></h1></div>
>
> 7. <div class="num"><h1></h1></div>
>
> 8. <div class="num"><h1></h1></div>
>
> 9. <div class="txt"><h2>累计确诊</h2></div>
>
> 10. <div class="txt"><h2>累计境外</h2></div>
>
> 11. <div class="txt"><h2>累计治愈</h2></div>
>
> 12. <div class="txt"><h2>累计死亡</h2></div>
>
> 13. </div>
>
> 14. <div class="mid_bot"></div>
>
> 15. <div class="left_top"></div>
>
> 16. <div class="left_bot"></div>
>
> 17. <div class="right"></div>
>
> 18. <script></script>
>
> 19. </body>
>
>
>
>
> html
>
> 
css(仅供参考):
> 1. body{
>
> 2. margin:0;
>
> 3. background: #333;
>
> 4. }
>
> 5.
>
> 6. .title{
>
> 7. position: absolute;
>
> 8. width: 40%;
>
> 9. height: 10%;
>
> 10. top:0;
>
> 11. left:30%;
>
> 12. display: flex;
>
> 13. align-items: center;
>
> 14. justify-content: center;
>
> 15. /* background-color: aqua; */
>
> 16. color:white;
>
> 17. font-family: 'helvetica neue', sans-serif;
>
> 18. font-size: 35px;
>
> 19. }
>
> 20. .clock{
>
> 21. position: absolute;
>
> 22. width: 30%;
>
> 23. height: 10%;
>
> 24. top:0;
>
> 25. right:0;
>
> 26. display: flex;
>
> 27. align-items: center;
>
> 28. justify-content: center;
>
> 29. /* background-color: white; */
>
> 30. color:white;
>
> 31. font: 1em sans-serif;
>
> 32. }
>
> 33. .data_display{
>
> 34. position: absolute;
>
> 35. width: 40%;
>
> 36. height: 30%;
>
> 37. left: 30%;
>
> 38. top:10%;
>
> 39. color: white;
>
> 40. /* background-color:cadetblue; */
>
> 41. }
>
> 42. .num{
>
> 43. width: 25%;
>
> 44. float: left;
>
> 45. display: flex;
>
> 46. align-items: center;
>
> 47. justify-content: center;
>
> 48. color:gold;
>
> 49. font-size: 15px;
>
> 50. margin-top:20px;
>
> 51. }
>
> 52. .txt{
>
> 53. width: 25%;
>
> 54. float: left;
>
> 55. display: flex;
>
> 56. align-items: center;
>
> 57. justify-content: center;
>
> 58. font-family: "幼圆";
>
> 59. }
>
> 60. .txt h2{
>
> 61. margin: 0%;
>
> 62. }
>
> 63. .mid_bot{
>
> 64. position: absolute;
>
> 65. width: 40%;
>
> 66. height: 60%;
>
> 67. left: 30%;
>
> 68. top:40%;
>
> 69. /* background-color:black; */
>
> 70. }
>
> 71.
>
> 72. .left_top{
>
> 73. position: absolute;
>
> 74. width: 30%;
>
> 75. height: 45%;
>
> 76. left: 0%;
>
> 77. top:10%;
>
> 78. /* background-color:brown; */
>
> 79. }
>
> 80.
>
> 81. .left_bot{
>
> 82. position: absolute;
>
> 83. width: 30%;
>
> 84. height: 45%;
>
> 85. left: 0%;
>
> 86. top:55%;
>
> 87.
>
> 88. }
>
> 89.
>
> 90. .right{
>
> 91. position: absolute;
>
> 92. width: 30%;
>
> 93. height: 90%;
>
> 94. right: 0%;
>
> 95. top:10%;
>
> 96. }
>
>
>
>
> css
>
> 
好的事情是——我们开始数据爬取。首先关注的是新浪的接口服务。请记住——将获取到的数据转换为JSON格式后进一步解析为字典形式将极大提升后续分析效率。随后可以考虑使用在线解析工具来处理JSON数据以便快速定位所需字段。明确目标——确定您所需的特定数据字段之后只需在后端系统中进行相应的处理逻辑即可完成任务
> 1. #未清洗的数据
>
> 2. def get_data():
>
> 3. url="https://interface.sina.cn/news/wap/fymap2020_data.d.json"
>
> 4. res=requests.get(url)
>
> 5. raw_data=res.json()
>
> 6. all_data=raw_data['data']
>
> 7. #print(all_data.keys())
>
> 8. #print(all_data['historylist'])
>
> 9. return all_data
>
>
>
>
> python
>
>
举例而言,在标题下方存在四个‘累计’的数据项。具体而言,在原始数据集中确实如此。这些数据项的keys分别为 'gntotal','jwsrNum','curetotal','deathtotal'。可以直接提取这四个值并将它们包装为一个元组或列表并返回。
> 1. #四个展示数字
>
> 2. def get_display_data():
>
> 3. all_data=get_data()
>
> 4. return (all_data["gntotal"],all_data["jwsrNum"],all_data["curetotal"],all_data["deathtotal"])
>
>
>
>
> python
>
>
另外,这些自己写的方法和接口都统一在一个文件里,然后在app.py里import,不会显得那么冗长,结构也更清晰
那么其他新浪相关的数据爬取就不做说明了,都是这样,自己在字典树里找想要的数据,想办法拿到,然后封装返回
②上海本地宝:用的是selenium+webdriver,selenium库自己导入,webdriver要下个驱动文件
自己先打开本地宝的那个网页,看看网页的结构,发现他已经帮你整理好了上海“新增确诊”和“新增无症状”的历史数据表格,格式非常规律,打开F12查看就很容易看明白,那么我们定位想要的元素,拿到它的text内容,就是需要的数据啦
> 1. from selenium import webdriver
>
> 2. from selenium.webdriver import ChromeOptions
>
> 3. from selenium.webdriver.chrome.options import Options
>
> 4.
>
> 5. def get_shanghai_weekdata():
>
> 6. chrome_option = Options()
>
> 7. #隐藏浏览器
>
> 8. chrome_option.add_argument('--headless')
>
> 9. chrome_option.add_argument('--disable-gpu')
>
> 10. # 防止检测
>
> 11. option = ChromeOptions()
>
> 12. option.add_experimental_option('excludeSwitches', ['enable-automation'])
>
> 13. # 导入配置
>
> 14. driver = webdriver.Chrome(chrome_options=chrome_option, options=option)
>
> 15. driver.get('http://sh.bendibao.com/news/2020119/233243.shtm')
>
> 16.
>
> 17. #观察xpath(是个table),获取一个礼拜的数据,[(date,本地确诊,本地无症状)...]
>
> 18. data_list=[]
>
> 19. for i in range(3,3+2*6+1,2):
>
> 20. date = driver.find_element_by_xpath(f'//*[@id="bo"]/table/tbody/tr[{i}]/td[1]/a/span[1]').text+'日'
>
> 21. new_confirm=driver.find_element_by_xpath(f'//*[@id="bo"]/table/tbody/tr[{i}]/td[3]/span').text
>
> 22. new_nocon=driver.find_element_by_xpath(f'//*[@id="bo"]/table/tbody/tr[{i+1}]/td[2]/span').text
>
> 23. data_list.append((date,new_confirm,new_nocon))
>
> 24.
>
> 25. driver.quit()
>
> 26. return list(reversed(data_list))
>
> 27.
>
> 28. if __name__=="__main__":
>
> 29. print(get_shanghai_weekdata())
>
>
>
>
> python
>
>
>
> 
注意到的是,在隐藏浏览器这一点上需要注意:由于它是用来进行自动化测试的;此外,在使用这种爬取方式时也需要注意其运行速度较慢;而随后进行的可视化渲染也会相对迟缓。
接下来,我以右上角的时间显示为例,来说说前后端数据是怎么传递的:
> 1. #时间
>
> 2. def get_time():
>
> 3. time_str= time.strftime("%Y{}%m{}%d{} %X")
>
> 4. return time_str.format("年","月","日")
>
>
>
>
> python
>
>
编写一个函数用于获取当前时间;然后,在后端代码中引用该函数,并将其路由配置为指向前端 URL 例如 /time。
前端请求该 API 后会接收到一个 promise 对象;可以通过链式操作将其转换为所需的格式类型。
常见的格式类型包括 text、JSON、Blob 和 Form 数据。
> 1. @app.route('/time')
>
> 2. def get_time():
>
> 3. return utils.get_time()
>
>
>
>
> python
>
>
utils.py是专门存放方法的py文件
> 1. <script>
>
> 2. //时间显示
>
> 3. function timeDisplay(){
>
> 4. const time_display=document.querySelector(".clock");
>
> 5. fetch('/time').then(response=>response.text()).then(data=>{
>
> 6. time_display.innerHTML=data;
>
> 7. });
>
> 8.
>
> 9. }
>
> 10. timeDisplay();
>
> 11. setInterval(timeDisplay,1000);
>
> 12. </script>
>
>
>
>
> html
>
> 
这里是后端传了个字符串,所以我把他转为text,大多数情况都是json
四个数据显示:
之前的方法已经实现了返回一个四元组的功能,在JavaScript中由于无法直接处理元组类型(即Tuple),因此需要进一步将其转换为对象(即Object)。具体而言:
- 我们可以将四元组的每个元素分别映射到对象的属性上
- 这样在JavaScript中就可以方便地访问对应的属性值
> 1. from flask import jsonify
>
> 2.
>
> 3. @app.route('/datadisplay')
>
> 4. def get_display_data():
>
> 5. data_to_send=utils.get_display_data()
>
> 6. data_to_send={
>
> 7. "gntotal":data_to_send[0],
>
> 8. "jwtotal":data_to_send[1],
>
> 9. "curetotal":data_to_send[2],
>
> 10. "deathtotal":data_to_send[3]
>
> 11. }
>
> 12. return jsonify(data_to_send) #转为json
>
>
>
>
> python
>
>
>
> 
> 1. <script>
>
> 2. //四个数据显示
>
> 3. function dataDisplay(){
>
> 4. const data_tab=document.querySelectorAll(".num h1");
>
> 5. fetch('/datadisplay').then(response=>response.json()).then(data=>{
>
> 6. data_tab[0].innerHTML=data['gntotal'];
>
> 7. data_tab[1].innerHTML=data['jwtotal'];
>
> 8. data_tab[2].innerHTML=data['curetotal'];
>
> 9. data_tab[3].innerHTML=data['deathtotal'];
>
> 10. });
>
> 11. }
>
> 12. dataDisplay();
>
> 13. //这里不知道它数据多久更新,随便设一个计时器就行啦
>
> 14. setInterval(.....)
>
> 15. </script>
>
>
>
>
> html
>
> 
ok,最后就是pyecharts和echarts的可视化啦,文章太太太长了,作图流程都是一致的,所以我就拿其中一张图演示吧:
有两种基本流程:① 后端处理数据-->pyecharts作图-->把pyecharts实例转化成echarts的option传到前端 -->前端接受,并用echarts的setOption设置-->渲染
②后端处理数据-->封装把数据传到前端-->直接用echarts作图渲染
之前说过规定要用python可视化,所以这里我用的第一种:
随便拿这个左上角的柱状图为例吧:
原始数据里有个key叫jwsrTop,盲猜就是境外输入排行榜啦,我们print这一项看看,发现是个字典列表,形如
> [{"城市名":"上海","境外输入人数":"4000","ename":"shanghai"},{...},{...}...]
>
> python
>
>
那么这个ename并非必要呀!于是我们可以对这个列表进行遍历,并将其中对应的键值对"ename"进行删除操作。值得注意的是,默认情况下字典的items()方法会返回键值对元组(通常是一个迭代器),因此我们需要将其转换为列表形式以供后续使用。
> 1. #获得境外输入排行数据
>
> 2. def get_jwsr():
>
> 3. all_data = get_data()
>
> 4. jw_top=all_data['jwsrTop']
>
> 5. for i in jw_top:
>
> 6. del i['ename']
>
> 7. jw_rank=[]
>
> 8. for i in jw_top:
>
> 9. jw_rank.append(tuple(i.values()))
>
> 10. return jw_rank
>
>
>
>
> python
>
>
>
> 
处理完的数据就应该是这样:
> [("上海","4000"),(...),(...)....]
>
> python
>
>
启动绘图过程,请参考pyecharts的详细说明;制作柱状图表时,请逐一处理数据并提取所有元组的第一个元素形成x坐标列表作为横轴数据。
> 1. #绘制境外输入排行柱状图
>
> 2. def draw_jwsrRank(jwsr_toplist):
>
> 3. x = list(map(lambda i:i[1],jwsr_toplist))
>
> 4. y = list(map(lambda i:i[0],jwsr_toplist))
>
> 5. bar = (
>
> 6. Bar()
>
> 7. .add_xaxis(x)
>
> 8. .add_yaxis("境外输入确诊", y)
>
> 9. .set_global_opts(title_opts={"text": "境外输入排行"},
>
> 10. xaxis_opts=opts.AxisOpts(interval=0))
>
> 11. )
>
> 12. return bar
>
>
>
>
> python
>
>
>
> 
该处代码逻辑实现了生成一个pyecharts类型的实例对象。
在后端实现相关功能时会调用这些方法并生成一个pyecharts类型的实例对象。
将生成的对象转换为与echats兼容的配置参数,并利用现有的接口将其配置参数传递给相应的函数以完成绘图操作。
由于py ech atts是基于e ch ats开发的一个扩展库,在内部实现了多种数据可视化功能,并提供了互相转化的接口如dum p_opt ions_w ith_q u o tes()用于实现不同库之间的兼容性。
> 1. @app.route('/jwsrbar')
>
> 2. def draw_jwsr_bar():
>
> 3. bar=utils.draw_jwsrRank(utils.get_jwsr())
>
> 4. return bar.dump_options_with_quotes()
>
>
>
>
> python
>
>
前端调用该选项后会触发获取想要渲染的那个div容器
> 1. //境外输入柱状图
>
> 2. function jwsrBar(){
>
> 3. var bar_chart = echarts.init(document.querySelector('.left_top'),'dark-bold');
>
> 4. fetch('/jwsrbar').then(response=>response.json()).then(data=>{
>
> 5. setTimeout(() => {
>
> 6. bar_chart.setOption(data);
>
> 7. bar_chart.setOption({xAxis:{axisLabel:{interval:0}}})
>
> 8. }, 500);
>
> 9. });
>
> 10. }
>
> 11. jwsrBar();
>
>
>
>
> javascript
>
>
>
> 
需要注意的是,在作图过程中需要调用echarts.min.js,并且最好采用多个主题以提高可读性。例如,在我的实现中使用了'dark-bold'作为主要风格。
> 1. <head>
>
> 2. <meta charset="utf-8">
>
> 3. <title>covidDataDisplay</title>
>
> 4. <link rel="stylesheet" href="../static/css/style.css">
>
> 5. <script type="text/javascript" src="../static/js/echarts.min.js"></script>
>
> 6. <script type="text/javascript" src="../static/js/china.js"></script>
>
> 7. <script type="text/javascript" src="../static/js/dark-bold.js"></script>
>
> 8.
>
> 9. </head>
>
>
>
>
> html
可视化显示效果需要自行进行参数配置;新增echarts配置项可以直接调用setOption方法;由于支持合并覆盖功能,默认情况下可实现数据交互;详细说明请参考官方文档中的相关内容。
好事告成啦!大致上已经完成了所有的工作流程图制作了。除此之外还有几张图也遵循同样的制作步骤和流程方法;不过其中的那个地图制作过程略显复杂;需要用到china.js库来实现功能;建议自行下载安装或者在互联网资源中查找所需代码。
> 1. #各个省份现有感染人数,用于绘制地图
>
> 2. def get_provinceCurrent():
>
> 3. all_data = get_data()
>
> 4. province_data=all_data['list']
>
> 5. cur_num_dict={}
>
> 6. for i in province_data:
>
> 7. name=i['name']
>
> 8. curnum=i['econNum']
>
> 9. cur_num_dict[name]=curnum
>
> 10. return cur_num_dict
>
> 11.
>
> 12. #绘制地图
>
> 13. def map_visualmap(dict) -> Map:
>
> 14. provinces=dict.keys()
>
> 15. value=dict.values()
>
> 16. # print(provinces)
>
> 17. # print(value)
>
> 18. map = (
>
> 19. Map()
>
> 20. .add("", [list(z) for z in zip(provinces, value)], "china")
>
> 21. .set_global_opts(
>
> 22. title_opts=opts.TitleOpts(title="全国各省现有确诊数"),
>
> 23. visualmap_opts=opts.VisualMapOpts(max_= 500),
>
> 24. )
>
> 25. )
>
> 26. return map
>
>
>
>
> python
>
>
>
> 
ok,剩下的图的代码就不放了,换汤不换药
