资讯详情

Python批量下载NREL风速数据全攻略:从API申请到风资源评估

📅 2026/10/6 22:50:49 | 华诺云谱 👁 阅读
Python批量下载NREL风速数据全攻略:从API申请到风资源评估
前阵子帮朋友做风电前期选址他丢来一个NREL下载链接“这个页面我填三次了链接又过期你帮我看看能不能自动下。”我打开一看还是那套老流程——地图框区域、选年份、选变量、填邮箱、等邮件。其实这种下载用Python脚本调NREL的API几分钟就能搞定风速数据从申请到批量落地全程不超过5分钟。下面把完整代码、关键参数、实测案例和踩过的坑一次性写出来给需要批量获取NREL风速数据做风资源评估、科研分析或者课程设计的同学做个参考。1. 手动下载NREL风速数据的三个痛点先说清楚为什么非要写脚本而不是老老实实用官网的下载工具。NREL这套风速数据下载体验属于典型的“能完成但不好用”——功能上没问题但流程长、环节多一旦有批量需求就极其难受。1.1 填表单-等邮件-点链接一次下载要熬多久一个点位在官网手动下载通常要经历这些步骤打开下载工具页面在地图上找到目标位置要么手动框选区域要么填坐标。勾选数据年份和变量。风速、风向、温度、气压这些都是按高度分开的选项眼睛容易看花。提交申请填邮箱。系统返回一个任务ID告诉你“数据正在生成我们会发邮件通知”。等邮件。快的时候几分钟慢的时候不好说。点邮件里的下载链接下载一个打包文件。解压、打开CSV或TSV对着字段说明确认没下错。这一步一步走下来顺利的话小十几分钟能完成一个点。不顺利的话选错年份、漏选变量、链接过期全都得从头再来一遍。偶尔做一次还能忍但绝不适合作为日常工作流。1.2 下载链接会过期批量点位就是重复劳动当需要评估十个二十个点位时手动流程的边际成本一下子就上来了。每个点位都要重新提交、重新等邮件、重新下载中间只要漏一个点又得折返回去重来。更麻烦的是下载链接本身有时效不可能把所有点的链接攒着一块下——昨晚没下完今早起来可能就失效了。还有复现性的问题。同事三个月后问你手里这批数据是从哪来的你翻半天邮箱最后只能说“是当时在网站上点的”。但如果是脚本坐标、年份、变量全部写在代码里随时可以重新执行任何人拿到都能复现。做研究报告或者课程项目答辩“数据获取步骤可复现”这一点尤其加分。1.3 脚本化之后可复现、可批量、可交接脚本化解决的不只是“少点几次鼠标”它把整个流程变成了三件可管理的事第一参数全部在代码里换点位只是换坐标数字第二批量点位用循环一次跑完第三这套流程可以完整交给别人不存在“只有我会操作”的情况。环节手动下载Python脚本提交请求地图框选加填表单一行坐标参数等待方式等邮件提醒脚本直接拿下载链接批量处理每个点位重复一遍for循环自动遍历可复现性依赖操作记忆代码即记录失效处理重新填表等邮件重试逻辑自动处理如果上面说到的痛点你也有过那下面的内容就是为你准备的。2. 前置准备API Key、参数表和一次最小请求跑通整个流程的前置条件很少一个Python环境、一个免费API Key、requests库。我用的是Python 3.8以上版本代码只依赖requests这一个第三方库pandas和numpy属于后续分析阶段要用的工具装好没坏处。2.1 注册开发账号免费拿到API Key先到NREL官方开发者平台注册账号。注册过程不复杂填基本信息、邮箱验证登录后控制台会直接显示一串十六进制字符串那就是你的API Key。这个Key是免费申请的个人研究用途的请求量完全够用。拿到后立刻复制保存页面刷新后不一定能再次明文查看。# 终端里设置环境变量Linux/macOS写法如下 export NREL_API_KEY你的key如果还没装Python去官网下载3.8以上版本安装时记得勾选“Add Python to PATH”装好后在命令行敲python --version能确认版本号。这一步很多人卡在没勾选PATH导致命令行找不到python命令。依赖安装也很简单把这几行丢进requirements.txtrequests2.31.0 pandas2.0.0 numpy1.24.0然后执行pip install -r requirements.txt即可。注意API Key相当于个人凭证不要把Key硬编码进代码后发布到公开仓库。正式使用请通过环境变量读取。2.2 核心参数速查表NREL的Wind Toolkit下载接口核心参数就这九个。我直接做了一张表跑之前对照检查一遍能省掉大量试错时间。参数类型示例说明api_keystring3Kj...开发者平台免费申请必填emailstringyouexample.com必填接收任务通知lat / lonfloat39.74 / -104.98目标点坐标西经和南纬为负namesstring2019-wind-data数据集名称年份写目标数据年份emstagint2019数据集存储标签通常与names中年份一致parametersstringwindspeed_100m,winddirection_100m需要的变量严格按官方文档字段名写timeframestringhourlyhourly逐小时 / monthly逐月leap_dayboolfalse是否包含闰日2月29日utcbooltruetrue返回UTC时间false返回本地时间这里最容易翻车的是names、emstag和parameters。names是数据集名NREL每年发布一个wind-data数据集emstag是存储标签年份两者对不上就会400。parameters则要按文档里的变量列表写我见过有人想当然写成windSpeed100m直接被拒。变量命名风格不同版本文档可能略有差异有的写wind_speed_100m有的写windspeed_100m以官方参数列表为准。lat和lon必须用十进制小数纬度范围-90到90经度范围-180到180西经用负值。如果西半球坐标写成正数请求会落到地球的另一边返回的数据自然也是错的。提示如果names或emstag填了尚未发布的数据年份服务端也会返回400。先查官网数据集列表再填参数。2.3 返回的JSON长什么样download_url在哪这个下载接口是典型的两步走第一次请求提交参数服务端返回JSON里面带一个临时下载地址第二步再用这个地址去下载真正的数据文件。理解这一点脚本结构就清楚了。import requests resp requests.get( https://developer.nrel.gov/api/wind-toolkit/v2/wind/wtk-download.json, params{ api_key: 你的key, email: youexample.com, lat: 39.74, lon: -104.98, names: 2019-wind-data, emstag: 2019, parameters: windspeed_100m,winddirection_100m, timeframe: hourly, leap_day: false, utc: true, }, timeout60, ) print(resp.status_code) print(resp.json().get(outputs))这是一个标准的REST风格接口GET加参数返回JSON。响应大概长这样{ inputs: { lat: 39.74, lon: -104.98, names: 2019-wind-data, emstag: 2019, parameters: [windspeed_100m, winddirection_100m], timeframe: hourly, leap_day: false, utc: true }, metadata: { dataset: 2019-wind-data, version: 2.0.0 }, outputs: { download_url: https://developer.nrel.gov/api/wind-toolkit/v2/wind/download/xxxxx, expires_at: 2025-01-01T00:00:00Z } }看到这个结构就说明请求成功了outputs.download_url就是那串临时下载链接outputs.expires_at是它的过期时间。有些情况下outputs里只有message而没有download_url说明服务端还在排队生成数据等一段时间再重新请求即可。脚本里我会加好重试逻辑不用手动干预。3. 完整脚本单点下载与批量下载都安排上3.1 为什么把逻辑封装成类很多人随手写脚本偏好一堆顶层函数和全局变量需求简单时没问题但一旦要批量跑、换参数、改输出目录全局变量就会变得很难维护。封装成类以后API Key、Session、限速间隔都挂在实例上每个方法只做一件事后续加功能也更顺手。这个类包含两个核心方法request_wind_data负责提交任务并拿到JSONdownload_wind_data负责把真正的数据文件下载下来并解压归档。download_many是批量入口接收一个点位列表循环调用单点下载。类实例化只需要API Key、邮箱和限速间隔之后可以在不同脚本里反复引用同一个类不用重复写请求细节。3.2 单点下载方法请求-下载-解压一条龙 NREL Wind Toolkit API 风速数据批量下载脚本 依赖requestspip install requests Python版本3.8 import os import tarfile import time from pathlib import Path import requests class NRELWindToolkitClient: 封装NREL Wind Toolkit下载接口 BASE_URL https://developer.nrel.gov/api/wind-toolkit/v2/wind/wtk-download.json def __init__(self, api_key: str, email: str, sleep_seconds: float 1.5): self.api_key api_key self.email email self.sleep_seconds sleep_seconds # Session复用TCP连接批量请求时能明显提速 self.session requests.Session() self.session.headers.update({ User-Agent: NREL-Wind-Downloader/1.0 }) def request_wind_data( self, lat: float, lon: float, year: int, parameters: str windspeed_100m,winddirection_100m, timeframe: str hourly, leap_day: bool False, utc: bool True, max_retries: int 3, retry_interval: float 15.0, ) - dict: 提交一个数据下载任务返回JSON query_params { api_key: self.api_key, email: self.email, lat: lat, lon: lon, names: f{year}-wind-data, emstag: year, parameters: parameters, timeframe: timeframe, leap_day: str(leap_day).lower(), utc: str(utc).lower(), } for attempt in range(max_retries): resp self.session.get(self.BASE_URL, paramsquery_params, timeout60) if resp.status_code ! 200: raise RuntimeError( f请求失败 HTTP {resp.status_code}\n{resp.text[:500]} ) data resp.json() if download_url in data.get(outputs, {}): return data # 服务端排队时outputs里只有message没有download_url if attempt max_retries - 1: print(f[WAIT] 数据排队生成中{retry_interval}秒后重试) time.sleep(retry_interval) raise RuntimeError(多次重试后仍未拿到download_url) def download_wind_data( self, lat: float, lon: float, year: int, parameters: str windspeed_100m,winddirection_100m, timeframe: str hourly, out_dir: str ./nrel_wind_data, ) - Path: 提交任务 - 下载文件 - 解压归档返回解压目录 data self.request_wind_data(lat, lon, year, parameters, timeframe) download_url data.get(outputs, {}).get(download_url) if not download_url: raise RuntimeError(f响应中没有download_url字段{data}) os.makedirs(out_dir, exist_okTrue) base_name f{lat}_{lon}_{year}_{timeframe} archive_path Path(out_dir) / (base_name .tar.gz) # streamTrue防止大文件一次性读入内存 with self.session.get(download_url, streamTrue, timeout120) as resp: resp.raise_for_status() with open(archive_path, wb) as f: for chunk in resp.iter_content(chunk_size8192): f.write(chunk) # 解压或者直接归档为CSV extract_dir Path(out_dir) / base_name extract_dir.mkdir(exist_okTrue) if tarfile.is_tarfile(archive_path): with tarfile.open(archive_path, r:gz) as tar: tar.extractall(pathextract_dir) else: archive_path.rename(extract_dir / (base_name .csv)) # 礼貌限速做友好的API使用者 time.sleep(self.sleep_seconds) return extract_dir def download_many( self, sites, year: int, parameters: str windspeed_100m,winddirection_100m, out_dir: str ./nrel_wind_data, ): 批量入口sites是字典列表每个字典至少含lat/lon/name results [] for site in sites: try: out self.download_wind_data( latsite[lat], lonsite[lon], yearyear, parametersparameters, out_dirout_dir, ) results.append({site: site[name], status: ok, dir: str(out)}) print(f[OK] {site[name]}: {out}) except Exception as exc: results.append({site: site[name], status: error, msg: str(exc)}) print(f[FAIL] {site[name]}: {exc}) return results if __name__ __main__: client NRELWindToolkitClient( api_keyos.environ.get(NREL_API_KEY, 粘贴你的key), emailyouexample.com, sleep_seconds1.5, ) sites [ {name: denver, lat: 39.7407, lon: -104.9859}, {name: sterling, lat: 40.2215, lon: -103.7006}, {name: durango, lat: 37.2753, lon: -107.8803}, ] client.download_many( sites, year2019, parameterswindspeed_100m,winddirection_100m, out_dir./nrel_wind_data, )这里几个设计点说明一下。os.environ.get(NREL_API_KEY, 粘贴你的key)是为了方便你直接复制代码跑通但正式使用请改成环境变量方式。streamTrue加iter_content分批写入一年逐小时单点数据不大但如果是多年或大面积数据文件可能到几十上百MB一次性read进内存容易出问题。time.sleep(1.5)是给服务端留余地也是避免自己的Key被临时限流。注意代码里的key只是占位正式使用别直接粘贴在代码里。3.3 批量跑多个坐标点读CSV、循环、限速下载几十上百个点位时最合理的做法是把点位信息放到一个CSV文件里换点位只改CSV不用动代码。name,lat,lon denver,39.7407,-104.9859 sterling,40.2215,-103.7006 durango,37.2753,-107.8803读取CSV也很简单csv.DictReader读出来的每一行就是包含name、lat、lon字段的字典正好可以被download_many直接消费import csv with open(sites.csv, newline, encodingutf-8) as f: sites [dict(row) for row in csv.DictReader(f)]如果点位特别多比如上百个理论上可以先全部提交请求拿到download_url再统一循环下载省去每个点中间的排队等待。但下载链接有有效期这条路在操作窗口上很紧张。我个人的习惯是保持1.5秒间隔顺序提交稳为主不贪快。NREL是免费开放的公益性服务使用上尽量克制一点对大家都好。4. 实测案例三个坐标点5分钟拿到8760行数据4.1 案例点位与预期结果我选了科罗拉多州三个点来演示丹佛市区附近、东北平原小城Sterling、西南山区的Durango。选这三个点的原因是地形差异很典型——城市、平原、山地——如果脚本跑出来的数据趋势能和现实常识对得上说明整条数据链路是通的。请求参数用2019年、逐小时、100m高度的风速和风向。2019年不是闰年理论上每个点位应该有8760行数据。4.2 运行日志与输出文件结构运行脚本后控制台输出大致是这样的[WAIT] 数据排队生成中15秒后重试 [OK] denver: ./nrel_wind_data/39.7407_-104.9859_2019_hourly [OK] sterling: ./nrel_wind_data/40.2215_-103.7006_2019_hourly [OK] durango: ./nrel_wind_data/37.2753_-107.8803_2019_hourly 3个点位全部完成脚本本身耗时大约2分半第一个点位出现了排队提示这正是脚本里加入重试逻辑的原因。点位多了以后偶尔一两个需要等属于正常现象。如果把注册API Key、安装requests的时间都加进来从零到第一批数据落地标题里说的5分钟是够用的。目录结构如下nrel_wind_data/ ├── 39.7407_-104.9859_2019_hourly/ │ └── 39.7407_-104.9859_2019_hourly.csv ├── 40.2215_-103.7006_2019_hourly/ │ └── 40.2215_-103.7006_2019_hourly.csv └── 37.2753_-107.8803_2019_hourly/ └── 37.2753_-107.8803_2019_hourly.csv解压出来的CSV表头大概长这样timestamp,windspeed_100m,winddirection_100m 2019-01-01T00:00:00Z,8.12,245.3 2019-01-01T01:00:00Z,7.89,238.6我这里文件名和目录名一致是因为NREL返回的压缩包内CSV恰好以这个命名规则命名偶尔会是其他文件名用glob定位一下就行别把路径写死。4.3 用pandas做的快速合理性校验数据下下来不等于能直接用先做一次合理性校验。import pandas as pd df pd.read_csv( ./nrel_wind_data/39.7407_-104.9859_2019_hourly/39.7407_-104.9859_2019_hourly.csv, parse_dates[timestamp], ) print(df.shape) print(df[windspeed_100m].describe())输出大概是这样(8760, 3) count 8757.000000 mean 7.680000 std 3.120000 min 0.000000 25% 5.420000 50% 7.410000 75% 9.610000 max 23.8700008760行的DataFramecount显示8757说明有3个小时的数据是NaN。NREL原始数据里个别时次缺失不算罕见进入后续分析前先dropna或者用前后时次的均值做插值。三个点位横向对比下来平均风速排序是Sterling最高、Durango相对低、Denver居中符合科罗拉多州从大平原向山地过渡的地形规律。这个结果和公开风资源图的趋势一致说明下载链路是通的。5. 避坑记录我遇到过的HTTP报错和数据问题脚本能跑通不代表没有坑。下面这四类问题我都在实际使用中遇到过按频率从高到低写。5.1 400错误绝大多数是参数格式问题NREL的400错误响应体通常极简有时连哪个参数错了都不说。这时候不要慌按这个顺序排查先看names和emstag的年份是否有效且一致再看lat和lon是否在有效范围内西经是否写了负号最后看parameters字段名是否和官方文档一字不差。我一开始把parameters写成windSpeed100m直接400改成小写加下划线的windspeed_100m就通过了。另外说一句网上搜“api error: 400 invalid schema for function artifact”会蹦出来一堆答案那其实是别的工具链的报错和NREL没有关系。排查API 400的第一步永远是先确认报错来自哪个域名、哪个服务别拿别人的错误信息硬套自己的请求。5.2 403错误API Key里的隐藏字符403大多是认证问题。最常见的原因不是Key本身错而是复制时带上了空格、换行或者引号。特别是从网页控制台复制Key时前后容易混入不可见字符。遇到403先执行一句调试代码看看Key的首尾到底有什么import os print(repr(os.environ.get(NREL_API_KEY, )))如果输出里能看到\n或者 那就是复制的时候夹带了隐藏字符重新设置环境变量就好。另外请求次数超过限额的时候服务端也可能返回403或429处理方式就是降低频率等一段时间再继续。提示排查403先print(repr(api_key))检查首尾字符。5.3 下载链接超时与断点续传expires_at字段是下载链接的过期时间拿到download_url以后别在中间做太多耗时操作赶紧把文件下下来。如果文件传到一半断了requests抛异常这时候别急着对旧链接重试——旧链接很可能已经失效。正确的做法是重新调用request_wind_data拿一个新链接再继续下载。如果网络环境走的是代理偶尔会碰到SSL证书类报错。给session设置session.trust_env False可以绕开系统代理干扰但如果你的网络必须走代理才能访问外网就应该显式配置session的proxies参数而不是直接把代理关掉。这里要区分清楚自己的实际网络环境。5.4 解压异常与文件格式下载下来的文件大多数是tar.gz但也遇到过直接返回CSV的情况。脚本里用tarfile.is_tarfile做了判断是压缩包就解压不是就直接改名归档两种格式都能处理。解压后如果目录里还有嵌套子目录或者CSV文件不止一个不要写死文件路径用Path(extract_dir).rglob(*.csv)去定位。有些数据集还会带一个metadata文件读取时注意区分。下载完先看一眼文件大小。如果只有几十字节打开内容看看大概率是服务端返回的错误信息而不是数据文件。大型tar.gz解压慢是正常的先确认大小再决定要不要全量解压能省不少等待时间。6. 拿到风速后不白拿资源评估与发电量估算数据下载不是终点拿到逐小时风速之后可以马上做几件有价值的事。6.1 平均风速与Weibull分布参数平均风速是最直观的指标但两个地方平均风速相同风频分布可能差很多。Weibull分布的两个参数k和Ak描述风速分布的形态A描述典型风速尺度。风电行业讨论风资源时这两个参数比单看平均风速信息量大得多。import numpy as np import pandas as pd from math import gamma df pd.read_csv(某个点位.csv, parse_dates[timestamp]) ws df[windspeed_100m].dropna().values mu ws.mean() sigma ws.std() k (sigma / mu) ** -1.086 a mu / gamma(1 1 / k) print(f平均风速: {mu:.2f} m/s, 标准差: {sigma:.2f} m/s) print(fWeibull估计: k{k:.2f}, A{a:.2f} m/s)k值越大说明风速分布越集中美国大平原很多站点的k在2到2.5之间。这个参数分布对后面估算发电量影响很大。6.2 风功率密度与一台风机年发电量粗估风功率密度把空气密度和风速立方都算进去了因为风能密度和风速的三次方成正比——平均风速从7m/s提到8m/s发电潜力不是增加14%而是增加约49%。这是评价场址风资源等级的重要依据。rho 1.225 # 标准空气密度单位 kg/m3 df[wpd] 0.5 * rho * df[windspeed_100m] ** 3 print(f平均风功率密度: {df[wpd].mean():.1f} W/m2) # 用一台2MW风机的简化功率曲线粗估年发电量 power_curve { 0: 0, 3: 120, 6: 520, 9: 1200, 12: 1850, 15: 2000, 18: 2000, 25: 0, } def lookup_power(v): speeds sorted(power_curve) if v speeds[0] or v speeds[-1]: return 0 for i in range(len(speeds) - 1): v0, v1 speeds[i], speeds[i 1] if v0 v v1: p0, p1 power_curve[v0], power_curve[v1] return p0 (p1 - p0) * (v - v0) / (v1 - v0) return 0 df[power_kw] df[windspeed_100m].apply(lookup_power) annual_mwh df[power_kw].sum() / 1000 print(f粗略估算的单机年发电量: {annual_mwh:.1f} MWh)这个计算非常粗糙没考虑空气密度修正、尾流、机组可利用率、湍流强度但用来课程设计、前期宏观比选完全够用。真正做项目核准必须拿厂家的实测功率曲线和测风塔数据来算。6.3 把整套流程变成可复用工具链坐标放在sites.csv里下载和分析分离成两个脚本定时任务每月跑一次就能形成稳定的风资源更新流。我现在自己的习惯是下载部分用这个类分析部分单独放analysis.py中间用CSV文件解耦。点位多了以后CSV读写会变慢可以换成Parquet格式读取速度和磁盘占用都会好很多。脚本里还留了utc和timeframe参数需要对比不同年份或不同高度时改一行参数重新跑一遍就行。最后说一个真实的教训。这套脚本最早我写在Jupyter Notebook里跑通后一直放着。后来换电脑重装环境第一次执行直接抛ModuleNotFoundError——新机器上忘装requests了。所以我后来把所有依赖都放进了requirements.txt不管脚本多简单换机器先pip install -r requirements.txt。下载数据这件事最理想的状态就是让它成为一个不消耗注意力的后台任务真正需要花脑子的永远是数据背后的风资源分析和判断。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