这个是去年9月份,大三上学期《大数据技术应该开发》的课程设计,这个作品我很早就想做了,但是因为课业加上项目,所以一直耽搁,所以就当成大数据课程作业做了。虽然做了一个月,只是因为个人技术菜,其实做了一小部分的功能,我也拿着这个作品去参加了很多的比赛,目前拿到了校内比赛“大数据应用大赛”的一等奖,还有另外一个比赛的三等奖。
其他有想法的小伙伴可以加上一些其他的功能,完善之后可以去参加计算机设计大赛,单拿这个去参赛还是比较单薄的,加上自己的想法完善一下也是一个不错的选择。
加上自己的想法,换上一个好看的模板(之前上传一些模板),继续优化优化,让内容更加丰富,就可以去参加很多比赛了。
资料包括:
源码下载:基于网络爬虫的新冠肺炎疫情数据可视化分析。
资源下载:可视化大屏模板。
唉……大学才四年,疫情占三年。
Spring Boot、MyBatis、Druid、HttpClient、Jsoup、Fastjson、MySQL、Maven。
ECharts、Vue.js、Axios、JavaScript、CSS、HTML。
基于爬虫技术在 https://ncov.dxy.cn/ncovh5/view/pneumonia 爬取的疫情数据,之后使用 Jsoup 解析器对数据进行解析处理得到原始的 JSON 数据,再将 JSON 数据转化为Java 实体类并进行持久化处理,基于ECharts对数据进行可视化展示。
为达到对疫情数据实时监控的效果,采用实时爬取数据的方式,开启 Spring Boot 的定时任务并将项目部署至阿里云服务器,于每日早上八点和晚上十点各爬取一次数据,并对数据进行处理和持久化操作,全天不间断地向前端输出接口,以保证数据的时效性、精确性。
数据持久化使用MyBatis很简单,数据展示使用ECharts也没什么难度。
稍有难度就是数据的爬取和处理了,所以在此处简单地将一下。
后端项目目录。
首先,创建一个Spring Boot工程项目,导入HttpClient和Jsoup的依赖。
TimeUtils.java工具类源码:
/**
* Author Mr.Zhang
* Date 2020/5/27 11:23
* Desc 时间工具类
*/
public abstract class TimeUtils {
public static String format(Long timestamp, String pattern) {
return FastDateFormat.getInstance(pattern).format(timestamp);
}
}
HttpUtils.java工具类源码:
/**
* Author Mr.Zhang
* Date 2021/9/10 16:05
* Desc 封装HttpClient工具,方便爬取网页内容
*/
@Component
public class HttpUtils {
//声明httpClient管理器对象(HttpClient连接池)
private static PoolingHttpClientConnectionManager cm = null;
private static RequestConfig config = null;
private static List<String> userAgentList = null;
//静态代码块会在类被加载的时候执行
static {
cm = new PoolingHttpClientConnectionManager();
cm.setMaxTotal(200);
cm.setDefaultMaxPerRoute(20);
config = RequestConfig.custom()
.setSocketTimeout(10000)
.setConnectTimeout(10000)
.setConnectionRequestTimeout(10000)
.build();
userAgentList = new ArrayList<String>();
userAgentList.add("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.132 Safari/537.36");
userAgentList.add("Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:73.0) Gecko/20100101 Firefox/73.0");
userAgentList.add("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_3) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.5 Safari/605.1.15");
userAgentList.add("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36 Edge/16.16299");
userAgentList.add("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36");
userAgentList.add("Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:63.0) Gecko/20100101 Firefox/63.0");
}
/**
* 获取页面的HTML的源码字符串
*
* @param url 页面的URL地址
* @return HTML的源码字符串
*/
public String getHtml(String url) {
//1.从连接池中获取HttpClient对象
CloseableHttpClient httpClient = HttpClients.custom().setConnectionManager(cm).build();
//2.创建HttpGet对象
HttpGet httpGet = new HttpGet(url);
//3.设置请求配置对象和请求头
httpGet.setConfig(config);
httpGet.setHeader("User-Agent", userAgentList.get(new Random().nextInt(userAgentList.size())));
//4.发起请求
CloseableHttpResponse response = null;
try {
response = httpClient.execute(httpGet);
//5.获取响应内容
if (response.getStatusLine().getStatusCode() == 200) {
String html = "";
if (response.getEntity() != null) {
html = EntityUtils.toString(response.getEntity(), "UTF-8");
}
return html;
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (response != null) {
try {
response.close();
} catch (IOException e) {
e.printStackTrace();
}
}
//httpClient.close();//注意:这里的HttpClient是从cm(连接池)中获取的,不需要关闭
}
return null;
}
}
分析:封装HttpClient工具类,对HttpClient连接池进行配置,对外提供一个静态方法getHtml(String url),方便爬取网页的内容。
传入https://ncov.dxy.cn/ncovh5/view/pneumonia,作为getHtml的参数。
Covid19DataCrawler.java的源码:
/**
* 实现疫情数据爬取
*/
@Component
public class Covid19DataCrawler {
private static final Logger logger = LoggerFactory.getLogger(Covid19DataCrawler.class);
@Autowired
private HttpUtils httpUtils;
@Autowired
private ProvinceCovidService provinceCovidService;
@Autowired
private CityCovidService cityCovidService;
@Autowired
private StatisticsDataService statisticsDataService;
//后续需要将该方法改为定时任务,如每天8点定时爬取疫情数据
//@Scheduled(initialDelay = 1000, fixedDelay = 1000 * 60 * 60 * 24)
// @Scheduled(cron = "0 8 8,0 * * ?")
@Scheduled(cron = "0 22 20,0 * * ?") //每天早的八点半,晚十点半,各更新执行一次
public void crawling() {
//获取当前时间
String dateTime = TimeUtils.format(System.currentTimeMillis(), "yyyy-MM-dd");
logger.info("当前系统的时间为:{}", dateTime);
//1.爬取指定页面
String html = httpUtils.getHtml("https://ncov.dxy.cn/ncovh5/view/pneumonia");
//2.解析页面中指定内容
//获取 id = getAreaStat 的全国疫情数据
Document document = Jsoup.parse(html);
String text = document.select("script[id=getAreaStat]").toString();
//3.使用正则表达式获取json格式的疫情数据
//定义正则规则
String regex = "\\[(.*)\\]";
//编译成正则对象
Pattern pattern = Pattern.compile(regex);
//去text中进行匹配
Matcher matcher = pattern.matcher(text);
String jsonStr = "";
if (matcher.find()) {
jsonStr = matcher.group(0);
//由各省份组成的JSON字符串,包含省份和下属城市
logger.info("已经爬取到截止至{}数据", dateTime);
} else {
logger.info("no match");
}
//对JSON数据进行进一步解析
//4.将第一层json(省份数据)解析为JavaBean
List<CovidData> provincesCovidDataList = JSON.parseArray(jsonStr, CovidData.class); //由各个省份CovidData组成的List集合
logger.info("已经爬取到 {} 个省份的数据", provincesCovidDataList.size()); //全国34个省(包括港澳台)
Integer provinceCount = 0; //省份的计数变量
for (CovidData provinceCovidData : provincesCovidDataList) { //provinceCovidData为省份的数据
provinceCovidData.setDatetime(dateTime); //设置省份实体的时间
provinceCount++;
//5.获取每个省份的每一天的新冠数据的统计数据
//System.out.println("count-- +" + count + "每天统计数据的URL:" + provinceCovidData.getStatisticsData()); //一共有34条URL,对应34个省份
String provinceStatisticsDataJsonStr = httpUtils.getHtml(provinceCovidData.getStatisticsData());
//5.1、provinceStatisticsDataJsonStr有两个字段,我们获取其中的“data”字段,“data”字段的值是某省每一天的统计数据(JSON数据格式)
String statisticsDataStr = JSON.parseObject(provinceStatisticsDataJsonStr).getString("data");
//5.2、把解析出来的每一天的统计数据设置回省份provinceCovidData的statisticsData中,因为之前存放的只是一个URL路径
// provinceCovidData.setStatisticsData(statisticsDataStr); //这个字段太长了,所以新建一个表单独分开
/*
CovidData的省份部分数据已经爬取完毕,实例化新冠数据省份实体类ProvinceCovid
*/
ProvinceCovid provinceCovid = new ProvinceCovid();
provinceCovid.setProvinceName(provinceCovidData.getProvinceName());
provinceCovid.setProvinceShortName(provinceCovidData.getProvinceShortName());
provinceCovid.setCurrentConfirmedCount(provinceCovidData.getCurrentConfirmedCount());
provinceCovid.setConfirmedCount(provinceCovidData.getConfirmedCount());
provinceCovid.setSuspectedCount(provinceCovidData.getSuspectedCount());
provinceCovid.setCuredCount(provinceCovidData.getCuredCount());
provinceCovid.setDeadCount(provinceCovidData.getDeadCount());
provinceCovid.setLocationId(provinceCovidData.getLocationId());
provinceCovid.setHighDangerCount(provinceCovidData.getHighDangerCount());
provinceCovid.setMidDangerCount(provinceCovidData.getMidDangerCount());
provinceCovid.setDatetime(provinceCovidData.getDatetime());
provinceCovidService.addProvinceCovid(provinceCovid); //将省份数据存入数据库中
logger.info("已持久化第 {} 个省份的数据:{} ", provinceCount, provinceCovid.getProvinceName());
logger.info("{} 当前确诊人数:{}", provinceCovid.getProvinceName(), provinceCovid.getCurrentConfirmedCount());
logger.info("{} 累计确诊人数:{}", provinceCovid.getProvinceName(), provinceCovid.getConfirmedCount());
logger.info("{} 疑似病例人数:{}", provinceCovid.getProvinceName(), provinceCovid.getSuspectedCount());
logger.info("{} 治愈人数:{}", provinceCovid.getProvinceName(), provinceCovid.getCuredCount());
logger.info("{} 死亡人数:{}", provinceCovid.getProvinceName(), provinceCovid.getDeadCount());
logger.info("统计时间:{}", provinceCovid.getDatetime());
//5.3、把provinceStatisticsDataJsonStr解析成StatisticsData实体对象集合
List<StatisticsData> statisticsDataList = JSON.parseArray(statisticsDataStr, StatisticsData.class);
for (StatisticsData statisticsData : statisticsDataList) {
//psId=null, provinceId=null, provinceName='null',dateId='20210703',其中,psId不用管,
String provinceName = provinceCovidData.getProvinceName();
statisticsData.setProvinceName(provinceName);//设置provinceName
/*
将StatisticsData实体对象存入到数据库中
*/
statisticsDataService.addStatisticsData(statisticsData);
}
logger.info("统计数量:{}", statisticsDataList.size());
logger.info("已持久化 {} 的统计数据", provinceCovidData.getProvinceName());
//6、获取每个省份的下属城市的数据
String citiesCovidDataJsonStr = provinceCovidData.getCities(); //每个省份包含城市组成的JSON字符串
//System.out.println(provinceCovidData.getProvinceName() + "的下属城市:"); //可以正常打印出34个省份的各个城市
//6.1、解析省份下属的城市
List<CityCovid> citiesCovidDataList = JSON.parseArray(citiesCovidDataJsonStr, CityCovid.class); //由当前省份下属城市CityCovid组成的List集合
for (CityCovid cityCovid : citiesCovidDataList) {
cityCovid.setDatetime(dateTime); //给城市实体设置时间
cityCovid.setProvinceId(provinceCovidService.getProvinceIdByProvinceName(provinceCovidData.getProvinceName())); //设置城市所属的省份id
/*
将当前省份的下属城市数据存入数据库中
*/
cityCovidService.addCityCovid(cityCovid);
logger.info("已持久化 {} 的 {} 的数据", provinceCovidData.getProvinceName(), cityCovid.getCityName());
}
}
}
}
分析:使用Jsoup的id选择器,获取"script[id=getAreaStat]"
疫情数据,再使用正则表达式"\\[(.*)\\]"
对疫情数据进行匹配,获取到由每个省份的新冠肺炎疫情数据组成的JSON数组。
//1.爬取指定页面
String html = HttpUtils.getHtml("https://ncov.dxy.cn/ncovh5/view/pneumonia");
//2.解析页面中指定内容
//获取 id = getAreaStat 的全国疫情数据
Document document = Jsoup.parse(html);
String text = document.select("script[id=getAreaStat]").toString();
//3.使用正则表达式获取json格式的疫情数据
String regex = "\\[(.*)\\]"; //定义正则规则
Pattern pattern = Pattern.compile(regex); //编译成正则对象
Matcher matcher = pattern.matcher(text); //去text中进行匹配
其中“statisticsData”字段的值是一个URL地址,需要再解析一次,如图所示。解析后得到JSON字符串,我们再拿到该字符串的data字段即可。
使用JSON校验工具对JSON数据进行格式化。
根据爬取得到的新冠肺炎疫情数据的JSON字符串的字段,创建省份、城市和统计数据三个实体类,将JSON数据的字段与实体类的属性一一对应,再使用Fastjson工具进行转换。
/**
* 省份实体类
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class ProvinceCovid implements Serializable {
private Integer provinceId; //省份id
private String provinceName; //省份名称
private String provinceShortName; //省份短名
private Integer currentConfirmedCount; //当前确诊人数
private Integer confirmedCount; //累记确诊人数
private Integer suspectedCount; //疑似病例人数
private Integer curedCount; //治愈人数
private Integer deadCount; //死亡人数
private Integer locationId; //当前省份的位置id
private Integer highDangerCount; //高风险地区
private Integer midDangerCount; //低风险地区
private String datetime; //时间
}
/**
* 城市实体类
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class CityCovid implements Serializable {
private Integer cityId; //城市id
private Integer provinceId; //所属省份id
private String cityName; //城市名
private Integer locationId; //城市的位置id
private Integer currentConfirmedCount; //当前确诊人数
private Integer confirmedCount; //累记确诊人数
private Integer suspectedCount; //疑似病例人数
private Integer curedCount; //治愈人数
private Integer deadCount; //死亡人数
private Integer highDangerCount; //高风险地区
private Integer midDangerCount; //中风险地区
private String datetime; //时间
}
/*
统计数据实体类
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class StatisticsData implements Serializable {
private Integer psId; //统计数据id
private String provinceName; //所属省份名
private Integer confirmedCount; //累计确诊人数
private Integer confirmedIncr; //新增累计确诊人数
private Integer curedCount; //治愈人数
private Integer curedIncr; //新增治愈人数
private Integer currentConfirmedCount; //当前确诊人数
private Integer currentConfirmedIncr; //新增当前确诊人数
private Integer deadCount; //死亡人数
private Integer deadIncr; //死亡人数新增
private Integer highDangerCount; //高风险地区
private Integer midDangerCount; //中风险地区
private Integer suspectedCount; //疑似病例
private Integer suspectedCountIncr; //新增疑似病例
private String dateId; //时间格式:yyyyMMdd,如20200119
}
Fastjson工具包提供了一个抽象类JSON,该类提供了两个方法,parseArray(String jsonStr)和parseObject(),根据需要调用即可。
//将JSON字符串转化为Class类实例化对象的List集合。
public static <T> java.util.List<T> parseArray(@Nullable String text, Class<T> clazz);
//将JSON字符串转化为Class类的实例化对象。
public static <T> T parseObject(@Nullable String text,Class<T> clazz);
至5.5,数据已经封装到Java的实体类中了,接下来就是常规的MyBatis持久化存储,然后通过Controller提供接口给前端进行数据可视化展示。
也就是常规的Controller、Service、Dao层代码的编写,这部分就不说了,不是重点,也没难度。
接下来以StatisticsDataController.java为例简单演示。
@Controller
@RequestMapping("/statisticsData")
public class StatisticsDataController {
private final static Logger logger = LoggerFactory.getLogger(CityCovidController.class);
@Autowired
private ProvinceCovidService provinceCovidService;
@GetMapping("/getCountrySumDeadCount")
@ResponseBody
public String getCountrySumDeadCount() {
Integer sumDeadCount = provinceCovidService.getSumDeadCount();
JSONObject jsonObject = new JSONObject();
jsonObject.put("sumDeadCount", sumDeadCount);
jsonObject.put("comment", "全国累计死亡人数");
logger.info("/statisticsData/getCountrySumDeadCount");
logger.info("已获取到全国累计死亡人数:{}", sumDeadCount);
return jsonObject.toJSONString();
}
@GetMapping("/getCountrySumCuredCount")
@ResponseBody
public String getCountrySumCuredCount() {
Integer sumCuredCount = provinceCovidService.getSumCuredCount();
JSONObject jsonObject = new JSONObject();
jsonObject.put("sumCuredCount", sumCuredCount);
jsonObject.put("comment", "全国累计治愈人数");
logger.info("/statisticsData/getCountrySumCuredCount");
logger.info("已获取到全国累计治愈人数:{}", sumCuredCount);
return jsonObject.toJSONString();
}
@GetMapping("/getCountrySumCurrentConfirmedCount")
@ResponseBody
public String getCountrySumCurrentConfirmedCount() {
Integer sumCurrentConfirmedCount = provinceCovidService.getSumCurrentConfirmedCount();
JSONObject jsonObject = new JSONObject();
jsonObject.put("sumCurrentConfirmedCount", sumCurrentConfirmedCount);
jsonObject.put("comment", "全国现存确诊人数");
logger.info("/statisticsData/getCountrySumCurrentConfirmedCount");
logger.info("已获取到全国现存确诊人数(当前确诊人数):{}", sumCurrentConfirmedCount);
return jsonObject.toJSONString();
}
@GetMapping("/getCountrySumConfirmedCount")
@ResponseBody
public String getCountrySumConfirmedCount() {
Integer sumConfirmedCount = provinceCovidService.getSumConfirmedCount();
JSONObject jsonObject = new JSONObject();
jsonObject.put("sumConfirmedCount", sumConfirmedCount);
jsonObject.put("comment", "全国累计确诊人数");
logger.info("/statisticsData/getCountrySumConfirmedCount");
logger.info("已获取到全国累计确诊人数:{}", sumConfirmedCount);
return jsonObject.toJSONString();
}
@GetMapping("/getCountrySumHighDangerCount")
@ResponseBody
public String getCountrySumHighDangerCount() {
Integer sumHighDangerCount = provinceCovidService.getSumHighDangerCount();
JSONObject jsonObject = new JSONObject();
jsonObject.put("sumHighDangerCount", sumHighDangerCount);
jsonObject.put("comment", "全国现存高风险地区的数量");
logger.info("/statisticsData/getCountrySumHighDangerCount");
logger.info("已获取到全国现存高风险地区的数量:{}", sumHighDangerCount);
return jsonObject.toJSONString();
}
@GetMapping("/getCountrySumMidDangerCount")
@ResponseBody
public String getCountrySumMidDangerCount() {
Integer sumMidDangerCount = provinceCovidService.getSumMidDangerCount();
JSONObject jsonObject = new JSONObject();
jsonObject.put("sumMidDangerCount", sumMidDangerCount);
jsonObject.put("comment", "全国现存中风险地区的数量");
logger.info("/statisticsData/getCountrySumMidDangerCount");
logger.info("以获取到全国现存中风险地区的数量:{}", sumMidDangerCount);
return jsonObject.toJSONString();
}
@GetMapping("/getCountrySumSuspectedCount")
@ResponseBody
public String getCountrySumSuspectedCount() {
Integer sumSuspectedCount = provinceCovidService.getSumSuspectedCount();
JSONObject jsonObject = new JSONObject();
jsonObject.put("sumSuspectedCount", sumSuspectedCount);
jsonObject.put("comment", "全国现存疑似病例");
logger.info("/statisticsData/getCountrySumSuspectedCount");
logger.info("全国现存疑似病例:{}", sumSuspectedCount);
return jsonObject.toJSONString();
}
}
至此,这个小项目算是完成了……