江河养人, 亦能淹人。 水患频发, 水情不可不察。 本文演示如何制作长江水情历史数据报表。
背景
重庆新闻说有洪灾, 湖北新闻说有洪灾, 安徽新闻也说有洪灾, 看到这些新闻, 你猜测也许整个长江都有洪灾, 但对长江汛情, 还是难以形成全局、 连续的认识。
观察发现, 洪灾新闻常常引用长江水文网的数据。 于是你找到长江水文网
, 找到了实时水情的数据。
可惜, 它不提供历史数据, 你看不见过去和未来的趋势。
作品展示
作品网址 https://cxumol.github.io/LongRiver/index.html (数据实时更新)
架构
+-----------+
|Data Source|
+--^--+-----+
| |
+-------+ +-+--v--+ +-------+
|Trigger+---->Fetcher+---->Storage|
+-------+ +-------+ +-^--+--+
| |
+--+--v--+
|Showcase|
+----+---+
|
+----+------+
| User view |
+-----------+
理论上流程是, 定期启动 (Trigger) 一个脚本 (Fetcher) 从数据源 (Data Source) 获得实时数据, 储存到某处 (Storage)。 当人 (User) 访问指定位置 (Showcase) , 就会取得数据并呈现。
所有这些, 都能放在一台服务器上面运行, 但我想用无服务器 “serverless” 的方案。 Serverless 不仅时尚, 而且实惠。
为了 Serverless, 尝试过不少云服务, 比如 crawlab , Task 君 , Glitch , UptimeRobot , DBHub 等。 有些难用, 有些不靠谱。
某天突然开窍想通了, 其实上 GitHub 全家桶就能统统搞定。 实际流程如下。
+-----------+
|Data Source|
+--^--+-----+
| |
+-----------------------------+
| | | Git Repository |
+----------------------------+ |
| +-----------+ | | | | |
| |on: | | +-+--v--+ | +-----------+ |
| | schedule:+---->main.py+--->.json,.csv | |
| | - cron | | +-------+ | +----+------+ |
| +--+--------+ | | | |
| | | +------------+ |
| GitHub Actions | | | .html,.js | |
+----------------------------+ +----+-------+ |
| | |
+-----------------------------+
|
+---v----------+
| GitHub Pages |
+--------------+
脚本用 python 好写; 存数据不用数据库, 用 json 和 csv 存纯文本; 获取数据和生成图表, 交给浏览器客户端去做。 于是, 无论脚本、 数据, 还是展示网页, 统统塞进 Git 仓库就完事了。 然后 GitHub Actions 套 crontab 语法定期调脚本更新数据, GitHub Pages 托管网页。
GitHub 全家桶能覆盖到这个 serverless 的方方面面, 真省心。
爬虫
脚本负责获取、 预处理、 储存、 更新数据。
打开 长江水文网
, F12
找实时水情, 发现是个 <iframe>
, 继而打开 对应网址
, 看源码, 正则匹配就能直接获取 JSON 字串, 从而得到原始数据。 真省心。 详细过程可以参考一篇图文并茂的大佬博客
。
JSON 中每个字段的含义, 最好要理解。 我纯靠自身聪明才智, 猜出来一些。
rvnm = river name = 河流名
stnm = station name = 水站名
tm = time = 时间
q = 流量
z = 水位
oq = out q = 出流量
stcd = station code = 水站站码
wptn = water potential tend? = 水势 (绿4落, 红5涨, 灰6平稳)
各站更新频率不一, 需要避免记录重复项。 以时间戳判断, 未更新的数据丢弃, 已更新的数据才添加为新纪录。
最后一步, 保存。 原始数据存到 json (LongRiver.json) ; 按水站分表, 存到各 csv (河流_水站.csv) 。
这些数据最后保存在这里
数据可视化
库太多, 选起来费劲。 有人说 D3.js 经典靠谱, 有人说 <canvas>
吊打 <svg>
, 有人说 bokeh 结合 py 好, 有人说 ECharts 国货当自强。
挑选库有什么窍门? 希望有大佬指点下。 总之, 看到基于 D3.js 简化的 C3.js 似乎比较简单易学, 于是看文档一路试着用下来, 发现还可以, 就先选它了。
如何使用 C3.js? HTML 引入 c3.min.js、 c3.min.css、 d3.min.js 的 CDN 网址, 然后给 c3.generate() 传入一个对象, 对象里填写生成这个图表所需的一切配置。
欲做大量图表, 先得做一个最朴素、 能看、 只有一座水站的数据图表。 然后扩充、 调整样式。 尽量调好一个图表以后, 再将同样配置扩展到多个图表。
调图表样式, 踩坑部分注解如下:
config = {
data: {
hide: ['oq', 'stcd', 'wptn', 'rvnm', 'stnm'], //不能从 csv 中直接挑选数据列, 但可以隐藏不需要的部分
legend: {
hide: ['oq', 'stcd', 'wptn', 'rvnm', 'stnm'] //lable 需要额外隐藏一次
},
axis: {
x: {
//type: 'timeseries', //这样解析不动 unix epoch 格式的时间, 不知为啥
tick: {
format: function(x) {
return new Date(x).toLocaleDateString('zh-CN');
} //好在允许通过回调函数设置时间格式, 于是就能抛开原本的 `axis.x.tick.type` 不管。
}
}
}
};
调好一个图表, 接下来就能批量生成多个图表, 以及对应标题。 这里通过 JS 操作网页 DOM 的方式完成。 for
循环取得每个文件名(河流_水站.csv), 变量对应到各图表的 数据源 data.url
选项和 绑定HTML元素 bindto
选项。
图表之间的顺序也不能乱排, 于是到地图软件搜索了解各个水站位置, 然后让这组图表大致按照上游到下游的顺序排列。
共 13 个水站, 有两个是水库。 水库可以人为控制蓄水和放水, 所以流量分为入流量和出流量, 而其他水站只有一个流量。 先前按照常规水站调整好的图表, 对于水库型水站需要额外调整。 采取的方式是, 通过文件名是否含有 “水库” 字样判断水站类型, 函数 genChart
多传一个带默认值的参数 type
, 函数内部再由 type
判断更改相关配置, 把"流量"改为"入流量"并添加"出流量"。
GitHub Actions
不像 Travis CI 的 cron jobs 只给你按天、 按周、 按月这几个选项定期运行, GitHub Actions 允许你使用真正的 crontab
语法。 好方便你尽情滥用
GitHub Actions 另一个好处是, 可以引用别人做好的动作包, 动作包在左侧 market place 搜索添加。 这次用到的动作包有:
actions/checkout@v2
开通自身仓库的权限;actions/setup-python@v2
配置 python 环境;EndBug/add-and-commit@v4.2.1
提交对仓库的修改。
此外, 欲调试 workflow, 最好加上 on: workflow_dispatch:
, 这样就能在仓库网页的 Actions
面板手动触发流程。
总结
请看 https://cxumol.github.io/LongRiver/index.html
清晰明了, 有助于形成对长江汛情全局、 连续的认识。
希望能以此帮助灾区朋友正确应对, 减少损失。
起初爬虫写出来后, 没找到合适的云服务运行, 就停止收集数据。 经同学的启发才醒悟到, 早该先在服务器上跑, 数据到手了再说。
改进方向:
找个数据库, 存进去吧, 做个体面人;现在按月份分表, 再也不需要数据库啦;- csv 里的无用字段, 白白浪费了客户端的流量和内存, 应产出简化版 csv, 剔除无用的字段;
- 长江水文网上的水位标记了三种颜色, 表示水势涨落。 C3.js 好像不支持同一折线上的点染不同颜色, 可能无法体现水势, 不知咋办, 要不换个库。
- 现经 GitHub API 获取可用月份, 易触顶, 应替代。
你对此有何意见或建议? 乐意在评论区聆听你的见解。
作业
请尝试回答以下问题:
- c3.js, d3.js, bokeh.js, chart.js 这些图表绘制库分别有什么缺点? 你有什么图表库推荐?
- 使用 GitHub 全家桶, 和分散使用多方云服务相比, 有什么缺点?
- 不用数据库, 用 json 保存数据, 有什么缺点?
欢迎在下方评论区留言你的解答或想法。
上期作业参考答案
- 点击按钮挂载, 发现这个挂载是只读模式的, 不香。
- 可能不是常见编码, 转码就行。 Colab 上就能转码, 就是慢。
- 自动生成字幕无需额外操作, 多等一会儿就行。 但示例视频不能自动生成字幕, 因为 Google 官网帮助页 说 “自动字幕目前支持英语、荷兰语、法语、德语、意大利语、日语、韩语、葡萄牙语、俄语和西班牙语。”, 而示例视频的语音为汉语普通话(洋腔洋调), 不在此列。