跳到主内容

空气质量公开数据采集踩坑实录:我换了个免密钥的公开API才把问题解决

大家通常怎么做:直奔监测总站那张实时发布页

空气质量公开数据怎么采?最省事的一条路是 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
  &current=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}"
       "&current=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.5PM10O₃NO₂EAQIUSAQI
北京73.675.90.091.276123
上海34.344.789.028.25176
广州32.734.59.066.36382
成都46.448.429.058.559105

反常识一:同一时刻,两套 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 与服务条款,控制请求频率,不碰登录态内容,不收集个人信息,数据只用于内部分析。

相关阅读

分享到:

本文链接:https://www.biyeyuanma.cn/post/249.html

服务热线

加我微信

加我微信