结对第二次作业——某次疫情统计可视化的实现
这个作业属于哪个课程 | 2020春丨w班 |
---|---|
这个作业要求在哪里 | 作业要求 |
结对学号 | 221701328 221701312 |
这个作业的目标 | 实现原型设计中的部分功能 |
作业正文 | 作业正文 |
其他参考文献 |
Github仓库地址和代码规范链接
展示你的成品
-
头部有3个按钮和一个日期选择按钮 “实时疫情”,“全国数据”和“辟谣和防护”点击后会跳转到相应的位置
-
选择日期后数据会截止到选择的日期
-
疫情地图可以查看现有确诊和累计确诊两个情况下的地图
-
鼠标移到某一省份上后会高亮显示该省份 并显示相应数据
-
点击省份后会跳转到该省份的具体页面
-
可以选择查看新增或是累计数据的折线图
-
返回后 在疫情地图下方是全国数据的折线图
-
可以查看三种情况下的全国数据折线图
-
下方是全国数据的表格
-
最后是辟谣与防护界面和版权信息
结对讨论过程描述
-
最开始的时候是我们两个创建了一个组织,然后创建了一个仓库InfectStatisticWeb
-
我们是用JavaEE开发的这个项目 所以仓库里的项目结构 如src文件夹下的servlet和web文件夹都是按JavaEE初始化好的。然后就开始讨论分工,得出的结果是我负责前端和爬虫工具,春翔负责后端和爬虫工具类的使用。爬虫工具我百度之后,就选择了Jsoup来进行爬虫,爬到网页的源代码后,再进行拆分,就得到了自己想要的数据。
-
遇到的问题就是每个省份的数据都存储在一个json文件中,因此又百度了一下Java读取web文件的方法,然后在json文件中提取想要的数据即可。下面是我写爬虫是遇到的问题,经过讨论之后知道了数据要写成log文件的格式,就像第一次作业那样,因此就有了思路。
描述设计实现过程
-
后端
后端使用JavaEE来搭建,由于我们JavaEE刚开始学习不久,只会使用Servlet没有使用Spring等后端框架。后端分为三层:servlet层用来接收请求,创建数据类,然后相应请求;data层放置数据类,把日志数据封装成类,并提供计算方法,以及将数据转化成json格式的方法,以供servlet层调用;dao层是数据接口层,使用日志文件来获取疫情数据,本层封装了访问文件和读取文件的方法,以供data层调用。
-
前端
项目实现了前后端分离,前端使用Jquery来进行AJAX请求和实现一些小动画,由echarts来绘制地图和统计图,使用bootstrap来美化界面。
-
功能结构图
关键代码
- 这是爬虫获取丁香园疫情地图中省份数据及其对应的json文件的代码。大致思路就是爬虫获取html网页源代码,将其转换为字符串,然后在字符串中提取到想要的信息(如json文件的url地址及其对应的省份名称),将信息放入ArrayList中存储。
public static String USER_AGENT = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:49.0) Gecko/20100101 Firefox/49.0";//模仿火狐浏览器
public static String HOST = "i.snssdk.com";
public static String REFERER = "https://i.snssdk.com/feoffline/hot_list/template/hot_list/forum_tab.html?activeWidget=1";
ArrayList<String> url = new ArrayList<>();
ArrayList<String> provinceName = new ArrayList<String>();
public void getUrlAndProvince(){
String link = "https://ncov.dxy.cn/ncovh5/view/pneumonia";//丁香园疫情地图网址
String resultBody = null,tempResult;
Document doc = null;
int position1,position2,i = 0;
try{
doc = Jsoup.connect(link).userAgent(USER_AGENT).header("Host",HOST).header("Referer",REFERER).get();//爬取网页html
}catch (Exception e){
e.printStackTrace();
}
resultBody = doc.getElementById("getAreaStat").toString();
Matcher matcher = Patterns.WEB_URL.matcher(resultBody);//匹配其中的url
StringBuffer buffer = new StringBuffer();
matcher.find();
while(matcher.find()){
buffer.append(matcher.group());
buffer.append("\n");
i++;
}
String[] urls = buffer.toString().split("\n");
for(i = 0;i < urls.length;i++){
url.add(urls[i]);
}
resultBody = resultBody.replaceAll("\"","").replaceAll(",","");//获取省份信息
for(i = 0;i < urls.length;i++) {
position1 = resultBody.indexOf("provinceShortName");
resultBody = resultBody.substring(position1);
position2 = resultBody.indexOf("currentConfirmedCount");
position1 = 0;
tempResult = resultBody.substring(position1, position2).substring(18);
resultBody = resultBody.substring(position2+21);
provinceName.add(tempResult);
}
}
- 根据json文件的url地址,省份名,还有log文件的路径名,生成连续的log文件,log文件的名称格式为yyyy-mm-dd.log.txt
public void getFiles(String path,String provinceName,String logPath) {
ArrayList<String> date = new ArrayList<>();
ArrayList<Integer> confirmedIncr = new ArrayList<>();
ArrayList<Integer> curedIncr = new ArrayList<>();
ArrayList<Integer> deadIncr = new ArrayList<>();
int HttpResult, position1, position2, position3, position4;
String result = null, temp1, temp2, temp3, temp4, line;
try {
URL url = new URL(path);
URLConnection urlconn = url.openConnection();
urlconn.connect();
HttpURLConnection httpconn = (HttpURLConnection) urlconn;
HttpResult = httpconn.getResponseCode();
if (HttpResult != HttpURLConnection.HTTP_OK) {
System.out.println("无法连接");
} else {
InputStreamReader isReader = new InputStreamReader(urlconn.getInputStream(), "UTF-8");
BufferedReader reader = new BufferedReader(isReader);
StringBuffer buffer = new StringBuffer();
line = reader.readLine(); // 读取第一行
while (line != null) { // 如果 line 为空说明读完了
buffer.append(line); // 将读到的内容添加到 buffer 中
buffer.append(" "); // 添加换行符
line = reader.readLine(); // 读取下一行
}
result = buffer.toString();
}
} catch (Exception e) {
e.printStackTrace();
}
position1 = result.indexOf("[");
position2 = result.indexOf("]");
result = result.substring(position1 + 1, position2);
String[] strs = result.split("}");
strs[0] = " " + strs[0];
for (String str : strs) {
str = str.substring(2);
str = str.replaceAll(",", " ").replaceAll("\"", "")
.replaceAll("\\{", "");
position3 = str.indexOf("dateId");
position4 = str.indexOf("deadCount");
temp1 = str.substring(position3, position4).substring(7);
temp1 = temp1.substring(0, 4) + "-" + temp1.substring(4, 6) + "-" + temp1.substring(6, 8);
position3 = str.indexOf("confirmedIncr");
position4 = str.indexOf("curedCount");
temp2 = str.substring(position3, position4).substring(14).trim();
position3 = str.indexOf("deadIncr");
temp3 = str.substring(position3).substring(9).trim();
position3 = str.indexOf("curedIncr");
position4 = str.indexOf("currentConfirmedCount");
temp4 = str.substring(position3, position4).substring(10).trim();
date.add(temp1);//temp1为日期 格式为yyyy-mm-dd
confirmedIncr.add(Integer.valueOf(temp2));//temp2为确诊人数
deadIncr.add(Integer.valueOf(temp3));//temp3为死亡人数
curedIncr.add(Integer.valueOf(temp4));//temp4为治愈人数
}
for (int j = 0; j < date.size(); j++) {
String filePath = logPath + date.get(j) + ".log.txt", content;
File newFile = new File(filePath);
try {
if (!newFile.exists()) {
newFile.createNewFile();
}
FileWriter fw = new FileWriter(filePath, true);
BufferedWriter bw = new BufferedWriter(fw);
if (confirmedIncr.get(j) != 0) {
content = String.format("%s 新增 感染患者 %d人\n", provinceName, confirmedIncr.get(j));
bw.write(content);//按照xx新增 感染患者 xx人的格式写入log文件中
}
if (deadIncr.get(j) != 0) {
content = String.format("%s 死亡 %d人\n", provinceName, deadIncr.get(j));
bw.write(content);//按照xx 死亡 xx人的格式写入log文件中
}
if (curedIncr.get(j) != 0) {
content = String.format("%s 治愈 %d人\n", provinceName, curedIncr.get(j));
bw.write(content);//按照xx 治愈 xx人的格式写入log文件中
}
bw.close();
fw.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
- eharts载入dom,并使用ajax以post方式请求服务器数据,返回数据类型为json
let existMap = echarts.init(document.getElementById("existMap"));
let cumulativeMap = echarts.init(document.getElementById("cumulativeMap"));
let incInfChart = echarts.init(document.getElementById("incInfChart"));
let exiCumInfChart = echarts.init(document.getElementById("exiCumInfChart"));
let deadCureChart = echarts.init(document.getElementById("deadCureChart"));
loadNationalData();
function loadNationalData(date) {
existMap.showLoading();
cumulativeMap.showLoading();
incInfChart.showLoading();
exiCumInfChart.showLoading();
deadCureChart.showLoading();
$.ajax({
type:"POST",
url:"NationalData",
data:{
date:date
},
dataType:"json",
success:function (data) {
......
existMap.setOption({
- 根据数据生成日期选择下拉框,选择日期后重新调用ajax方法
$("#dropdown").empty();
for(var i =0;i < data[0].length;i++) {
$("#dropdown").append("<li><button class='dateList btn'>"+data[0][i].date+"<tton><>");
}
$(".dateList").click(function (e) {
loadNationalData($(this).html());
});
- 点击地图区域跳转网页,并传递点击地区名给目标页面
existMap.on("click",function (param) {
location.href = "province.html?province=" + encodeURI(param.name);
});
cumulativeMap.on("click",function (param) {
location.href = "province.html?province=" + encodeURI(param.name);
})
- 处理日志数据
public void initData(LogDao dao) {
List<File> logList = dao.getLogList();
for(File log : logList) {
String date = log.getName().substring(0,log.getName().indexOf("."));
DatePartData datePartData = new DatePartData();
List<String> records = dao.getRecords(log);
List<Integer> cumList;
int num;
for(String record : records) {
String[] data = record.split(" ");
if(data.length < 2) {
continue;
}
switch (data[1]) {
case "新增":
if ("感染患者".equals(data[2])) {
num = MyUtil.parseData(data[3]);
datePartData.add(data[0],0, num);
cumList = cumData.get(data[0]);
cumList.set(0,cumList.get(0) + num);
} else if ("疑似患者".equals(data[2])) {
num = MyUtil.parseData(data[3]);
datePartData.add(data[0],1, MyUtil.parseData(data[3]));
cumList = cumData.get(data[0]);
cumList.set(1,cumList.get(1) + num);
}
break;
case "死亡":
num = MyUtil.parseData(data[2]);
datePartData.add(data[0],2, num);
cumList = cumData.get(data[0]);
cumList.set(2,cumList.get(2) + num);
break;
case "治愈":
num = MyUtil.parseData(data[2]);
datePartData.add(data[0],3, num);
cumList = cumData.get(data[0]);
cumList.set(3,cumList.get(3) + num);
break;
}
}
datePartData.setInitData(cumData);
datePartData.compute();
statistics.put(date,datePartData);
}
}
- 计算累计数据和现存数据
public void compute() {
for(String province : provincesDataExi.keySet()) {
List<Integer> list = provincesDataExi.get(province);
provincesCumInfect.put(province,list.get(0));
nationalCumInfect += list.get(0);
list.set(0,list.get(0) - list.get(2) - list.get(3)); //现存感染等于累计感染减去死亡和治愈
for(int i = 0;i < 4;i++) {
nationalDataInc.set(i,nationalDataInc.get(i) + provincesDataInc.get(province).get(i));
nationalDataExi.set(i,nationalDataExi.get(i) + provincesDataExi.get(province).get(i));
}
}
}
- Servlet的dopost方法,接收请求并初始化数据对象。使用session保证在一次会话中不多次计算占用系统资源。返回json数据给前端。
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
request.setCharacterEncoding("utf-8");
response.setContentType("application/json;charset=utf-8");
response.setCharacterEncoding("utf-8");
PrintWriter writer = response.getWriter();
TotalData totalData = (TotalData) request.getSession().getAttribute("totalData");
String date = request.getParameter("date");
if (totalData == null) {
String path = getServletConfig().getServletContext().getRealPath("/") + "log";
LogDao dao = new LogDao(path);
totalData = new TotalData();
totalData.initData(dao);
request.getSession().setAttribute("totalData",totalData);
}
if (date == null) {
writer.write(totalData.getAllNationalJson(MyUtil.getNewestDate()));
} else {
writer.write(totalData.getAllNationalJson(date));
}
writer.flush();
writer.close();
}
- 把数据转化成json
private String getTotalDataJson(String date) {
DatePartData data = statistics.get(date);
List<Integer> increment = data.getNationalDataInc();
List<Integer> exist = data.getNationalDataExi();
StringBuilder jsonStr = new StringBuilder();
jsonStr.append("[");
jsonStr.append("{");
jsonStr.append("\"type\":").append("\"现存确诊\"").append(",");
jsonStr.append("\"num\":").append(exist.get(0)).append(",");
jsonStr.append("\"compare\":").append(increment.get(0) - increment.get(2) - increment.get(3));
jsonStr.append("}").append(",");
jsonStr.append("{");
jsonStr.append("\"type\":").append("\"累计确诊\"").append(",");
jsonStr.append("\"num\":").append(data.getNationalCumInfect()).append(",");
jsonStr.append("\"compare\":").append(increment.get(0));
jsonStr.append("}").append(",");
jsonStr.append("{");
jsonStr.append("\"type\":").append("\"现存疑似\"").append(",");
jsonStr.append("\"num\":").append(exist.get(1)).append(",");
jsonStr.append("\"compare\":").append(increment.get(1));
jsonStr.append("}").append(",");
jsonStr.append("{");
jsonStr.append("\"type\":").append("\"现存死亡\"").append(",");
jsonStr.append("\"num\":").append(exist.get(2)).append(",");
jsonStr.append("\"compare\":").append(increment.get(2));
jsonStr.append("}").append(",");
jsonStr.append("{");
jsonStr.append("\"type\":").append("\"现存治愈\"").append(",");
jsonStr.append("\"num\":").append(exist.get(3)).append(",");
jsonStr.append("\"compare\":").append(increment.get(3));
jsonStr.append("}");
jsonStr.append("]");
return jsonStr.toString();
}
心路历程与收获
221701328张春翔:
我有一定的web开发经验,对服务端和AJAX都有一定的了解,不需要去学习太多的内容,项目开发起来也没有遇到什么困难,比起上次结对作业要轻松不少。但相对来说这次的工作量偏大,又是两人结对,相互之间配合总是不能达到十分默契,在交流的过程中也花费了不少时间。总的来说这次结对作业也收获了很多,学习了新的GitHub使用技巧,获得了合作编程的经验等等。最后感谢队友的配合,期待下次合作。
221701312张庭博:
我的web开发经验不是很多,只能勉强写写HTML这样子,对Bootstrap之类的也不是很懂,因此这次作业对我来说难度是挺大的。刚好春翔是web经验挺多的人,我从他那儿学到了很多东西,像是AJAX,echars等等对我来说不太了解的技术,经过学习之后,也能进行一些前端的开发了。对于爬虫的话,我以前就接触过一些,因此这次的爬虫就是我负责来写,和他讨论好爬虫怎么用才方便后,我进行了爬虫工具的开发,使用工具后能生成log文件,正好与第一次作业相呼应,不用去再去写代码了,压力减小了许多。经过不断地沟通交流,我们的开发过程也是异常顺利,项目如期地完成,项目完成的时刻,我的内心充满了满足感,这一次结对作业可以说是收获颇丰。
评价结对队友
221701328张春翔:
逻辑思维很好,编程能力很强,懂得使用爬虫获取数据,从他身上我学到了很多。
221701312张庭博
前端大佬,可以说是相当可靠的队,实力强,善于沟通,跟他合作我学到了很多知识。