[Ktor] 实现疫情地图

这一阵子被武汉肺炎搞得完全不敢出门,说好的要去重庆看某人也只能暂时搁置了[手动狗头]。不过身为程序员还是停不住折腾,就画个疫情地图吧。

当然我们没有一手数据,就先从腾讯这拿了,通过抓包可以知道,疫情地图的数据来自以下 URL:

$ curl 'https://view.inews.qq.com/g2/getOnsInfo?name=wuwei_ww_area_counts&callback=&_='

在 URL 最后跟上一个 13 位的时间戳就好了。

那么如何把这些数据变成地图来呈现呢,下面我们就来简单的做个项目吧


一、建立 Ktor 项目

如果你使用我以前写的 KtGen 来生成项目,是一点都不麻烦的,完事后可以将 application.conf 的内容改成以下,因为我们这个项目不需要 https 部署,也不需要数据库:

ktor {
    deployment {
        port = 80
        port = ${?PORT}
    }
    application {
        modules = [ com.rarnu.ncov.ApplicationKt.module ]
    }
}

接着就可以直接把项目编译通过:

$ gradle build

二、获取疫情数据

这个也很简单了,上面已经给出了相关的 URL,我们只需要简单的请求,取回数据后进行包装即可:

private val dataUrl: String get() = "https://view.inews.qq.com/g2/getOnsInfo?name=wuwei_ww_area_counts&callback=&_=${System.currentTimeMillis()}"

get("/map") {
    call.respondText {
        try {
            JSONArray(JSONObject(HttpClient().get(dataUrl)).optString("data")).filter { country ->
                (country as JSONObject).optString("country") == "中国"
            }.groupBy { area ->
                (area as JSONObject).optString("area")
            }.mapValues { confirm ->
                confirm.value.sumBy { item ->
                    (item as JSONObject).optInt("confirm", 0)
                }
            }.stringIntToJson()
        } catch (th: Throwable) {
            "[]"
        }
    }
}

可能有一些同学对这种写法比较陌生,稍做解释:

// 请求获得疫情数据,解析 JSON 后获取其中的 data 数据,并再次解析为一个 JSONArray
JSONArray(JSONObject(HttpClient().get(dataUrl)).optString("data")).filter { country ->
    // 过滤出中国的数据,最终得到 List
    (country as JSONObject).optString("country") == "中国"
}.groupBy { area ->
    // 按照区域分组,最终得到 Map>
    (area as JSONObject).optString("area")
}.mapValues { confirm ->
    // 对每个分组里的经确诊的感染人数进行求和,最终得到 Map
    confirm.value.sumBy { item ->
        (item as JSONObject).optInt("confirm", 0)
    }
}.stringIntToJson()

最后一步将 Map 转换为 Json 字符串,用于返回给用户,转换函数如下:

fun Map.stringIntToJson() = """[${toList().joinToString(",") { """{"name":"${it.first}","value":${it.second}}""" }}]"""

现在把项目跑起来就可以在浏览器里获取到数据了:

$ gradle run

在浏览器里请求 http://0.0.0.0/map 就可以得到以下数据了:

[
    {"name":"湖北","value":4523},
    {"name":"广东","value":354},
    {"name":"浙江","value":428},
    {"name":"重庆","value":180},
    {"name":"湖南","value":277},
    {"name":"安徽","value":200},
    {"name":"北京","value":114},
    {"name":"上海","value":112},
    {"name":"河南","value":278},
    {"name":"四川","value":142},
    {"name":"山东","value":158},
    {"name":"广西","value":78},
    {"name":"江西","value":168},
    {"name":"福建","value":101},
    {"name":"江苏","value":129},
    {"name":"海南","value":46},
    {"name":"辽宁","value":41},
    {"name":"陕西","value":63},
    {"name":"云南","value":70},
    {"name":"天津","value":29},
    {"name":"黑龙江","value":43},
    {"name":"河北","value":65},
    {"name":"山西","value":35},
    {"name":"香港","value":10},
    {"name":"贵州","value":11},
    {"name":"吉林","value":9},
    {"name":"甘肃","value":26},
    {"name":"宁夏","value":12},
    {"name":"台湾","value":9},
    {"name":"新疆","value":14},
    {"name":"澳门","value":7},
    {"name":"内蒙古","value":18},
    {"name":"青海","value":6},
    {"name":"西藏","value":1}
]

三、数据可视化

只拿到 Json 还是太原始了,我们得把中国地图画出来,这里我选用 echarts.js 来实现,同时也已经有开源的 china.js 可供使用,所以这项工作就变得非常简单了。

首先完成一个页面,最简单的就好:



    
    
    
    


然后写一点 js 就完事了:

function showMap() {
    $.ajax({
        url: '/map',
        dataType: 'json',
        success: (res) => {
            let optionMap = {
                backgroundColor: '#FFFFFF',
                title: {
                    text: '全国疫情数据',
                    x:'center'
                },
                tooltip: { trigger: 'item'},
                visualMap: {
                    show: true,
                    x: 'right',
                    y: 'center',
                    splitList: [{start: 1000},{start: 500, end: 999},{start: 100, end: 499},{start: 10, end: 99},{start: 1, end: 9},{start: 0, end: 0}],
                    color: ['#7D0000','#D52F30','#F4664C','#FFA477','#FFD5C0','#FFF1D5']
                },
                series: [{
                    name: '确诊人数',
                    type: 'map',
                    mapType: 'china',
                    roam: false,
                    label: {
                        normal: { show: true},
                        emphasis: { show: false}
                    },
                    data:res
                }]
            };
            let chart = echarts.init(document.getElementById('map'));
            chart.setOption(optionMap);
        }
    });
}

