大家通常怎么做:直奔监测总站那张实时发布页
空气质量公开数据怎么采?最省事的一条路是 Open-Meteo 的空气质量 API:免密钥、支持历史与预报、一次请求能批量取多个城市,底层是哥白尼大气监测服务 CAMS,CC BY 4.0 授权。下面把我实采四城的真实返回值和踩过的坑都摊开讲。
但先说清楚,我不是一开始就这么干的。空气质量数据是公开的——这句话没错,很多人理解成了"那我应该能随便下载"。
我的第一反应和大家一样:打开中国环境监测总站的全国城市空气质量实时发布平台(air.cnemc.cn:18007),页面上北京市的万寿西宫、农展馆、海淀万柳、奥体中心这些点位一个个列着,AQI、PM2.5、PM10、SO₂、NO₂、CO、O₃ 七项全都有,还带健康影响提示。数据长得这么规整,写个爬虫抓下来不就完了?
为什么这条路走不通:看得见的没有历史,有历史的拿不到
花了两晚上,我确认了三件事。
第一,那个页面是前端渲染的,地址还挂在 18007 这种非标准端口上。你用最朴素的方式把 HTML 拉下来,拿到的是个空壳,表格里一个数字都没有。要拿就得去分析它的接口、处理它那套请求,成本和稳定性都不划算。
第二,实时发布平台只管当下。想做趋势,你得找历史。国家地球系统科学数据中心有一份"中国空气质量数据集(2014-2020 年)",覆盖全国 360 个城市的逐日数据,含 PM2.5、PM10、SO₂、NO₂、O₃、CO、AQI,数据是完整的——但它走的是在线订单 + 用途审核,不是点一下就下载。国家地球系统科学数据中心黄土高原分中心之外,noda.ac.cn 上那份"2014 年-2022 年中国城市空气污染站点数据"更细,171 万条逐小时记录、站点级,但它的共享方式明确写着离线申请、公益性访问、授权分发范围为国内。
第三,也是最容易被忽略的:这些是站点观测值,背后是一套国控监测点位体系。你要做论文、做达标判定、做城市排名,必须用它。但它不适合"我现在就想跑一遍四城对比看看"这种场景。
一句话总结:能白拿的没有历史,有历史的要审,审下来的又重得要命。
换个思路:先用模式再分析数据,别一上来就磕观测站
我改了方向。如果你的诉求是趋势对比、教学演示、模型练手,其实完全可以先拿模式再分析数据垫底,等口径跑通了再去申请观测数据做校正。省下的时间不止一点半点。
具体用的是 Open-Meteo Air Quality API。它底层接的是哥白尼大气监测服务(Copernicus Atmosphere Monitoring Service,CAMS),三个优点正好打在上面三个痛点上:
- 免密钥:HTTP GET 直接请求,不用注册、不用填信用卡,返回一个干净的 JSON。
- 能要历史:
past_days参数回拉过去若干天的逐小时序列,forecast_days拿未来预报,一次请求两头都有。 - 能批量:经纬度支持逗号分隔,多个城市一次请求拿全,返回体变成数组。
口径必须先搞清楚,不然后面全是错的
这是最要命的一步。官方文档给出的两组参数我抄在这里(以仓库最新说明为准):
| 数据集 | 区域 | 空间分辨率 | 时间分辨率 | 起始时间 | 更新频率 |
|---|---|---|---|---|---|
| CAMS 欧洲空气质量预报 | 欧洲 | 0.1°(约 11 km) | 逐小时 | 2023 年 10 月起 | 每 24 小时一次,4 天预报 |
| CAMS 全球大气成分预报 | 全球 | 0.4°(约 45 km) | 3 小时 | 2022 年 8 月起 | 每 12 小时一次,5 天预报 |
两点提醒。一是它输出的是模式结果,不是站点观测。45 km 网格意味着北京城区和近郊很可能落在同一个格点里,你拿它跟监测站发布的城市 AQI 做逐小时核对,对不上是正常的,不是数据错了。这一条和我们做指数采集时踩过的坑一模一样——地域口径对不上,数据就完全不可比,混在一张表里后面所有环比都是假的。
二是用途边界。模式再分析数据适合看大尺度时空趋势、做演示、做模型输入,不适合替代本地校准传感器,更不能拿去做监管判定或个人健康建议。
授权与署名
Open-Meteo 的公开数据采用 CC BY 4.0 授权,可免费使用与再分发,含商业用途,但必须署名。非商业用途的免费额度是每日 1 万次调用(具体以官方最新公布为准),商用或更高量级有订阅计划。服务端代码在 GitHub 上以 AGPLv3 开源,量大的话可以自建实例。
本文只给出方法与示例,不做数据二次分发。
具体做法:一条 GET 请求拿到四城数据
请求长这样,四个城市一次拿全:
https://air-quality-api.open-meteo.com/v1/air-quality
?latitude=39.9042,31.2304,23.1291,30.5728
&longitude=116.4074,121.4737,113.2644,104.0668
¤t=pm2_5,pm10,ozone,nitrogen_dioxide,european_aqi,us_aqi
&timezone=Asia/Shanghai 几个参数的含义值得记一下:timezone=Asia/Shanghai 不加的话时间戳是 GMT+0,中国城市会整体错 8 小时;timeformat=unixtime 可以把 ISO 时间换成 Unix 秒;domains=cams_europe 或 cams_global 可以手动指定域,默认 auto 是自动组合。
返回的字段和单位如下(实测返回的 current_units 里写得很清楚):
| 字段 | 含义 | 单位 |
|---|---|---|
| pm2_5 / pm10 | 细颗粒物 / 可吸入颗粒物 | μg/m³ |
| carbon_monoxide | 一氧化碳 CO | μg/m³ |
| nitrogen_dioxide | 二氧化氮 NO₂ | μg/m³ |
| sulphur_dioxide | 二氧化硫 SO₂ | μg/m³ |
| ozone | 臭氧 O₃ | μg/m³ |
| european_aqi / us_aqi | 欧洲 / 美国空气质量指数 | EAQI / USAQI |
Python 侧用标准库就能跑,零第三方依赖:
import json, urllib.request
BASE = "https://air-quality-api.open-meteo.com/v1/air-quality"
cities = {
"北京": (39.9042, 116.4074),
"上海": (31.2304, 121.4737),
"广州": (23.1291, 113.2644),
"成都": (30.5728, 104.0668),
}
lat = ",".join(str(v[0]) for v in cities.values())
lon = ",".join(str(v[1]) for v in cities.values())
url = (f"{BASE}?latitude={lat}&longitude={lon}"
"¤t=pm2_5,pm10,ozone,nitrogen_dioxide,european_aqi,us_aqi"
"&timezone=Asia/Shanghai")
with urllib.request.urlopen(url, timeout=30) as r:
data = json.load(r)
for name, item in zip(cities, data):
c = item["current"]
print(name, c["time"], "PM2.5=", c["pm2_5"], "USAQI=", c["us_aqi"]) 要历史序列就把 current 换成 hourly,再加 past_days / forecast_days。我实测 past_days=1&forecast_days=1 返回 48 行逐小时数据,PM2.5 无缺失值。
批量这块有个通用经验:坐标要分批切,别一口气塞几百个点,失败了要跳过而不是静默错位。这一点和我们做"地域—关键词"二维切片是一个道理,细节写在批量采集全流程解析里。接口对接的一般做法——按分页循环拉全、开启 gzip 压缩——我们在指数采集接口的产品说明里也提过,换数据源照样适用。要是你的环境不允许数据外传,那就只能本地化部署,发票识别那篇记过一次"客户是金融机构、数据不得离境"的场景,处理方式可以参照。
效果对比:实测值与两个必须知道的反常识
下面这组是我今晚(2026-09-29T00:00,GMT+8)实采的真实返回值,不是编的:
| 城市 | PM2.5 | PM10 | O₃ | NO₂ | EAQI | USAQI |
|---|---|---|---|---|---|---|
| 北京 | 73.6 | 75.9 | 0.0 | 91.2 | 76 | 123 |
| 上海 | 34.3 | 44.7 | 89.0 | 28.2 | 51 | 76 |
| 广州 | 32.7 | 34.5 | 9.0 | 66.3 | 63 | 82 |
| 成都 | 46.4 | 48.4 | 29.0 | 58.5 | 59 | 105 |
反常识一:同一时刻,两套 AQI 差得离谱。北京 EAQI 是 76,USAQI 是 123。不是算错了,是欧标和美标的分段断点与取值方法不同,欧标对 PM2.5 的容忍区间更宽。写进报告的时候必须标明用的哪一套,混着用会闹笑话。
反常识二:臭氧夜间为零不是缺测。我第一次看到北京 00:00 的 O₃ 是 0.0 时以为接口挂了,把当天 48 小时拉出来才发现规律:00:00 到 06:00 全是 0.0,09:00 升到 20.0,12:00 是 59.0,15:00 到峰值 70.0,18:00 回落到 41.0,21:00 又归零。夜间没有光照、臭氧被一氧化氮滴定消耗掉,数值贴近零是物理上合理的。
所以处理臭氧只有一条正路:按日最大 8 小时滑动平均或者只取白天时段,绝不能直接对 24 小时求均值,那样算出来的"日均臭氧"会被夜里的零拉到毫无意义。同理,别把 0 当缺失值去插值。
回头看,这一趟最大的收获不是拿到了数据,而是确认了一件事:口径确认应该吃掉八成的准备时间,写请求最多占两成。这句话我们在关键词工程那份复盘里说过一次,换到数据领域照样成立。真要长周期、站点级的中国城市数据,还是得回到前面说的那两家平台去申请,用观测值给模式结果做校正,两边各取所长。类似的完整数据集我们站上也整理过,比如那份 15 年城市级指数日均数据,思路可以互相对照。
常见问题
这个接口要钱吗?
非商业用途免费,官方给出的额度是每日 1 万次调用,不需要 API key。商业用途或更高量级要订阅。具体额度以官方最新公布为准,别照抄我这篇文章里的数字。
可以拿去发论文或者对外发布吗?
数据是 CC BY 4.0 授权,允许商业使用与再分发,但必须署名。做研究建议标注"数据来源:Copernicus CAMS via Open-Meteo,CC BY 4.0"。另外它属于模式再分析产品,严谨的学术研究需要配合站点观测数据做验证,不宜单独作为结论依据。
为什么和我手机上看到的空气质量不一样?
手机上通常是本地国控监测站点的实测值,这个接口是 45 km 网格(全球域)的模式输出,两者口径不同,本来就不该完全相等。拿它看跨城市、跨月份的相对趋势没问题,拿它做单点的达标判定不行。
历史数据能回溯多久?
通过 past_days 可以回拉过去若干天的逐小时数据,具体上限以官方接口文档为准。再往前、站点级的中国城市长序列,要走国家地球系统科学数据中心或 noda.ac.cn 的申请流程。
采集这类数据需要注意什么合规问题?
本例走的是官方公开 API,不涉及网页抓取,是最省心的路径。如果你是自己抓页面,务必遵守目标平台的 robots.txt 与服务条款,控制请求频率,不碰登录态内容,不收集个人信息,数据只用于内部分析。

