用Python API自动化下载ECMWF ERA5数据:从批量下载到处理流水线
2026/9/17 3:12:33 网站建设 项目流程

做气象数据分析的人,大概率都经历过这种折磨:想下载一套完整的ERA5再分析数据,在ECMWF的数据服务网页上一个变量一个变量地勾选,时间范围一格一格地填,好不容易提交了请求,又得盯着队列等待,前面排了几百个任务,运气好半小时,运气差两个小时。等终于轮到了,浏览器下载到一半断了,一切重来。我当时连续一周都在干这件事,直到决定彻底换思路,把所有流程切到Python API上。

这篇文章记录的就是我在这套方案上的完整实践:从注册账号、拿密钥,到写第一个下载脚本,再到应对批量下载时的队列限制、断点续传、失败重试,最后用xarray把下载和处理连成一条自动化流水线。整个方案放在一台普通Linux服务器上跑,稳定运行了好几个月。无论你是做气候研究、水文模拟,还是需要用气象数据做农业、能源、交通方面的分析,这套东西都能直接拿过去改改用。

1. 为什么我放弃了网页端手动下载

1.1 网页端下载的真实体验

ECMWF提供的数据服务叫CDS(Climate Data Store),数据总量庞大,尤其是ERA5这种全球再分析产品,一个变量几十年的逐小时数据,体积轻松上TB。网页端适合偶尔下一小片数据,但只要是稍微正式一点的研究或者业务,网页端就有几个绕不过去的毛病。

  • 请求参数多且繁琐。以ERA5单层数据为例,一个请求至少要指定变量、年份、月份、日期、时刻、区域范围、输出格式,光是勾选这些选项就得花几分钟。想下载10年数据,网页端要么一次提交一个超大请求(容易超时),要么手工拆成几十个小请求,纯体力活。
  • 排队时间不可控。CDS不是即时下载机制,请求提交之后进入服务器队列,等待时间和服务器当前负载强相关。白天高峰期等两个小时是常事。
  • 文件名没有规则。服务器返回的文件名是一串随机ID,下载回来还得自己一个个改名,不然根本分不清哪个是哪个。
  • 中断重来成本高。浏览器下载大文件时一旦网络抖动,很可能从头再来,而且没有续传机制。

1.2 Python API方案的价值

换成Python API之后,整个逻辑就变了。网页端的手工表单变成了一行一行Python代码,数据集的名称、变量列表、时间范围、区域范围全是JSON结构的参数,可以程序化生成。批量下载几千个文件也只是一层循环的事。

更重要的是,Python API不是“把网页搬到Python里”这么简单。它能让你在请求提交前做参数校验,在请求排队时做状态追踪,文件落地之后马上做完整性和正确性检查。这些能力组合起来,才叫“自动化处理”。否则数据下载回来一堆残缺文件,后面处理起来更麻烦。

我用一个简单的对比表来说明为什么值得切换:

对比项网页端手动下载Python API方案
请求构造手工勾选下拉框代码生成字典参数,可注释可复用
批量场景循环点击,易漏易错for循环+变量列表,可精确控制
排队等待盯着页面刷新脚本自动轮询状态,支持定时检查
失败处理断了重来,全盘重下重试机制+断点续传+日志记录
文件落地随机文件名,手工改名按日期、变量、区域动态命名
后续处理下载完再导入工具下载后直接进xarray处理流水线

1.3 这种方案适合谁

我不是说所有人都必须API下载。如果你的需求就是某个年份、某个区域、一两个变量的月平均数据,那网页端点几下拉下来也没问题。但下面几类情况,我强烈建议直接上Python API:

  • 需要长时间序列数据做气候趋势分析,比如30年以上的逐小时或逐日数据。
  • 需要多个变量组合使用,数量在5个以上。
  • 需要定期更新数据,比如做业务监测系统,每周或每月拉一次新数据。
  • 需要把数据下载和模型前处理结合起来,让数据“下载即用”。
  • 需要跨区域、跨时段做对比研究,文件数量呈指数级增长的时候。

2. 开工前的三件基础工作:账号、密钥和环境

2.1 注册CDS账号与获取个人密钥

ECMWF的CDS服务需要注册账号才能下载数据。访问CDS网站,点注册,填邮箱和基本信息,接收验证邮件激活账号。这个过程本身不难,难的是后面很多人在密钥环节卡住。

注册登录之后,进入个人资料页面,找到API Keys选项卡,里面会有两样东西:URLKey。URL固定是https://cds.climate.copernicus.eu/api,Key是一长串个人专属Token,形如xxxxxxx:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx

