结对第二次作业——某次疫情统计可视化的实现
这个作业属于哪个课程 | 2020春|S班(福州大学) |
---|---|
这个作业要求在哪里 | 作业要求 |
结对学号 | 221701419 & 221701410 |
这个作业的目标 | 培养小团队开发能力,可视化的实现,Git的合作开发(Release、Dev分支等功能的使用),《构建之法》四五章的学习 |
作业正文 | 作业正文 |
其他参考文献 | jQuery | echarts | JSON | 阿里巴巴JAVA开发手册 | 阿里云服务器搭建 |
1.GitHub仓库 & 代码规范
GitHub地址:https://github.com/904566722/InfectStatisticWeb
代码规范链接:https://github.com/904566722/InfectStatisticWeb/blob/master/codestyle.md
2.成品展示
- 首页效果
- 省份页面
- 不同颜色区分,鼠标悬停显示
- 地图切换
- 点击省份查看详细数据
- 选择相应日期-主页
- 选择相应日期-省份
- 趋势图
- 治愈率/死亡率
- 首页导航
- 页面跳转
3.组间讨论
3.1 讨论实现、确定框架、分工
3.2 讨论前后端数据交互
3.3 解决bug
3.4 部署服务器
4.设计过程
4.1 设计实现过程
4.1.1 前端设计(221701419)
对照现有的需实现的功能前期准备:
- javaScript库:jquery
- 地图的绘制:rapheal.js
- 图表展现:echarts
- 对照原型进行区域划分,定义相应容器
首页:
省份页面:
- 目录结构划分
- 开始编码,定义相应容器,初步实现布局
- 与后台数据交互显示
- 详细css样式调整
4.1.2 后端设计(221701410)
-
确定框架和工作
使用javaEE原生框架进行开发,后端主要工作为将爬虫爬取到的数据文本写入数据库并对数据库进行操作,以json格式返回前端需要的数据。 -
数据库设计
为使项目更易运行和部署,数据库使用了嵌入式数据库SQLite。在对爬虫爬到的数据文本和所需返回前端的数据进行分析之后,将数据库设计为一张data表,表中有六个字段:id,date,province,eip(现存确诊),esp(现存疑似),tip(累计确诊),tsp(累计疑似)。表中的每条数据反映了某日某省的各项数据。 -
基本结构设计
由于是用javaEE原生框架,因此后端结构也基本确定,主要分为Servlet层、Entity层和DAO层,并将一些工具类放入Util包。
4.2 功能结构图
5.关键代码说明
5.1 前端(221701419)
- 统计人数数据显示
- 获取后端数据
利用request.getAttribute("")方法获得后台传来的数据,格式上进行一些简单的处理,为jsp所用
Object totalData = request.getAttribute("totalData"); Object compareData = request.getAttribute("compareData"); JSONArray compareJson = (JSONArray)compareData; JSONObject compare = (JSONObject) compareJson.get(0); JSONArray toatlDataJson = (JSONArray)totalData; String strEip = ""; String strEsp = ""; String strDead = ""; String strCure = ""; int intEip = compare.getInt("eip"); int intEsp = compare.getInt("esp"); int intDead = compare.getInt("dead"); int intCure = compare.getInt("cure"); if (intEip > 0){ strEip = "+" + intEip; }else{ strEip = "" + intEip; } if (intEsp > 0){ strEsp = "+" + intEsp; }else{ strEsp = "" + intEsp; } if (intDead > 0){ strDead = "+" + intDead; }else{ strDead = "" + intDead; } if (intCure > 0){ strCure = "+" + intCure; }else{ strCure = "" + intCure; }
- 数据显示
由于采用javaEE开发,利用<%=%>嵌入相应的值得以显示
<div id="wholeData"> <div id ="wdPs"> <p class="ps1"> <span style="color: #7F7F7F">截至<%=endDate%>全国数据统计</span> </p> <p class="ps2"> <span id="dataSource" style="color: #AAAAAA">数据说明</span> </p> </div> <div id="wdData"> <div class="ip"> <strong><%=wholeNationData.getInt("eip")%></strong><br> <span>现存确诊</span> <div class="compareToday"> <span style="font-size: 8px">较昨日:</span> <span style="font-size: 8px;color: #F74C31"><%=strEip%></span> </div> </div> <div class="sp"> <strong><%=wholeNationData.getInt("esp")%></strong><br> <span>现存疑似</span> <div class="compareToday"> <span style="font-size: 6px">较昨日:</span> <span style="font-size: 6px;color: #F78207"><%=strEsp%></span> </div> </div> <div class="cure"> <strong><%=wholeNationData.getInt("cure")%></strong><br> <span>累计治愈</span> <div class="compareToday"> <span style="font-size: 6px">较昨日:</span> <span style="font-size: 6px;color: #28B7A3"><%=compare.getInt("cure")%></span> </div> </div> <div class="dead"> <strong><%=wholeNationData.getInt("dead")%></strong><br> <span>累计死亡</span> <div class="compareToday"> <span style="font-size: 6px">较昨日:</span> <span style="font-size: 6px;color: #5D7092"><%=strDead%></span> </div> </div> </div> </div>
- 地图绘制
从全部数据中获取到相应的值:eip(现存确诊)/tip(累计确诊),判断数值的区间划分颜色等级,然后定义不同等级相应的颜色,调用paintMap绘制地图
- 路径准备
function paintMap(R) { var attr = { "fill": "#97d6f5", "stroke": "#eee", "stroke-width": 1, "stroke-linejoin": "round" }; china.aomen = { name: "澳门", path: R.path("M413.032,414.183l-0.96,1.752c0,0,0.889,0.883,1.98,1.086s1.995-0.493,1.995-0.493L413.032,414.183z").attr(attr) } china.hk = { name: "香港", path: R.path("M417.745,409.005l3.394,0.773l3.453-2.558l1.666,4.582c0,0-5.521,2.673-3.691,1.785c1.828-0.884-4.641-0.355-4.641-0.355l-0.834-3.454L417.745,409.005z").attr(attr) }; ... }
- 地图的绘制
/* * 根据后台传入的json数据渲染颜色 * param json 后台获取的json全国数据 * parm path 项目路径 */ function distinguishColorEip(json, path){ //1. 获得数据--json格式 //2. json转换为数组 var data = string2Array(JSON.stringify(json)); var flag; var arr = new Array(); for(var i=0; i<data.length; i++){ var d = data[i].eip; if(d == 0){ rank = 0; }else if(0<d && d<10){ rank = 1; }else if(10<=d && d<100){ rank = 2; }else if(100<=d && d<500){ rank = 3; }else if(500<=d && d<1000){ rank = 4; }else if(1000<=d && d<10000){ rank = 5; }else if(10000<=d){ rank = 6; } arr.push(rank); } //定义颜色 var colors = ["#fff","#DCF4EE","#44CEF5","#4C8CAF","#177CB0","#2E4E7D","#00336F"]; //地图绘制 var R = Raphael("map1", 600, 500); paintMap(R); var textAttr = { "fill" : "#000", "font-size" : "12px", "font-weight" : "bold", // "cursor" : "pointer" }; var i = 0; var j = 0; for(var state in china){ china[state]['path'].color = Raphael.getColor(0.9); (function (st, state) { // console.log(china[state]['name']); //获取当前图形的中心坐标 var xx = st.getBBox().x + (st.getBBox().width / 2); var yy = st.getBBox().y + (st.getBBox().height / 2); //修改部分地图文字偏移坐标 switch (china[state]['name']) { case "江苏": xx += 5; yy -= 10; break; case "河北": xx -= 10; yy += 20; break; case "天津": xx += 10; yy += 10; break; case "上海": xx += 10; break; case "广东": yy -= 10; break; case "澳门": yy += 10; break; case "香港": xx += 20; yy += 5; break; case "甘肃": xx -= 40; yy -= 30; break; case "陕西": xx += 5; yy += 10; break; case "内蒙古": xx -= 15; yy += 65; break; default: } //写入文字 china[state]['text'] = R.text(xx, yy, china[state]['name']).attr(textAttr); var fillcolor = colors[arr[i]];//获取对应的颜色 st.attr({fill:fillcolor});//填充背景色 st[0].onmouseover = function () { st.animate({fill: "#fdd", stroke: "#eee"}, 500); china[state]['text'].toFront(); R.safari(); displayProvinceInfo(data[indexOfData[state]]); st[0].style.cursor = "pointer"; }; st[0].onmouseout = function () { st.animate({fill: fillcolor, stroke: "#eee"}, 500); china[state]['text'].toFront(); R.safari(); $("#provinceInfo").hide(); st[0].style.cursor = "pointer"; }; st[0].onclick = function () { window.location.href = path + "/DataServlet?action=getProvinceData&province=" + china[state]['name']; } })(china[state]['path'], state); i++; } }
- 趋势图绘制
趋势图的绘制利用了echarts,主要是获取到相应的数据,对series进行配置
function drawLineChart2(date, cure, dead){
$("#showLineChart1").css("background-color","#F59A23");
$("#showLineChart2").css("background-color","#288ADE");
$("#showLineChart3").css("background-color","#F59A23");
$("#lineChart1").hide();
$("#lineChart3").hide();
$("#lineChart2").show();
// step2. 基于准备好的dom,初始化echarts实例
var lineChart2 = echarts.init(document.getElementById('lineChart2'));
// step3. 指定图表的配置项和数据
var option = {
title: {
text: '累计治愈/累计死亡'
},
tooltip: {
trigger: 'axis'
},
legend: {
data: ['累计治愈', '累计死亡']
},
...
series: [
{
name: '累计治愈',
type: 'line',
stack: '总量1',
data: cure
},
{
name: '累计死亡',
type: 'line',
stack: '总量2',
data: dead
},
]
};
// step4. 使用刚指定的配置项和数据显示图表。
lineChart2.setOption(option);
}
- 表格数据的生成
采用一个循环,遍历数据,打印表格每行数据
<%
JSONArray jsonData = (JSONArray)totalData;
for(int i=0; i< jsonData.size(); i++){
JSONObject objData = jsonData.getJSONObject(i);%>
<tr><td><%=objData.getString("name")%></td>
<td><%=objData.getInt("eip") %></td>
<td><%=objData.getInt("tip") %></td>
<td><%=objData.getInt("dead") %></td>
<td><%=objData.getInt("cure") %></td>
</tr>
<%}
%>
5.2 后端(221701410)
- 数据库建表
- 连接数据库并建表,为读文件做准备。
try (Connection connection = DBConnect.getConnection()) {
connection.setAutoCommit(false);
Statement statement = connection.createStatement();
statement.executeUpdate("create table data(id integer primary key autoincrement, date date, " +
"province varchar(20), eip integer, esp integer, tip integer, tsp integer, cure integer, dead integer);");
connection.commit();
...
- 读取爬虫爬取写入的txt文件并存至数据库
- 这部分代码为寒假第二次作业的改版,适应了新的文本格式并将数据写入数据库。
public void addToDatabase(String str, String logDate) {
String pattern = "(\\W+) (\\W+) (\\d+) (\\d+) (\\d+) (\\d+) (\\d+) (\\d+) (\\d+) (\\d+)";
Pattern r = Pattern.compile(pattern);
Matcher m = r.matcher(str);
String province = "";
int eip, esp, tip, tsp, cure, dead;
eip = esp = tip = tsp = cure = dead = 0;
if (m.find()) {
province = m.group(1);
tip = Integer.parseInt(m.group(3));
tsp = Integer.parseInt(m.group(4));
...
} else {
System.out.println("NO MATCH");
}
for (int i = 0; i < provinceString.length; i++) {
if (provinceString[i].equals(province)) {
if (influencedProvince[i] == 0) {
influencedProvince[i] = 1;
String abbr = abbreviateProvince(province);
try (Connection connection = DBConnect.getConnection()) {
connection.setAutoCommit(false);
Statement statement = connection.createStatement();
statement.executeUpdate("INSERT INTO data VALUES(null, '" + logDate + "', '" + abbr
+ "', " + eip + ", " + esp + ", " + tip + ", " + tsp + ", " + cure + ", " + dead + ")");
connection.commit();
- 响应前端请求的DataServlet类
- DataServlet类响应前端请求,action为getTotalData时会返回首页所需数据并跳转至首页,action为getProvinceData时会返回省份页所需数据并跳转至省份页。action缺省时返回首页。
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
request.setCharacterEncoding("UTF-8");
response.setCharacterEncoding("UTF-8");
String action = request.getParameter("action");
if (action != null && action.equals("getTotalData")) {
this.getTotalData(request, response);
} else if (action != null && action.equals("getProvinceData")) {
this.getProvinceData(request, response);
} else {
this.getTotalData(request, response);
}
}
- DataServlet?action=getTotalData需要一个参数endDate,为页面所需数据的截止日期。Servlet的方法写得较为冗长,但本次作业请求简单,因此也易理解。
private void getTotalData(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
DataDAO dataDAO = new DataDAO();
String endDate = request.getParameter("endDate");
String latestDate = dataDAO.getLatestDate();
String oldestDate = dataDAO.getOldestDate();
if (endDate != null && endDate.compareTo(latestDate) > 0) {
endDate = latestDate;
} else if (endDate != null && endDate.compareTo(oldestDate) < 0) {
endDate = oldestDate;
} else if (endDate != null) {
endDate = dataDAO.changeDateFormat(endDate);
} else {
endDate = dataDAO.getLatestDate();
}
JSONArray totalData = dataDAO.getTotalData(endDate, "全国");
JSONArray dailyData = dataDAO.getDailyData(endDate, "全国");
JSONArray compareData = dataDAO.getCompareData(endDate, "全国");
request.setAttribute("totalData", totalData);
request.setAttribute("dailyData", dailyData);
request.setAttribute("compareData", compareData);
request.setAttribute("endDate", endDate);
request.setAttribute("latestDate", latestDate);
request.setAttribute("oldestDate", oldestDate);
request.getRequestDispatcher("index.jsp").forward(request, response);
}
- DataServlet?action=getProvinceData需要两个参数endDate,分别为页面所需数据的截止日期和页面的省份信息。代码与getTotalData大致相当。
- 数据库处理的DataDAO类
- getPrvinceDailyData方法,需要endDate和province两个参数,返回该省份截止endDate每天的新增确诊、新增疑似、累计治愈和累计死亡的json数据。
public JSONArray getProvinceDailyData(String endDate, String province) {
JSONArray jsonArray = new JSONArray();
int oldTip, oldTsp;
oldTip = oldTsp = 0;
try (Connection connection = DBConnect.getConnection()) {
Statement statement = connection.createStatement();
String sql = "SELECT * FROM DATA WHERE date <= '" + endDate + "' AND province = '" + province + "'";
ResultSet resultSet = statement.executeQuery(sql);
int flag = 0;
while (resultSet.next()) {
double tip = resultSet.getInt("tip");
double tsp = resultSet.getInt("tsp");
double cure = resultSet.getInt("cure");
double dead = resultSet.getInt("dead");
JSONObject jsonObject = new JSONObject();
jsonObject.put("date", resultSet.getString("date"));
jsonObject.put("ip", (int)tip - oldTip);
jsonObject.put("sp", (int)tsp - oldTsp);
jsonObject.put("cure", (int)cure);
jsonObject.put("dead", (int)dead);
if (tip != 0 && cure != 0) {
jsonObject.put("cureRate", String.format("%.3f", cure / tip));
} else {
jsonObject.put("cureRate", "0");
}
if (tip != 0 && dead != 0) {
jsonObject.put("deadRate", String.format("%.3f", dead / tip));
} else {
jsonObject.put("deadRate", "0");
}
oldTip = (int) tip;
oldTsp = (int) tsp;
...
return jsonArray;
}
- getLatestDate方法,获取数据库中最新的日期。
public String getLatestDate() {
String latestDate = "2020-01-01";
try (Connection connection = DBConnect.getConnection()) {
Statement statement = connection.createStatement();
String sql = "SELECT * FROM DATA WHERE province = '" + "湖北" + "'";
ResultSet resultSet = statement.executeQuery(sql);
while (resultSet.next()) {
if (latestDate.compareTo(resultSet.getString("date")) < 0) {
latestDate = resultSet.getString("date");
}
}
} catch (SQLException e) {
e.printStackTrace();
}
return latestDate;
}
- getCompareData方法,需要endDate和province两个参数,返回该省份endDate当天相较于昨天的各项数据变化。
public JSONArray getCompareData(String endDate, String province) {
int eip, esp, tip, tsp, cure, dead;
eip = esp = tip = tsp = cure = dead = 0;
String yesterday = getYesterday(endDate);
JSONArray jsonArray = new JSONArray();
try (Connection connection = DBConnect.getConnection()) {
Statement statement = connection.createStatement();
String sql = "SELECT * FROM DATA WHERE date = '" + endDate + "' AND province = '" + province + "'";
ResultSet resultSet = statement.executeQuery(sql);
while (resultSet.next()) {
eip += resultSet.getInt("eip");
esp += resultSet.getInt("esp");
...
}
String sql2 = "SELECT * FROM DATA WHERE date = '" + yesterday + "' AND province = '" + province + "'";
ResultSet resultSet2 = statement.executeQuery(sql2);
while (resultSet2.next()) {
eip -= resultSet2.getInt("eip");
esp -= resultSet2.getInt("esp");
...
}
} catch (SQLException e) {
e.printStackTrace();
}
}
JSONObject jsonObject = new JSONObject();
jsonObject.put("eip", eip);
jsonObject.put("esp", esp);
...
jsonArray.add(jsonObject);
return jsonArray;
}
5.3 数据获取(221701419)
import time
import datetime
from scrapy import cmdline
import pandas as pd
import requests
import json
class TencentSpider:
def __init__(self):
self.url = "https://view.inews.qq.com/g2/getOnsInfo?name=disease_h5"
self.headers = {"user-agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.132 Safari/537.36"}
self.update_time = ""
# json格式的data数据
def getData(self, url):
response = requests.get(url, headers=self.headers).json()
return json.loads(response['data']) #转化为字典
# 处理数据
def getCitiesRes(self,data):
# 最新更新时间
self.update_time = data['lastUpdateTime']
all_counties = data['areaTree']
for country_data in all_counties:
if country_data['name'] != '中国':
continue
else:
all_provinces = country_data['children'] # children--各个省份的情况
all_list = []
# 1.遍历所有省份
for province_data in all_provinces:
# 2.获得省份名称
province_name = province_data['name']
# 3.获得所有城市
all_cities = province_data['children']
# 4.遍历所有城市
for city_data in all_cities:
city_name = city_data['name']
city_total = city_data['total'] # 市级的total:{'confirm': 198,'suspect': 0,'dead': 9,'deadRate': '4.55', 'showRate': False, 'heal': 180,'healRate': '90.91','showHeal': True}},
#字典形式的结果
city_result = {'省份名称': province_name, '城市': city_name, '更新时间': self.update_time}
city_result.update(city_total) # 一行--市级total添加到结果字典
all_list.append(city_result)
return all_list
def getProvideRes(self, data):
dataDict = data
updateTime = dataDict['lastUpdateTime'] # 更新时间
countries = dataDict['areaTree'] # 所有国家
for countryData in countries:
if countryData['name'] != '中国':
continue
else:
provides = countryData['children'] # children--所有省份
title = "//省份 城市 省份确诊 省份疑似 省份治愈 省份死亡 城市确诊 城市疑似 城市治愈 城市死亡"
ps = "//数据获取时间:" + updateTime
fw = open(updateTime.split(' ')[0] + '.txt', 'w')
fw.write(title + '\n' + ps + '\n')
# 遍历省份
for provide in provides:
provideTotal = provide['total']
confirm = provideTotal['confirm']
suspect = provideTotal['suspect']
dead = provideTotal['dead']
heal = provideTotal['heal']
deadRate = provideTotal['deadRate']
healRate = provideTotal['healRate']
oneLine1 = provide['name'] + " " + "城市占位字" + " " + str(confirm) + " " + str(suspect) + " " + str(
heal) + " " + str(dead) + " " + "0 0 0 0"
# oneLine2 = "//" + provide['name'] + " " + "治愈率:" + healRate + " " + "死亡率:" + " " + deadRate
fw.write(oneLine1 + '\n')
fw.close()
def getCountriesRes(self, data):
dataDict = data
updateTime = dataDict['lastUpdateTime'] # 更新时间
countries = dataDict['areaTree'] # 所有国家
title = "//国家 确诊 疑似 死亡 治愈 死亡率 治愈率"
ps = "//数据获取时间:" + updateTime
fw = open("[国家]" + updateTime.split(' ')[0] + '.txt', 'w')
fw.write(title + '\n' + ps + '\n')
# 遍历省份
for country in countries:
country_total = country['total']
confirm = country_total['confirm']
suspect = country_total['suspect']
dead = country_total['dead']
heal = country_total['heal']
deadRate = country_total['deadRate']
healRate = country_total['healRate']
oneLine = country['name'] + " " + str(confirm) + " " + str(suspect) + " " + str(heal) + " " + str(
dead) + " " + str(deadRate) + " " + str(healRate)
fw.write(oneLine + '\n')
fw.close()
def run(self):
data = self.getData(self.url)
self.getProvideRes(data)
self.getCountriesRes(data)
# 设定时间
def doIt(h=21, m=30, s=20):
while True:
now = datetime.datetime.now()
if now.hour == h and now.minute == m and now.second == s:
tencentSpider = TencentSpider()
tencentSpider.run()
if __name__ == '__main__':
tencentSpider = TencentSpider()
tencentSpider.run()
# doIt()
6.总结
6.1 心路历程 & 收获
221701419
这次的的项目我们采用了javaEE来开发,采用jsp编写的前端,还是有比较大的耦合,因此在数据的交接上不免的交流修改,但也正是因此在整个交流开发的过程中更加增进了彼此的感情,我想这也是结对的一个意义所在,培养交流能力、协作能力。整个开发的过程也有不少磕碰,但也都在彼此的互帮互助下循序前进,如今也将文档写到了这里,回想这个历程还是很美好的,其次在技术上也有了一定的进步,jQuery的使用、echarts的配置、前端的调试等等,通过这次的作业也更加的熟练。
221701410
在本次项目中我收获良多,在一边学习javaEE课程的同时,一边和伙伴合作亲手建立起一个简易的javaEE项目,这对学习来说是非常好的实践。这次项目的时间相对较短,因此很多功能还未完善。在进行这个项目的时候我新学到了sqlite的操作、json的使用、云服务器的搭建等新知识,同时也巩固了数据库的相关知识,并动手实践了javaEE的相关内容,收益匪浅。希望在以后的实践中越来越进步!
6.2 队友评价
221701419评价221701410
非常靠谱,耐心且细心,整个开发过程中碰到了不少问题,但都一起愉快的解决了,是能够共同进步的队友。
221701410评价221701419
非常给力的队友!合作过程中交流融洽,项目开发进展顺利,并且在我对数据来源一筹莫展的时候写好了爬虫,使我只要读取本地文件即可更新数据库,tql!希望以后有机会能继续搭档!