简介:这份全国各流域水质数据集基于环保部门公开数据整理,每日更新,面向环保研究人员、数据科学家及水环境治理相关从业者。压缩包共309个文件,含308个JSON数据文件及1个Markdown说明文档,整体仅2.68MB,JSON按日期存储日度水质监测结果,MD文件用于说明字段结构及使用方式。数据涵盖主要河流、湖泊、水库的pH值、溶解氧、氨氮、化学需氧量、总磷、总氮等多项指标,可支撑污染源解析、趋势追踪与治理效果评估。已有2599人学习下载,配合清晰的文件命名与目录结构,方便按时间序列批量处理,也可结合接口变更记录进行版本适配,是开展中国水环境质量研究与政策制定的实用基础数据。 很多年前我第一次接触水质数据时,想的特别简单:找个网站,把全国各流域的水质数据爬下来,画几张图,完事。结果真正动手才发现,这摊水比想象中深得多——数据源分散、格式混乱、字段命名不统一,有些站点还带反爬,好不容易把数据弄下来,单位又是五花八门,pH、溶解氧、高锰酸盐指数、氨氮、总磷……光是清洗就得折腾掉半条命。
这个项目名就叫water_quality,目标很直接:把全国各流域的水质数据整合成一套统一、干净、可复用的数据集。目前这套流程我已经跑通了完整链路——自动抓取、字段归一化、指数换算、按《地表水环境质量标准》自动定类,最终落库成一张可以直接查的表。这篇文章就完整拆一遍我的技术方案和踩坑实录,给同样被水质数据折磨过的人一个参考。
1. 需求拆解与整体思路
1.1 水质数据的核心痛点
先说个结论:水质数据不算稀缺,稀缺的是“干净”的水质数据。
我最初的目标很简单——拿到全国主要流域监测断面的水质指标,做趋势分析和地域对比。真正搜了一圈之后发现,问题集中在三点:
第一,数据源极度分散。既有国家的公开平台,也有各省市的独立发布渠道,还有论文附录里零散的监测记录。每个地方的发布格式还不一样,有的给 Excel,有的给 HTML 表格,有的干脆是 PDF。
第二,字段标准不统一。同样是监测断面,A 平台叫“高锰酸盐指数”,B 平台叫“CODMn”,C 平台可能连单位都给你标成 mg/L,另一个平台却用 μg/L。同一份数据,不做单位归一化根本没法横向比。
第三,质量参差不齐。缺测值、异常值、重复记录、时间断档……如果把原始数据直接拿来用,分析结果基本没法看。
这个项目的核心思路,就是把上面这些脏活累活一次性搞定——用一套自动化流程,把多源数据统一清洗成标准结构,保证任何人拿到手就能直接分析,不需要再花三天做数据预处理。
1.2 技术选型为什么这么做
工具选型我遵循一条原则:够用、好维护、样例多。
- 抓取层用 Python + requests + BeautifulSoup,遇到需要执行 JS 的动态页面就上 Selenium,覆盖面足够广。
- 数据处理层用 pandas,这个没什么悬念,表格数据清洗的事实标准。
- 存储层用 SQLite,单文件落库,免部署,分享起来拷个文件就行。
- 可视化先用 matplotlib + pyecharts 做探索性分析,后面再按需接 BI 工具。
这一套组合没有特别花哨的组件,但在数据采集这个场景下非常稳定。我试过用 Scrapy 做分布式抓取,对单个数据源来说完全是杀鸡用牛刀。Scrapy 适合大规模爬虫工程,而水质数据这种中小体量、多源异构的场景,requests + BeautifulSoup 反而更灵活,排查问题也更直接。
提示:如果你的目标是做长期巡检或者数据量真的很大(比如每分钟级的水质自动站数据),再上 Scrapy 不迟。日常的分析型项目,别把架构搞复杂了。
2. 数据采集方案与落地细节
2.1 数据源怎么选、怎么判断可靠性
数据源的选择直接决定数据质量的上限。我的筛选标准有三个:有明确的监测依据、公开可访问、持续更新。
我优先选择国家地表水水质自动监测实时数据发布系统这类官方公开渠道,数据权威性有保障,字段相对完整,站点覆盖全国主要流域。省市级的环保部门公开数据作为补充,尤其是国家平台覆盖不到的小流域和湖泊。
还有一个容易被忽略的渠道——论文附录。很多环境科学领域的论文会在附录里放出详细的监测数据,尤其是一些中小河流的数据,平时根本找不到官方发布渠道,但论文里可能有连续几年的完整记录。用正则表达式或者 Tabula 这种 PDF 解析工具,就能把这些数据提取出来。
筛选数据源时保留一个“来源 ID”字段,这样任何一条记录都能回溯到原始出处,方便后续核验。这个习惯值得养成,尤其当数据要用于论文或报告时,溯源能力就是数据的生命线。
2.2 抓取策略:请求频率、解析与断点续传
抓取层的设计要温柔且稳妥。几个关键细节:
请求频率控制:统一走一个带随机延迟的请求函数,延迟区间设置在 1~3 秒,相邻请求之间不完全固定,模拟真人浏览的节奏。抓取频率别太激进,这不是跟谁比赛,数据源被封了才是最麻烦的。
解析容错:每个字段的解析都包在 try/except 里,单个字段解析失败不影响整条记录入库。宁可字段留空,也不能因为一行脏数据中断整个流程。
断点续传:抓取进度记录在一个本地 JSON 文件里,记录每个页面已经抓取到的页码。中断后重新运行脚本,直接从断点继续,不用从头开始。
import requests import time import json from bs4 import BeautifulSoup def safe_get(url, session, retries=3): for attempt in range(retries): try: time.sleep(random.uniform(1, 3)) resp = session.get(url, timeout=15) if resp.status_code == 200: return resp except requests.RequestException as e: print(f"[Retry {attempt+1}] {url} -> {e}") return None def get_resume_point(resume_file): try: with open(resume_file, "r", encoding="utf-8") as f: return json.load(f).get("page", 1) except FileNotFoundError: return 1注意:代码层面做好编码统一,数据源的页面编码可能是 utf-8 也可能是 gbk,解析之前先用 resp.apparent_encoding 做一次判断。这个细节坑了我两次,后来固定成工具函数才消停。
2.3 动态加载页面的处理思路
部分数据平台的表格是后加载的,请求静态 HTML 拿不到任何数据。这种情况无非两条路:一是找后端接口地址直接请求 JSON,二是上浏览器渲染工具。
我的首选方案是前者——打开浏览器开发者工具,切到 Network 面板,翻页操作时盯住 XHR 请求,找到真正返回数据的那个接口。这些接口往往不需要过于复杂的签名参数,直接 requests 请求就能拿到结构化 JSON,省掉了 Selenium 的启动开销。
只有当数据接口本身加密参数太复杂、短时间破解代价太高时,才启用 Selenium 兜底。但 Selenium 每次启动浏览器很耗时,而且占用资源较大,不适合大规模抓取,只作为最后的手段。
3. 数据清洗与标准化实战
3.1 序号换站名、别名规整
拿到原始数据后的第一件事是字段对齐。原始数据的表头可能叫“序号”“站点名称”“pH”这样的中文名,也有的表头直接用英文缩写——“Station”“DO”“CODMn”。我设计了一套字段映射方案,把原始字段统一映射到内部标准字段。
field_mapping = { "序号": "id", "站点名称": "station_name", "pH": "ph", "溶解氧": "do", "高锰酸盐指数": "codmn", "CODMn": "codmn", "氨氮": "nh3n", "总磷": "tp", "总氮": "tn", }站名和指标名的别名问题比想象中严重。同一指标可能有五六种写法,同一站点在不同年份的记录里可能换了名字。这种时候不能靠正则硬扛,直接维护一张映射表,遇到未知名称就停下来人工确认一次,确认过的名字就固定进映射表,下次自动处理。数据清洗的前两天基本都在干这个活。
3.2 单位归一化与浓度换算
这是最容易被忽视的环节。像总磷、总氮这类指标,不同数据源有用 mg/L 的,也有用 μg/L 的,差着三个数量级。如果不做单位归一化,画图的时候会莫名出现几个“异常高值”,查半天发现是单位没换算。
我在内部统一采用 mg/L 作为基准单位(因为国标的限值就是以 mg/L 为单位的),换算逻辑集中在一个函数里:
def normalize_units(value, unit_from, indicator): # 统一转为 mg/L if unit_from == "μg/L": return value / 1000.0 if unit_from == "mg/L": return value # 其他特殊单位按指标处理 return value换算后的数据再根据《地表水环境质量标准》(GB 3838-2002)的限值表,自动给每条记录打上水质类别标签——I 类到劣 V 类,对应从“优”到“重度污染”。有了这个分级字段,后续分析直接按类别聚合就行,省去了频繁查标准表的功夫。
3.3 缺失值处理策略
水质数据的缺失值处理不能一刀切。我总结了一套分层策略:
- 缺测值(数据源明确标注“-”或“缺测”)→ 置为 NaN,不填数。这是最诚实的选择,水质数据本身是周期性波动很明显的指标,擅自用均值填充会抹掉真实变化特征。
- 单次异常跳变(比如 pH = 12.5)→ 先判定为离群值,结合前后两次记录判断是否属于设备故障,确认异常后置为 NaN。
- 连续缺失超过 30% 的断面 → 该断面的时间序列分析价值已经很低,分析时直接剔除,但保留在原始表中备查。
经验:宁可保留缺失,也不要硬造数据。很多下游分析任务对“假数据”的容忍度比“缺数据”低得多。
4. 数据分析与可视化实践
4.1 流域断面水质类别分布
清洗完成后的第一张图,我建议先画水质类别分布。把所有断面按 I~劣 V 类的数量做一个堆叠柱状图,一眼就能看出整体分布形态。
这类图既能验证数据清洗效果(如果劣 V 类占了大半,要么数据源有偏,要么清洗哪里出了问题),也能给后续分析定个基调——到底重点分析“优水区的变化趋势”,还是“污染区的治理效果”。
4.2 时间序列趋势与季节性识别
水质的季节规律非常明显。以氨氮为例,枯水期河流稀释能力下降,氨氮浓度往往走高;丰水期则相反。把断面数据按月聚合画时间序列折线,能清楚看到每年的振荡周期。
有一个细节值得注意:降水稀释与面源冲刷是双重效应,比如暴雨过后的一两天,部分污染物浓度可能不降反升——这是地表径流把岸上污染物冲进河流的结果。所以分析水质趋势时不能只看月均值,要结合降雨事件做事件维度的解析,否则会得出“下雨导致水质变差”这种片面的结论。
我在实践中建议至少做两个时间粒度的分析:年度趋势看治理效果,月度/事件粒度看异常波动。两张图配合使用,才能讲清楚一个流域的水质故事。
4.3 空间分布与热力展示
空间维度的展示比时间维度更适合做汇报——一张流域水质热力图,比十张折线图都有说服力。我选的是 pyecharts + 地图底图的方案,把经纬度信息和综合水质类别映射成地图上的点/面颜色。
需要注意的坑:坐标系的统一。不同数据源的经纬度有的用 GCJ-02(国测局坐标),有的用 WGS-84,混用会导致点位偏移几公里甚至更远。统一转成 WGS-84 后再做地图标注,才能跟底图对上。
绘图方案上,热力图适合区间对比,散点地图适合精确点位展示。如果看流域级别的整体水质,建议用分级设色地图;如果关注具体断面的监测值,用带 tooltip 的散点地图更直观。
5. 常见问题与排查技巧实录
5.1 高频问题速查表
| 问题现象 | 可能原因 | 排查方法 |
|---|---|---|
| 请求返回 403 | 触发服务端反爬策略 | 加 User-Agent / Referer 伪装;降低请求频率 |
| 解析结果全为空 | 页面数据为动态加载 | 打开开发者工具,查找真实 XHR 接口 |
| 中文乱码 | 页面编码判断错误 | 用resp.apparent_encoding覆盖resp.encoding |
| 数据出现数量级异常 | 单位未归一化 | 检查原始单位是 μg/L 还是 mg/L,统一换算 |
| 站点名对不上 | 站点改名或重名 | 维护断面别名映射表,按经纬度辅助匹配 |
| 时间字段解析失败 | 日期格式不统一 | 用pd.to_datetime(..., errors="coerce")统一解析 |
5.2 反爬应对的边界意识
做数据采集要守住两条底线:一是请求频率控制好,别把对方服务器打得喘不过气;二是数据仅用于个人学习研究,不做商业化转售。我宁可慢一点,比如每分钟只请求 60 次以内,也要保证不给数据源添麻烦。
被临时封了 IP 也没必要慌,停半小时再跑通常就好了。真要长期抓取,建议把抓取逻辑和数据存储走本地化——先持久化到 CSV 临时文件,再分批入库,避免长任务中途失败导致内存数据全部丢失。
5.3 数据质量验证的“土办法”
清洗完数据别急着画图。先写几条简单的校验逻辑过一遍:
- 断面总数是否在合理范围(全国范围上千个断面是正常的,个位数就要怀疑抓取遗漏)。
- 时间跨度是否完整(是否有整年缺失)。
- 随机抽几个站点,人工去原始平台核对数值是否一致。
我每次跑完清洗流程都会抽 5 个站点人工核对,用抽样代替全量验证,既保证质量又控制时间成本。别嫌土,这个环节救过我不少次。
6. 项目扩展与实际使用建议
数据清洗完成后,只是拿到了一个数据库而已,真正的生活场景价值还需要配合实际需求去挖掘。
如果你有自己的水质监测设备(比如鱼缸水质检测仪、小型河流环境监测浮标),这套数据可以直接作为背景基准数据来用——把你的观测值和全国同类流域的分布做对比,快速判断自己的数据是否在正常范围内。这也是我目前常态化的使用方式:单独维护一张本地监测表,与water_quality库里的同流域数据做 join 对比。
如果想把项目做成长期巡检工具,可以加上定时调度(比如 cron 或 GitHub Actions 计划任务),每天自动更新一次数据。配合 SQLite 存储天然支持增量插入的特性,不用每次全量重抓。
再往后,可以引入机器学习模型做水质预测。有一个比较成熟的思路:用上游断面的历史水质数据和当天的水文气象数据(降雨量、流量等),训练一个回归模型,预测下游断面的氨氮或溶解氧浓度。这个方向的可行性很高,因为水质数据本身的季节性规律很清晰,特征工程做好之后,模型精度会比想象中好。
最后再分享一个我的个人习惯:这个项目的代码和数据文件我用统一的目录结构归档,raw/放原始抓取内容,processed/放清洗后的标准表,scripts/放各阶段的处理脚本。这样无论是三个月后回来自查,还是同事需要复用数据,都能快速定位,不用再翻聊天记录和网盘找文件。
水质数据这个领域的核心门槛从来不是技术,而是耐心——多源数据、混乱字段、缺失记录,每一道脏活都得一道道过。把标准化流程沉淀下来,后面再做任何跟水环境相关的分析,底气都会不一样。
本文还有配套的精品资源,点击获取