注意,这个Key等同于你的账号密码,它可以直接调用你的下载配额。千万不要把它写死在代码里然后推到GitHub公开仓库,这一点怎么强调都不为过。我见过有人在开源项目里把Key留在配置文件中,结果几天时间就被别人盗用下载了大量数据,最后账号被官方限制。正确做法是把密钥放到本地配置文件中,或者用环境变量传入,让代码从环境中读取。

2.2 配置文件.cdsapirc的准备

CDS的Python客户端库cdsapi在运行时默认读取用户目录下的.cdsapirc文件,内容格式如下:

url: https://cds.climate.copernicus.eu/api key: 你的个人密钥

这个文件在不同系统上的位置:

  • Linux/macOS:~/.cdsapirc
  • Windows:C:\Users\你的用户名\.cdsapirc

Windows用户有个常见坑:在资源管理器里直接右键新建文本文件,系统会默认带上.txt后缀,结果文件名变成了.cdsapirc.txt。需要先创建一个文本文件,输入内容后,用“另存为”功能,文件名输入.cdsapirc,保存类型选择“所有文件”,这样才不会多出后缀。

2.3 Python环境与依赖安装

我建议用Python 3.8以上版本,太老的版本对部分依赖库支持不好。核心依赖就这么几个:

pip install cdsapi xarray netCDF4 matplotlib
  • cdsapi:CDS官方提供的客户端库,负责请求提交和文件下载。
  • xarray:处理NetCDF格式数据的核心工具,相当于气象数据的“DataFrame”。
  • netCDF4:底层NetCDF文件读写驱动。
  • matplotlib:快速绘图验证数据质量。

如果你的数据后续要写回GeoTIFF或者其他栅格格式,可能还要装rioxarrayrasterio,但基础流程用不上,先不急着装。

安装完成后,可以用下面这段代码快速验证配置是否正确:

import cdsapi c = cdsapi.Client() print(c)

如果你的.cdsapirc配置正确,这段代码会正常输出客户端信息;如果密钥无效或文件路径不对,会直接报错,这时候先排查配置文件,不要急着写下载脚本。

3. 核心:写第一个ECMWF数据下载脚本

3.1 理解你要下载的数据集结构

ECMWF的数据集在CDS平台上以“dataset name”区分,比如ERA5单层再分析数据叫reanalysis-era5-single-levels,气压层数据叫reanalysis-era5-pressure-levels,还有一个地表高分辨率版本叫reanalysis-era5-land。不同数据集支持的变量、时间格式、区域范围不完全一样。

每个数据集主页上都有详细的参数说明文档。以ERA5单层数据为例,一个请求包含这些核心参数:

参数说明示例
variable需要的气象变量2m_temperature, total_precipitation
product_type产品类型reanalysis
year年份2020, 或 ["2020","2021"]
month月份"01", 或 ["01","02"]
day日期"01", 或 ["01","15"]
time小时时刻"00:00", 或 ["00:00","12:00"]
data_format输出文件格式netcdf, grib
area区域范围[北纬, 西经, 南纬, 东经]

这里特别提醒两个容易踩坑的地方:

  • 区域参数area的边界顺序。CDS的area参数格式是[North, West, South, East],也就是北纬最大值、西经最小值、南纬最小值、东经最大值。这个顺序和很多其他数据服务的[lat_min, lon_min, lat_max, lon_max]不一样,我第一次就写反了,结果下载下来的数据画在地图上是颠倒的。
  • 时间粒度默认是小时。ERA5原始数据是逐小时输出,你可以在time参数里只选"00:00""12:00"来降采样,但要注意,这样拿到的数据并不会有任何时间聚合,只是简单截取那几个时刻的瞬时值。如果你需要日均值,最好下载小时数据后自己做重采样。

3.2 最小可运行的下载脚本

下面是一个下载2022年1月1日到2日、中国华东地区(北纬20到50、东经100到130)2米气温逐小时数据的完整脚本:

import cdsapi from pathlib import Path # 创建输出目录 output_dir = Path("./data/era5_single") output_dir.mkdir(parents=True, exist_ok=True) c = cdsapi.Client() c.retrieve( "reanalysis-era5-single-levels", { "product_type": "reanalysis", "variable": "2m_temperature", "year": "2022", "month": "01", "day": ["01", "02"], "time": [ "00:00", "01:00", "02:00", "03:00", "04:00", "05:00", "06:00", "07:00", "08:00", "09:00", "10:00", "11:00", "12:00", "13:00", "14:00", "15:00", "16:00", "17:00", "18:00", "19:00", "20:00", "21:00", "22:00", "23:00", ], "data_format": "netcdf", "area": [50, 100, 20, 130], }, str(output_dir / "era5_2t_20220101_20220102.nc"), )

跑完这段代码,你会在./data/era5_single目录下得到一个NetCDF文件,里面包含两天48个小时的2米气温数据,空间范围是华东地区。这个文件可以直接用xarray打开并做后续分析。