好了,现在运行项目就可以看到页面啦:

[Ktor] 实现疫情地图_第1张图片

四、每日疫情数折线图

同样的,再写一个接口用于获取数据:

private val dailyUrl: String get() = "https://view.inews.qq.com/g2/getOnsInfo?name=wuwei_ww_cn_day_counts&callback=&_=${System.currentTimeMillis()}"

get("/daily") {
    call.respondText {
        try {
            val mDate = mutableListOf()
            val mConfirm = mutableListOf()
            val mSuspect = mutableListOf()
            val mDead = mutableListOf()
            val mHeal = mutableListOf()
            // 请求获得每日情况数据,解析 JSON 后获取其中的 data 数据,并再次解析为一个 JSONArray
            JSONArray(JSONObject(HttpClient().get(dailyUrl)).optString("data")).sortedBy { item ->
                // 按日期进行排序,最终得到 List
                (item as JSONObject).getString("date")
            }.forEach { item ->
                // 将数据填到列表里
                with(item as JSONObject) {
                    mDate.add(getString("date").trim())
                    mConfirm.add(getString("confirm").trim().toInt())
                    mSuspect.add(getString("suspect").trim().toInt())
                    mDead.add(getString("dead").trim().toInt())
                    mHeal.add(getString("heal").trim().toInt())
                }
            }
            // 将数据拼装成 json 返回
            """{"date":${mDate.stringListToJson()},"confirm":${mConfirm.intListToJson()},"suspect":${mSuspect.intListToJson()},"dead":${mDead.intListToJson()},"heal":${mHeal.intListToJson()}}"""
            } catch (th: Throwable) {
                """{"date":[],"confirm":[],"suspect":[],"dead":[],"heal":[]}"""
            }
        }
    }

同样的,前端依然用 echarts.js 来制作图表:

function showDaily() {
    $.ajax({
        url: '/daily',
        dataType: 'json',
        success: (res) => {
            let optionMap = {
                tooltip: {trigger: 'axis' },
                legend: { data: ['确诊','疑似','死亡','治愈'] },
                xAxis: [{
                        type: 'category',
                        boundaryGap: false,
                        data: res.date
                    }],
                yAxis: [{type : 'value'}],
                series: [
                    {name: '确诊',type: 'line',data: res.confirm,color: '#D52F30'},
                    {name: '疑似',type: 'line',data: res.suspect,color: '#FFA477'},
                    {name: '死亡',type: 'line',data: res.dead,color: '#848586'},
                    {name: '治愈',type: 'line',data: res.heal,color: '#64CC98'}
                ]
            };
            let chart = echarts.init(document.getElementById('daily'));
            chart.setOption(optionMap);
        }
    });
}

完成后效果如下所示:

[Ktor] 实现疫情地图_第2张图片

五、各城市数据列表

同样的,这个列表也来自于上面的 dataUrl,发起请 求并获取数据即可,不同的地方在于地区下面要有城市列表,并且展示每个城市(区)所对应的数据。我们可以简单的予以处理:

get("/detail") {
    call.respondText {
        try {
            // 请求获得疫情数据,解析 JSON 后获取其中的 data 数据,并再次解析为一个 JSONArray
            JSONArray(JSONObject(HttpClient().get(dataUrl)).optString("data")).filter { country ->
                // 过滤出中国的数据,最终得到 List
                (country as JSONObject).optString("country") == "中国"
            }.groupBy { area ->
                // 按照区域分组,最终得到 Map>
                (area as JSONObject).optString("area")
            }.mapKeys { item ->
                // 更改 map key,将 key 改为一个包含了求和后数据的 Json,最终得到 Map>
                val sumConfirm = item.value.sumBy { i -> (i as JSONObject).optInt("confirm", 0) }
                val sumDead = item.value.sumBy { i -> (i as JSONObject).optInt("dead", 0) }
                val sumHeal = item.value.sumBy { i -> (i as JSONObject).optInt("heal", 0) }
                DataArea(item.key, sumConfirm, sumDead, sumHeal)
            }.mapValues { item ->
                // 对 area 下属城市,按确诊人数进行逆序排序
                item.value.sortedByDescending { i -> (i as JSONObject).optInt("confirm") }.map { i ->
                    // 更改 map value,将 value 改为一个包含了 area 对应下属城市的 List,最终得到 Map>
                    with(i as JSONObject) {
                        DataCity(getString("city"), getInt("confirm"), getInt("dead"), getInt("heal"))
                    }
                }
            }.dataToJson()
        } catch (th: Throwable) {
            "[]"
        }
    }
}

其中 dataToJson 扩展的代码如下:

fun List.cityToJson() = """[${joinToString(",") { """{"city":"${it.city}","confirm":${it.confirm},"dead":${it.dead},"heal":${it.heal}}""" }}]"""
fun Map>.dataToJson() = """[${toList().joinToString(",") { """{"area":"${it.first.area}","confirm":${it.first.confirm},"dead":${it.first.dead},"heal":${it.first.heal},"cities":${it.second.cityToJson()}}""" }}]"""

最后,同样用 js 写出一个获取数据并包装出 UI 的函数,此处不再赘述。

最终实现的效果如下:

[Ktor] 实现疫情地图_第3张图片

最后我们只需要把页面拼成一个就结束了,当然为了保险起见,避免太多次请求,在真实项目里是需要做缓存的。

我在这里提供完整项目供大家下载参考,请移步去 Github/rarnu/nCoVMap 啦!

你可能感兴趣的:([Ktor] 实现疫情地图)