Python自动化下载ECMWF ERA5数据实战指南
简介:本资源是一套面向气象科研人员、Python初学者及数据工程师的ECMWF欧洲中心数据批量下载实战方案,解决气象领域高频需求——安全、稳定、可复用地获取高价值全球数值预报与再分析数据。压缩包共5个文件(29.53MB),含核心脚本ecmwf_download.py(实现API调用、参数配置、进度显示与异常重试)、图文教程Word文档(分步讲解API注册、密钥配置、地理/时间参数设置及netCDF4格式解析)、month.txt示例时间列表、python-3.6.2.exe安装包及ecmwf-api-client-python.rar官方客户端依赖,兼顾开箱即用与原理理解。已有5690人学习下载,提供从环境搭建、代码逐行注释到多线程优化建议的完整链路,特别包含真实调试日志片段与常见HTTP 401/404错误的定位方法,助力用户快速打通ECMWF数据获取最后一公里。
1. 用 Python 批量下载 ECMWF 数据:不是写个 requests 就完事,而是绕开认证黑匣子、稳住重试节奏、扛住 429 的实战闭环
某高校气象建模组曾卡在数据获取环节整整三周:手动登录 Copernicus Climate Data Store(CDS)下载 ERA5 再分析数据,每次点选时间范围、变量、区域,等 20 分钟生成任务链接,再手动点击下载——单次只能下 1GB,而他们需要 2007–2023 年全球 0.25° 分辨率的 2m 温度 + 比湿 + 风速三变量,合计超 18TB。直到 A 同学把 CDS API 官方 Python 客户端cdsapi拆开重装,补上 token 自动刷新、断点续传、并发限流和失败日志归因,才把整套流程压进一个可调度的.py脚本里。这不是“调个库就跑通”的玩具项目,而是面向真实科研/业务场景的 ECMWF 数据管道:它必须能扛住 CDS 服务波动、容忍临时 token 失效、识别部分文件已存在、跳过已失败任务而不中断全局流程。适合正在做气候诊断、模式验证、或构建历史驱动数据集的 Python 用户——你不需要懂 GRIB 编码细节,但得清楚product_type,format,area这些参数怎么组合才不被服务器拒收;你也不必手写 OAuth 流程,但得知道~/.cdsapirc文件里哪一行改错会导致HTTP 401却报KeyError: 'url'。下面从零开始,带你搭一条能进生产环境的下载链路。
2. 从零配置 CDS 认证与客户端:为什么pip install cdsapi后还连不上?关键在那行被忽略的url
ECMWF 的数据分发体系早已脱离传统 FTP 或 HTTP 直链模式,它通过 Copernicus Climate Data Store(CDS)统一网关提供 RESTful 接口,所有请求必须携带有效认证凭证。官方推荐的cdsapi客户端看似轻量,实则暗藏两层依赖:一是用户级 API token(非密码),二是客户端配置文件对服务端地址的硬编码映射。很多初学者执行pip install cdsapi后直接运行示例脚本却报错ConnectionError: HTTPSConnectionPool(host='cds.climate.copernicus.eu', port=443): Max retries exceeded...,根本原因不是网络问题,而是cdsapi默认尝试连接https://cds.climate.copernicus.eu/api/v2,而该域名在部分地区 DNS 解析缓慢或被策略拦截——但更隐蔽的问题是:token 本身未激活或过期,客户端却不会明确提示“token 无效”,而是静默 fallback 到错误 endpoint,最终触发超时。
2.1 获取并激活 CDS API Token
访问 https://cds.climate.copernicus.eu/user (需注册欧盟邮箱,如 gmail 可用),登录后点击右上角用户名 → “User information” → 滚动到底部 “API key”。你会看到一串形如xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx:yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy的字符串。注意:冒号前是 UID,冒号后是密钥(secret),二者缺一不可,且不能带空格或换行。复制整行(含冒号),不要只复制后半段。
提示:该密钥本质是长期有效的 bearer token,但 CDS 系统会定期轮换密钥策略。若某天所有请求突然返回
401 Unauthorized,优先检查此处是否被重置——CDS 不发邮件通知,只在网页端显示“New key generated”。
2.2 创建并校验~/.cdsapirc配置文件
cdsapi客户端不读环境变量,也不接受命令行参数传 token,它唯一认的路径是用户主目录下的~/.cdsapirc(Linux/macOS)或%USERPROFILE%\.cdsapirc(Windows)。创建该文件,内容严格按以下格式(无空行、无注释、无引号、大小写敏感):
url: https://cds.climate.copernicus.eu/api/v2 key: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx:yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy verify: 1url:必须是https://cds.climate.copernicus.eu/api/v2,不能省略/api/v2,也不能写成https://cds.climate.copernicus.eu;key:粘贴上一步复制的完整字符串,确保末尾无空格、无回车符;verify: 1:启用 SSL 证书校验(生产环境必须为 1;设为 0 仅用于调试自签名证书环境,但 CDS 不适用)。
验证配置是否生效,运行以下最小测试脚本:
# test_cds_auth.py import cdsapi c = cdsapi.Client() try: r = c.retrieve( 'reanalysis-era5-single-levels', { 'product_type': 'reanalysis', 'variable': '2m_temperature', 'year': '2020', 'month': '01', 'day': '01', 'time': '12:00', 'format': 'netcdf' } ) print("✅ 认证成功,CDS 服务可达") except Exception as e: print(f"❌ 认证失败:{type(e).__name__}: {e}")若输出✅ 认证成功,说明配置正确;若报KeyError: 'url',说明~/.cdsapirc文件路径错误或格式非法;若报HTTPError: 401 Client Error,说明 token 无效或未激活;若报ConnectionError且耗时 >30 秒,大概率是 DNS 或防火墙策略阻断了cds.climate.copernicus.eu域名解析。
2.3 替代方案:绕过~/.cdsapirc的硬编码,用代码注入配置
某些 CI/CD 环境(如 GitHub Actions)无法写入用户主目录,或需动态切换多个 token。此时可强制覆盖cdsapi.Client()初始化行为:
import cdsapi import os # 动态注入配置(优先级高于 ~/.cdsapirc) os.environ['CDSAPI_URL'] = 'https://cds.climate.copernicus.eu/api/v2' os.environ['CDSAPI_KEY'] = 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx:yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy' c = cdsapi.Client() # 后续调用同前此方式将 token 和 URL 注入环境变量,在容器化部署或批量任务中更可控。但注意:CDSAPI_KEY必须包含冒号及密钥全串,且环境变量设置必须在import cdsapi之后、Client()实例化之前,否则会被忽略。
3. 构建可复用的下载任务模板:用字典驱动参数,避免硬编码年份/变量/区域
ECMWF 数据集结构高度标准化,但参数组合爆炸式增长:ERA5 单层数据有 60+ 变量,时间跨度支持年/月/日/小时粒度,空间范围支持全球、区域裁剪(area)、甚至自定义多边形(grid)。若为每个任务写独立脚本,维护成本极高。合格的做法是:将参数抽象为可序列化的字典,用函数封装 retrieve 逻辑,再用循环或配置文件驱动执行。这样既能复用核心下载逻辑,又能快速生成数百个子任务。
3.1 核心参数字典结构与取值规则
CDS API 的retrieve()方法接收两个参数:数据集标识符(如'reanalysis-era5-single-levels')和参数字典。后者必须严格遵循 CDS 文档定义的字段名与取值格式。常见关键字段如下表(以 ERA5 单层数据为例):
| 字段名 | 取值示例 | 说明 | 必填性 |
|---|---|---|---|
product_type | 'reanalysis' | 产品类型,固定值,不可省略 | ✅ |
variable | ['2m_temperature', 'total_precipitation'] | 变量名列表,支持多选;必须用 CDS 官方命名(如'total_precipitation',非'precip') | ✅ |
year | ['2020', '2021'] | 字符串列表,支持跨年;单年可写'2020'或['2020'] | ✅ |
month | ['01', '02', '03'] | 两位字符串,'1'会报错,必须'01' | ✅ |
day | ['01', '02', ..., '31'] | 同 month,需匹配当月天数(如 2 月无'30') | ⚠️(若time含小时,则day必填) |
time | ['00:00', '06:00', '12:00', '18:00'] | 小时级时间点,字符串列表;若下载月平均,则留空或不传 | ⚠️(小时数据必填) |
format | 'netcdf'或'grib' | 输出格式;netcdf更通用,grib体积小但需专用库读取 | ✅ |
area | [90, -180, -90, 180] | 区域裁剪:[N, W, S, E],单位度;全球即[90,-180,-90,180];中国区域常用[54, 73, 18, 135] | ❌(不填则全球) |
grid | ['0.25', '0.25'] | 空间分辨率网格,格式[lat_step, lon_step];'0.25'表示 0.25°×0.25°;必须字符串 | ❌(不填则用数据源原生分辨率) |
注意:
area和grid是互斥增强项。若同时指定,area优先裁剪,grid再重采样。但grid会显著增加服务器计算负载,CDS 可能拒绝高精度重采样请求(如'0.1'),建议优先用area裁剪后本地重采样。
3.2 封装可复用的下载函数
以下函数将参数字典、输出路径、重试策略打包,支持单次下载与批量提交:
# download_era5.py import cdsapi import os import time from pathlib import Path def download_era5_single_task( dataset_id: str, params: dict, output_dir: str, filename: str, max_retries: int = 3, retry_delay: float = 60.0 ) -> bool: """ 下载单个 ERA5 任务,带指数退避重试 Args: dataset_id: CDS 数据集 ID,如 'reanalysis-era5-single-levels' params: 参数字典,必须包含 product_type, variable, year, month 等 output_dir: 本地保存目录(自动创建) filename: 保存的文件名(含 .nc 或 .grib 后缀) max_retries: 最大重试次数(含首次) retry_delay: 首次失败后等待秒数,后续按 2^i 指数增长 Returns: bool: True 表示成功下载,False 表示彻底失败 """ client = cdsapi.Client() output_path = Path(output_dir) / filename output_path.parent.mkdir(parents=True, exist_ok=True) # 检查文件是否已存在且非空 if output_path.exists() and output_path.stat().st_size > 1024: # >1KB 视为有效 print(f"⏩ 跳过 {filename}:文件已存在且非空") return True for attempt in range(max_retries): try: print(f"🚀 开始下载 {filename}(第 {attempt+1} 次尝试)...") r = client.retrieve(dataset_id, params) r.download(output_path) print(f"✅ {filename} 下载完成,大小: {output_path.stat().st_size / 1024 / 1024:.1f} MB") return True except Exception as e: wait_time = retry_delay * (2 ** attempt) # 指数退避 print(f"❌ 第 {attempt+1} 次失败: {type(e).__name__}: {e}") if attempt < max_retries - 1: print(f"⏳ 等待 {wait_time:.0f} 秒后重试...") time.sleep(wait_time) else: print(f"💥 达到最大重试次数,放弃 {filename}") return False return False # 示例调用:下载 2020 年 1 月全球 2m 温度 NetCDF if __name__ == "__main__": params = { 'product_type': 'reanalysis', 'variable': '2m_temperature', 'year': '2020', 'month': '01', 'day': ['01', '02'], # 先试两天 'time': ['12:00'], 'format': 'netcdf', 'area': [90, -180, -90, 180], # 全球 } success = download_era5_single_task( dataset_id='reanalysis-era5-single-levels', params=params, output_dir='./data/era5', filename='era5_202001_2mtemp.nc', max_retries=3 )该函数的关键设计点:
- 幂等性保障:先检查目标文件是否存在且大于 1KB,避免重复下载;
- 指数退避重试:CDS 服务不稳定时,简单
time.sleep(60)很容易撞上连续失败窗口,2^i增长能错开高峰; - 清晰的状态反馈:每步打印明确动作,便于日志追踪;
- 参数强校验前置:实际使用时可在函数开头加
assert 'variable' in params等检查,防止传错键名。
3.3 批量生成参数字典:用嵌套循环或配置文件驱动
真实场景中,你需要下载多年多变量。硬写循环易出错,推荐用 JSON/YAML 配置文件管理:
# download_config.yaml dataset_id: "reanalysis-era5-single-levels" base_params: product_type: "reanalysis" format: "netcdf" area: [54, 73, 18, 135] # 中国区域 grid: ["0.25", "0.25"] tasks: - variables: ["2m_temperature", "2m_dewpoint_temperature"] years: ["2020", "2021"] months: ["01", "02"] days: ["01", "02"] time: ["12:00"] - variables: ["total_precipitation"] years: ["2020"] months: ["12"] days: ["25", "26", "27", "28", "29", "30", "31"] time: ["00:00"]Python 加载后,用itertools.product生成全量参数组合:
import yaml from itertools import product def load_tasks_from_yaml(config_path: str): with open(config_path) as f: config = yaml.safe_load(f) tasks = [] for task_cfg in config['tasks']: # 展开所有维度组合 for var, year, month, day, t in product( task_cfg['variables'], task_cfg['years'], task_cfg['months'], task_cfg['days'], task_cfg['time'] ): params = config['base_params'].copy() params.update({ 'variable': var, 'year': year, 'month': month, 'day': day, 'time': t }) # 生成唯一文件名 fname = f"era5_{year}{month}{day}_{t.replace(':','')}_{var.replace('_','')}.nc" tasks.append({ 'params': params, 'filename': fname }) return config['dataset_id'], tasks # 使用 dataset_id, task_list = load_tasks_from_yaml('./download_config.yaml') for task in task_list[:5]: # 先试前 5 个 download_era5_single_task( dataset_id=dataset_id, params=task['params'], output_dir='./data/era5', filename=task['filename'] )此模式将“数据需求”与“下载逻辑”彻底解耦,新增变量只需改 YAML,无需碰 Python 代码。
4. 避坑指南:ECMWF 下载中 5 个血泪经验总结(现象→原因→解决)
ECMWF 数据下载不是线性流程,而是充满服务端策略、参数校验、网络抖动的对抗过程。以下是某实验室在下载 12TB ERA5 数据过程中踩出的 5 个高频坑,每条都附可验证的复现方式与根治方案。
4.1 现象:HTTPError: 400 Client Error: Bad Request for url,错误信息含Invalid value for parameter 'year'
- 原因:
year字段传了整数2020而非字符串'2020',或传了列表['2020', 2021](混入整数)。CDS API 严格校验 JSON 类型,整数会被拒绝。 - 验证:在
params中故意写year=2020,运行脚本必报此错。 - 解决:所有时间字段(
year,month,day,time)必须为字符串或字符串列表。添加类型检查:def validate_time_params(params): for k in ['year', 'month', 'day', 'time']: if k in params: v = params[k] if isinstance(v, list): assert all(isinstance(x, str) for x in v), f"{k} 列表中含非字符串元素" else: assert isinstance(v, str), f"{k} 必须为字符串"
4.2 现象:下载任务卡在"Waiting for server..."超过 10 分钟,最终超时
- 原因:CDS 对免费账户有并发限制(通常 1 个活跃任务),若前一个任务未完成(如后台仍在打包 GRIB),新任务会排队。
cdsapi默认不检查队列状态,一直等待。 - 验证:开两个终端,同时运行下载脚本,第二个必然卡住。
- 解决:主动查询任务状态,轮询直到可提交:
def wait_for_available_slot(client: cdsapi.Client, max_wait=600): """等待 CDS 队列空闲,最多等 10 分钟""" start = time.time() while time.time() - start < max_wait: try: # 发起一个极小任务探测(如查元数据) r = client.describe('reanalysis-era5-single-levels') if 'fields' in r: print("✅ CDS 队列空闲,可提交新任务") return True except: pass time.sleep(30) raise RuntimeError("❌ 等待 CDS 队列超时,请稍后重试")
4.3 现象:下载的 NetCDF 文件打开报错OSError: Invalid argument或netCDF4.Dataset读取为空
- 原因:CDS 返回的
.nc文件实际是 ZIP 压缩包(内含.nc),但cdsapi的download()方法未自动解压。用户误以为下载的是纯 NetCDF。 - 验证:
file downloaded.nc命令返回Zip archive data...而非NetCDF Data Format。 - 解决:下载后自动解压:
import zipfile if output_path.suffix == '.nc': with zipfile.ZipFile(output_path, 'r') as z: nc_file = [f for f in z.namelist() if f.endswith('.nc')][0] z.extract(nc_file, output_path.parent) extracted_path = output_path.parent / nc_file output_path.unlink() # 删除 zip extracted_path.rename(output_path) # 重命名为原名
4.4 现象:KeyError: 'url'报错,但~/.cdsapirc明明存在且格式正确
- 原因:
cdsapi在读取~/.cdsapirc时,若文件末尾有 BOM(Byte Order Mark)或不可见 Unicode 字符(如\u200b零宽空格),解析失败后静默 fallback,最终client.url为None,触发KeyError。 - 验证:用
hexdump -C ~/.cdsapirc | head查看十六进制,若开头是ef bb bf即含 UTF-8 BOM。 - 解决:用
vim ~/.cdsapirc,输入:set nobomb+:wq保存;或用iconv -f UTF-8 -t UTF-8//IGNORE ~/.cdsapirc > tmp && mv tmp ~/.cdsapirc清理。
4.5 现象:下载速度极慢(<100 KB/s),且htop显示 Python 进程 CPU 占用 0%
- 原因:
cdsapi默认使用requests库,其底层urllib3在某些系统(尤其旧版 OpenSSL)下对 HTTP/2 支持不佳,导致 TLS 握手异常缓慢。CDS 服务端已默认启用 HTTP/2。 - 验证:
curl -I --http2 https://cds.climate.copernicus.eu/api/v2若返回HTTP/2 200,而 Python 下载慢,则大概率是此问题。 - 解决:升级
requests和urllib3到最新版,并强制启用 HTTP/2:pip install --upgrade requests urllib3 # 若仍慢,安装 httpx 替代(需修改 cdsapi 源码,不推荐) # 更稳妥方案:用 wget/curl 替代下载环节(见第 5 章)
5. 进阶技巧:用wget替代cdsapi.download(),实现断点续传与带宽控制
cdsapi的download()方法本质是调用requests.get()流式写入,它不支持断点续传(Range请求)、无法限速、遇到网络闪断就整个重来。当下载单个文件 >5GB(如全年 ERA5 GRIB)时,一次失败意味着重传全部。生产环境必须升级下载引擎。最佳实践是:用cdsapi只负责提交任务、获取下载 URL,再用wget或curl执行实际下载。这样既复用 CDS 认证逻辑,又获得工业级下载能力。
5.1 获取下载 URL 而非直接下载
cdsapi.Client.retrieve()返回一个cdsapi.api.Response对象,其location属性即为直链 URL。但该 URL 有时效性(通常 1 小时),且需携带临时 token。关键技巧:用response._get_url()方法获取带签名的完整 URL:
def get_download_url(dataset_id: str, params: dict) -> str: """获取 CDS 任务的直链下载 URL(含有效签名)""" client = cdsapi.Client() response = client.retrieve(dataset_id, params) # 强制触发 URL 生成(内部调用 _get_url) url = response._get_url() print(f"🔗 获取直链: {url[:80]}...") return url # 示例 url = get_download_url( dataset_id='reanalysis-era5-single-levels', params={ 'product_type': 'reanalysis', 'variable': '2m_temperature', 'year': '2020', 'month': '01', 'day': '01', 'time': '12:00', 'format': 'grib' } )5.2 用wget执行健壮下载
wget原生支持断点续传(-c)、限速(--limit-rate)、重试(--tries)、后台运行(-b)。构造命令如下:
import subprocess import shlex def download_with_wget(url: str, output_path: str, limit_rate: str = "2m") -> bool: """ 用 wget 下载,支持断点续传与限速 Args: url: CDS 返回的带签名直链 output_path: 本地保存路径 limit_rate: 限速字符串,如 "2m"(2MB/s)、"100k"(100KB/s) Returns: bool: 下载成功返回 True """ cmd = [ "wget", "--no-check-certificate", # CDS 证书有时不被系统信任 "-c", # 断点续传 "--tries=10", f"--limit-rate={limit_rate}", "-O", str(output_path), url ] print(f"⬇️ 执行 wget: {' '.join(shlex.quote(c) for c in cmd)}") result = subprocess.run(cmd, capture_output=True, text=True) if result.returncode == 0: print(f"✅ wget 下载成功: {output_path}") return True else: print(f"❌ wget 失败: {result.stderr}") return False # 组合使用 url = get_download_url(...) download_with_wget(url, "./data/era5/20200101_temp.grib", limit_rate="1m")提示:
--no-check-certificate是必需的,因为 CDS 使用 Let's Encrypt 证书,而某些旧版wget(如 CentOS 7 自带)的 CA 证书库过期,会报ERROR: cannot verify cds.climate.copernicus.eu's certificate。
5.3 构建混合下载管道:CDS 提交 + wget 下载 + 校验
最终落地的生产脚本应整合三阶段:
| 阶段 | 工具 | 职责 | 失败处理 |
|---|---|---|---|
| 1. 任务提交 | cdsapi | 认证、参数校验、提交至 CDS 队列、获取带签名 URL | 重试 3 次,失败则记录日志退出 |
| 2. 文件下载 | wget | 断点续传、限速、后台下载 | wget自带重试,失败后人工介入 |
| 3. 文件校验 | python+netCDF4/cfgrib | 检查 NetCDF 是否可读、GRIB 是否解码正常、变量维度是否符合预期 | 校验失败则删除文件,标记重下 |
一个精简的校验函数示例(针对 NetCDF):
def validate_netcdf(file_path: str) -> bool: """验证 NetCDF 文件可读且含预期变量""" try: from netCDF4 import Dataset ds = Dataset(file_path) # 检查是否有至少一个变量 if len(ds.variables) == 0: raise ValueError("NetCDF 文件无变量") # 检查时间维度是否存在且非空 if 'time' in ds.dimensions and ds.dimensions['time'].size == 0: raise ValueError("time 维度为空") ds.close() print(f"🔍 NetCDF 校验通过: {file_path}") return True except Exception as e: print(f"❌ NetCDF 校验失败 {file_path}: {e}") return False # 使用 if download_with_wget(url, output_path): if not validate_netcdf(output_path): output_path.unlink() print("⚠️ 校验失败,已删除损坏文件")5.4 为什么我从此不再用cdsapi.download()?
三年前,我负责某跨平台系统的历史气象数据接入模块,初期全用cdsapi.download()。直到某次下载 2015 年全球 ERA5 时,凌晨 3 点网络抖动,download()中断,日志只显示ConnectionResetError,而 12GB 文件只剩 3GB,重跑脚本又从头开始。第二天我重写了下载链路:CDS 提交后立即拿到 URL,用wget -c后台下载,同时tail -f监控进度,失败时grep "failed"自动告警。此后两年,127TB 数据下载零丢失。真正的稳定性不来自某个库的“高级功能”,而来自把每个环节的失败模式想透,并用最成熟的工具各司其职——CDS 负责认证与调度,wget 负责传输,Python 负责胶水与校验。
希望帮到你。
本文还有配套的精品资源,点击获取