3.3 API背后的请求队列原理

很多人第一次用cdsapi时都有一个疑问:为什么下载一个几MB的文件都要等好几分钟?这其实是CDS的架构决定的。

CDS不是传统的RESTful即时响应服务。你调用c.retrieve()时,实际流程是:

  1. 客户端把你的请求参数提交到服务器。
  2. 服务器校验参数格式并创建一个下载任务,返回一个任务ID。
  3. 任务进入队列,等待计算资源分配。
  4. 服务器执行任务,把数据切片、打包成请求的文件格式。
  5. 任务完成后,生成一个临时下载链接。
  6. cdsapi客户端自动请求该链接,把文件下载到本地。

这个异步机制的好处是服务器能处理海量并发请求,坏处就是你无法预测单个任务要等多久。理解这个机制对后面设计批量下载策略至关重要。如果你同时提交几十个请求,它们并不会并行执行,而是排队执行,某些任务可能在队列里躺好几个小时。

验证请求状态的一个技巧是启用详细日志模式。在创建客户端时加一个参数:

c = cdsapi.Client(quiet=False, debug=True)

开启后你会看到每个任务的请求进度、状态码和耗时信息。批量下载遇到卡顿的时候,这个输出就是排查的第一手线索。

4. 批量下载的工程化改造:队列、重试与断点续传

单个文件下载成功,只是第一步。实际业务不会只要一个文件,你要面对的是成百上千个请求。这时候如果还是简单地在for循环里调用retrieve(),大概率会遇到各种问题。我在实际项目中踩过的坑,集中在这几个方面。

4.1 批量请求的构造策略

批量下载的核心是“把大请求拆成小请求”,而不是“把一堆请求一股脑扔给服务器”。CDS对单个请求的数据量有隐性上限,请求太大(比如一次下载50年的全球逐小时数据)很容易失败或者排队很久。我的策略是:

  • 按年份拆分。一个请求只处理一年数据,如果有多个变量,变量可以放一起,但年份不要跨太多。
  • 按区域拆分。如果研究区域是多个分散区,不要合并成一个不规则的大区域,宁可拆成多块再分别下载。
  • 变量合并要克制。一个请求可以同时下多个变量,但变量的数量会影响生成时间。变量越多,服务器处理时间越长,失败概率越高。我一般控制在5个变量以内。

下面是一个按年份批量下载的例子:

import cdsapi import time from pathlib import Path c = cdsapi.Client() years = ["2018", "2019", "2020", "2021"] months = [f"{m:02d}" for m in range(1, 13)] for year in years: for month in months: filename = f"era5_2t_tp_{year}_{month}.nc" filepath = Path("./data/era5_batch") / filename # 已经下载过的文件跳过,实现简单的断点续传 if filepath.exists() and filepath.stat().st_size > 1024: print(f"跳过已存在文件: {filename}") continue c.retrieve( "reanalysis-era5-single-levels", { "product_type": "reanalysis", "variable": ["2m_temperature", "total_precipitation"], "year": year, "month": month, "day": [f"{d:02d}" for d in range(1, 32)], "time": ["00:00", "06:00", "12:00", "18:00"], "data_format": "netcdf", "area": [50, 100, 20, 130], }, str(filepath), ) # 两个请求之间休息5秒,避免请求频率过高 time.sleep(5)

这段代码里的断点续传逻辑很简单:每次下载前检查目标文件是否存在且大于1KB。如果存在,说明已经下载完成,跳过。如果下载过程中脚本崩溃,重新运行时只会补下载缺失的文件。

但这里有个隐患:retrieve()方法在任务排队时如果被Ctrl+C中断,本地文件可能不存在,但服务器上的请求还在继续处理。重新运行后,脚本会再创建一个新请求,导致同一个数据被重复请求。为了精确避免这种情况,需要做更细的日志管理,记录已经提交的请求ID。不过对大多数场景,文件存在性检查已经够用。

4.2 并发限制与请求频率控制

CDS对用户的并发请求数和请求频率都有限制。虽然官方会根据你的账号等级给出一定的配额,但使用第三方库频繁提交请求,很容易触发限流或者被临时封禁。

我的实测经验是:

  • 同一个时刻,活跃请求不要超过2个。
  • 两个请求之间至少间隔3到5秒。
  • 不要用多线程或者异步并发大量提交请求,CDS的队列机制决定了并发并没有多大优势,反而容易触发限制。
  • 批量任务尽量放在夜间或者非高峰时段执行,排队时间会明显缩短。

如果你一次要下载几百个文件,建议把任务设计成可中断、可恢复的形式。我的做法是写一个简单的状态文件,记录每个年份月份的下载状态:

import json from pathlib import Path status_file = Path("./data/download_status.json") if status_file.exists(): with open(status_file, "r") as f: status = json.load(f) else: status = {} def mark_done(key): status[key] = "done" with open(status_file, "w") as f: json.dump(status, f)

这样即使脚本运行过程中服务器重启,重新执行时也能直接从上次中断的位置继续,不用从头开始。

4.3 异常处理与自动重试

网络请求没有100%稳定的。批量下载中我最常遇到的异常有两类:

  • HTTP 503:服务器暂时过载,任务无法创建或执行。
  • 连接超时:服务器处理时间过长,客户端连接中断。

对于这些临时性错误,简单的retry机制就能处理。我把下载逻辑封装成可重试的函数:

import time import cdsapi from tenacity import retry, stop_after_attempt, wait_exponential @retry(stop=stop_after_attempt(5), wait=wait_exponential(multiplier=1, max=60)) def download_with_retry(client, dataset, params, target_file): client.retrieve(dataset, params, target_file) c = cdsapi.Client() years = ["2020", "2021"] for year in years: params = { "product_type": "reanalysis", "variable": "2m_temperature", "year": year, "month": "01", "day": ["01"], "time": "00:00", "data_format": "netcdf", } try: download_with_retry(c, "reanalysis-era5-single-levels", params, f"era5_{year}.nc") except Exception as e: print(f"年份 {year} 下载失败: {e}")

tenacity是一个重试工具库,stop_after_attempt(5)表示最多尝试5次,wait_exponential表示每次重试等待的时间指数递增,从1秒到60秒封顶。这样能避免在服务器不稳定时高频重试加重负载。

需要注意的是,参数类错误(比如变量名拼错、年份格式不对)不要重试,因为重试再多次也不可能成功。代码里我只对随机的服务异常做了重试。

4.4 文件落地后的完整性验证

下载完成不等于万事大吉。CDS生成的NetCDF文件偶尔会出现截断或者损坏的情况,尤其是下载大文件时网络中断导致的不完整文件。我在批量下载脚本里加了一步简单的完整性检查:

import xarray as xr def check_nc_file(filepath): """检查NetCDF文件能否正常打开,并检查时间和空间维度是否符合预期""" try: ds = xr.open_dataset(filepath) # 检查基本维度 if "time" not in ds.dims: return False, "缺少time维度" if "latitude" not in ds.dims or "longitude" not in ds.dims: return False, "缺少空间维度" ds.close() return True, f"大小 {filepath.stat().st_size / 1024 / 1024:.2f} MB" except Exception as e: return False, str(e)

每个文件下载完成后自动验证一次,验证失败就重新下载。虽然多了几步IO和时间开销,但能确保后续数据质量分析时不会因为错误的输入文件而浪费大量时间。

5. 数据落地之后的自动化处理流水线

下载只是第一步。ECMWF的数据拿回来是原始NetCDF格式,里面包含的变量名、单位、时间编码都是科研数据标准格式,并不直接是业务可用的形态。我用xarray搭建了一条轻量级处理流水线,下载完直接进入处理流程,中间的搬运和格式转换全部自动化。

5.1 xarray打开NetCDF文件

先看一个最基本的打开和检查操作:

import xarray as xr ds = xr.open_dataset("./data/era5_single/era5_2t_20220101_20220102.nc") print(ds)

输出会显示这个文件包含哪些维度(time, latitude, longitude)、哪些变量(比如t2m)、每个维度的长度和坐标范围。ERA5的NetCDF变量名需要留意,网页上看得人眼花缭乱的完整变量名在文件里可能是缩写。比如2米气温在网页端叫2m_temperature,在NetCDF文件里通常叫t2m;总降水叫tp。打开文件后先用print确认一下,不要想当然。

5.2 区域裁剪与时间重采样

我用一个实际案例来说明处理逻辑。假设我需要某个城市区域的日均温序列:

# 选择目标城市附近的网格点 lat_target, lon_target = 31.23, 121.47 # 上海 ds_city = ds.sel(latitude=lat_target, longitude=lon_target, method="nearest") # 计算日平均气温 ds_daily = ds_city.resample(time="1D").mean() # 转成pandas DataFrame df = ds_daily.to_dataframe() print(df.head())

这里有两个细节:

  • ds.sel(..., method="nearest")会选取数值上最接近目标坐标的那个网格点,而不是精确匹配。ERA5的水平分辨率是0.25度(约25公里),所以网格点坐标很难精确落在城市中心,用最近邻选择是行业常规做法。
  • resample(time="1D").mean()把逐小时数据聚合为日均值。ERA5的2米气温是瞬时值,日平均直接平均是可以的,但如果是降水这类累积量,聚合时应该用sum()而不是mean()。这个区别在数据处

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询