杂谈
如何优雅地从 EarthData 下载数据
EarthData 是一个由 NASA 维护的数据门户网站,这里可以很方便地检索并下载地理或者气象数据,例如 MODIS 数据。
EarthData 是一个由 NASA 维护的数据门户网站,这里可以很方便地检索并下载地理或者气象数据,例如 MODIS 数据。
但是如果通过网页检索有一个弊端,就是 EarthData 会把检索到的所有文件的下载链接缓存到页面上,然后再生成下载元文件,当检索的数据量过大时,检索时间很长不说,大量的链接文本也会导致页面崩溃,因而前功尽弃。
本文提供一个通过 python 接口检索下载链接的方法,该方法不是基于爬虫,而是由 NASA 官方提供接口,所以不需要担心被限流等问题。
import os
import sys
import requests
from requests.auth import HTTPBasicAuth
from datetime import datetime
import time
import concurrent.futures
# Earthdata 登录信息
USERNAME = "Your Earthdata Account" # 替换为您的 Earthdata 用户名
PASSWORD = "Your Earthdata Password" # 替换为您的 Earthdata 密码
# 检查年份格式
# 如果希望更加细致的自定义时间,可以参考下面 params 中的 temporal 字段构建方法
start_year = 2000 # 下载时间的起始年
end_year = 2001 # 下载时间的结束年
# API URL 和参数
base_url = "https://cmr.earthdata.nasa.gov/search/granules.json" # NASA 提供的 CMR 查询接口
# 获取下载链接的函数
def get_granules(start_year, end_year):
granules = []
params = {
"short_name": "MOD16A2GF", # 数据集名称
"version": "061", # 数据集版本
"temporal": f"{start_year}-01-01T00:00:00Z,{end_year}-12-31T23:59:59Z", # 时间范围
"page_size": 2000, # 每页返回记录数
}
page = 1 # 分页查询
while True:
print(f"Fetching page {page} for {year}...")
response = requests.get(base_url, params={**params, "page_num": page})
if response.status_code != 200:
print(f"Failed to fetch granules for {year}, page {page}: {response.status_code}")
break
data = response.json()
entries = data.get("feed", {}).get("entry", [])
if not entries:
print(f"No entries found for {year} on page {page}. Exiting.")
break
for entry in entries:
for link in entry["links"]:
if "href" in link and link["href"].endswith(".hdf") and link['href'].startswith("http"):
granules.append(link["href"]) # 提取 .hdf 文件链接
page += 1
return granules # 返回链接列表
get_granules(satrt_year, end_year)
该方法会返回一个列表,每个元素代表一个文件的下载链接。这些链接可以直接复制到浏览器下载,可以使用 Earthdata 提供的下载脚本,或者自己写一个 curl 或 wget 脚本下载。这里提供一个基于 python 的多进程下载脚本。
import os
import sys
import requests
from requests.auth import HTTPBasicAuth
from datetime import datetime
import time
import concurrent.futures
# Earthdata 登录信息
USERNAME = "Your Earthdata Account" # 替换为您的 Earthdata 用户名
PASSWORD = "Your Earthdata Password" # 替换为您的 Earthdata 密码
# 检查年份格式
# 如果希望更加细致的自定义时间,可以参考下面 params 中的 temporal 字段构建方法
start_year = 2000 # 下载时间的起始年
end_year = 2001 # 下载时间的结束年
# API URL 和参数
base_url = "https://cmr.earthdata.nasa.gov/search/granules.json" # NASA 提供的 CMR 查询接口
# 获取下载链接的函数
def get_granules(year):
"""
此处修改为分年下载,从而实现每年并行
"""
granules = []
params = {
"short_name": "MOD16A2GF", # 数据集名称
"version": "061", # 数据集版本
"temporal": f"{year}-01-01T00:00:00Z,{year}-12-31T23:59:59Z", # 时间范围
"page_size": 2000, # 每页返回记录数
}
page = 1 # 分页查询
while True:
print(f"Fetching page {page} for {year}...")
response = requests.get(base_url, params={**params, "page_num": page})
if response.status_code != 200:
print(f"Failed to fetch granules for {year}, page {page}: {response.status_code}")
break
data = response.json()
entries = data.get("feed", {}).get("entry", [])
if not entries:
print(f"No entries found for {year} on page {page}. Exiting.")
break
for entry in entries:
for link in entry["links"]:
if "href" in link and link["href"].endswith(".hdf") and link['href'].startswith("http"):
granules.append(link["href"]) # 提取 .hdf 文件链接
page += 1
return granules # 返回链接列表
# 下载文件的函数
def download_file(url, download_dir, retries=3):
try:
local_filename = os.path.join(download_dir, url.split("/")[-1]) # 从 URL 提取文件名
if os.path.exists(local_filename):
print(f"File {local_filename} already exists. Skipping download.")
return
print(f"Downloading {url} to {local_filename}...")
with requests.get(url, auth=HTTPBasicAuth(USERNAME, PASSWORD), stream=True) as response:
response.raise_for_status() # 如果请求失败,将引发异常
with open(local_filename, "wb") as f:
for chunk in response.iter_content(chunk_size=8192):
f.write(chunk)
print(f"Download complete: {local_filename}")
except requests.exceptions.RequestException as e:
print(f"Error downloading {url}: {e}")
if retries > 0:
print(f"Retrying download for {url}...")
time.sleep(2) # 延迟 2 秒后重试
download_file(url, download_dir, retries - 1)
else:
print(f"Failed to download {url} after retries.")
# 主函数,用于执行并行下载
def download_granules_for_year(year, download_dir):
if not os.path.exists(download_dir):
os.makedirs(download_dir) # 创建下载目录
granules = get_granules(year)
print(f"Found {len(granules)} granules for {year}.")
with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor:
# 使用线程池并行下载
futures = [executor.submit(download_file, url, download_dir) for url in granules]
for future in concurrent.futures.as_completed(futures):
future.result() # 阻塞,直到任务完成
if __name__ == "__main__":
# 允许用户指定下载目录
download_dir = "Your Data Storage Dir"
# 对于每个年份并行下载
for year in range(start_year, end_year + 1):
print(f"Starting downloads for {year}...")
download_granules_for_year(year, os.path.join(download_dir, str(year)))