夜雨聆风学习资料网

ARTICLE · 1344607

IMERG 与 CMORPH 卫星降水下载教程:两段可直接运行的脚本

IMERG 与 CMORPH 卫星降水下载教程:两段可直接运行的脚本

QUOTE

同样是卫星降水,IMERG 和 CMORPH 的下载路径完全不同;把两套流程写成可以直接跑的脚本,再用一场暴雨检验它们的日累积。

—— 气象数据实战

做降水研究,卫星降水是绕不开的资料。最常用的两支是 NASA 的 IMERG 和 NOAA 的 CMORPH。这篇不做产品综述,只解决两件事:

一是 怎么把数据下下来——两段完整脚本,复制就能跑;二是 它们对同一场暴雨的刻画差在哪——用 2021 年河南「7·20」的日累积做对比。

本文看点

01

两种产品的差异

02

两段可运行脚本

03

7·20 暴雨对比

01

DATA

先分清两种产品

两者都是全球 60°S–60°N、长序列、免费;差别主要在分辨率、认证和获取方式。

产品
机构
空间分辨率
时间分辨率
认证
IMERG Final V07B
NASA GES DISC
0.1°
半小时 / 逐日
Earthdata 账号
CMORPH V1.0
NOAA NCEI
0.25°(日)/ 8 km(半小时)
半小时 / 逐日
无需注册

NOTE

两套日产品单位都是 mm/day,时间段都是 UTC 00:00–23:59,可以直接比较。跨日分析记得换算北京时间。

02

IMERG

下载 IMERG(需两步认证)

IMERG 的门槛只有认证:一次配好,之后就是普通 HTTP 下载。

STEP 01写入 ~/.netrc 并改权限

把 Earthdata 账号写进 ~/.netrc,然后 chmod 600 ~/.netrc。

bash

machine urs.earthdata.nasa.gov

  login 你的邮箱账号

  password 你的密码

STEP 02授权 GES DISC 应用

浏览器登录 Earthdata 后打开授权页,点同意(每个账号一次):https://urs.earthdata.nasa.gov/approve_app?client_id=e2WVk8Pw6weeLUKZYOxvTQ

STEP 03跑脚本

下面这段就是完整下载脚本,保存为 imerg_download.py 即可运行。支持逐日与半小时、断点续跑、失败重试。

python

#!/usr/bin/env python3

# -*- coding: utf-8 -*-

"""IMERG Final V07B 下载(逐日 df / 半小时 hh),可直接运行。

准备(一次性):

  1) ~/.netrc 写入 Earthdata 账号并 chmod 600:

    machine urs.earthdata.nasa.gov

    login 你的邮箱

    password 你的密码

  2) 浏览器登录 Earthdata,授权 GES DISC 应用:

    https://urs.earthdata.nasa.gov/approve_app?client_id=e2WVk8Pw6weeLUKZYOxvTQ

用法:

  python imerg_download.py --product df --start 20210720 --end 20210720 --out ./imerg

  python imerg_download.py --product hh --start 20210720 --end 20210720 --out ./imerg --proxy http://127.0.0.1:7890

"""

import argparse

import os

import time

from concurrent.futures import ThreadPoolExecutor, as_completed

from datetime import date, datetime, timedelta

import requests

BASE = "https://gpm1.gesdisc.eosdis.nasa.gov/data/GPM_L3"

MAGIC = b"\x89HDF\r\n\x1a\n"          # HDF5/NetCDF4 文件魔数,用来识别“登录页假文件”

def make_session(proxy=None):

 s = requests.Session()

 s.trust_env = True                 # 自动读取 ~/.netrc

 if proxy:

  s.proxies = {"http": proxy, "https": proxy}

 s.headers["User-Agent"] = "imerg-download/1.0"

 return s

def urls_of_day(d: date, product: str):

 """逐日:1 个文件;半小时:48 个文件,目录按 年/年内日序(DOY) 组织。"""

 if product == "df":

  fn = f"3B-DAY.MS.MRG.3IMERG.{d:%Y%m%d}-S000000-E235959.V07B.nc4"

  return [(f"{BASE}/GPM_3IMERGDF.07/{d:%Y}/{d:%m}/{fn}", fn)]

 doy = d.timetuple().tm_yday

 out = []

 for i in range(48):

  t0 = datetime(2000, 1, 1) + timedelta(minutes=30 * i)

  t1 = t0 + timedelta(minutes=29, seconds=59)

  fn = (f"3B-HHR.MS.MRG.3IMERG.{d:%Y%m%d}-S{t0:%H%M%S}-E{t1:%H%M%S}"

     f".{30 * i:04d}.V07B.HDF5")

  out.append((f"{BASE}/GPM_3IMERGHH.07/{d:%Y}/{doy:03d}/{fn}", fn))

 return out

def fetch(sess, url, dest, retries=3):

 """先写 .part,校验魔数后原子改名;已存在则跳过。"""

 if os.path.exists(dest) and os.path.getsize(dest) > 0:

  return "skip", dest

 part = dest + ".part"

 for k in range(1, retries + 1):

  try:

   with sess.get(url, stream=True, timeout=300) as r:

    r.raise_for_status()

    with open(part, "wb") as f:

     for chunk in r.iter_content(1 << 20):

      f.write(chunk)

   with open(part, "rb") as f:

    if f.read(8) != MAGIC:

     raise RuntimeError("不是 HDF5(可能是登录页,检查 .netrc 与授权)")

   os.replace(part, dest)

   return "ok", dest

  except Exception as e:

   if os.path.exists(part):

    os.remove(part)

   if k == retries:

    return f"fail: {e}", dest

   time.sleep(2 * k)

def main():

 ap = argparse.ArgumentParser()

 ap.add_argument("--product", choices=["df", "hh"], default="df")

 ap.add_argument("--start", required=True, help="YYYYMMDD")

 ap.add_argument("--end", required=True, help="YYYYMMDD")

 ap.add_argument("--out", default="./imerg")

 ap.add_argument("--workers", type=int, default=4)

 ap.add_argument("--proxy", default=None, help="直连慢时再给,如 http://127.0.0.1:7890")

 a = ap.parse_args()

 d0 = datetime.strptime(a.start, "%Y%m%d").date()

 d1 = datetime.strptime(a.end, "%Y%m%d").date()

 sess = make_session(a.proxy)

 tasks = []

 d = d0

 while d <= d1:

  for url, fn in urls_of_day(d, a.product):

   tasks.append((url, os.path.join(a.out, f"{d:%Y}", fn)))

  d += timedelta(days=1)

 os.makedirs(a.out, exist_ok=True)

 print(f"共 {len(tasks)} 个文件 -> {os.path.abspath(a.out)}")

 stat = {}

 with ThreadPoolExecutor(max_workers=a.workers) as ex:

  futs = [ex.submit(fetch, sess, u, p) for u, p in tasks]

  for i, f in enumerate(as_completed(futs), 1):

   st, path = f.result()

   key = st.split(":")[0]

   stat[key] = stat.get(key, 0) + 1

   if key == "fail":

    print("[FAIL]", os.path.basename(path), st)

   if i % 20 == 0 or i == len(futs):

    print(f"{i}/{len(futs)}", stat)

 print("完成:", stat)

if __name__ == "__main__":

 main()

运行:

bash

# 逐日(1 个文件/天,单位 mm/day)

python imerg_download.py --product df --start 20210720 --end 20210720 --out ./imerg

# 半小时(48 个文件/天)

python imerg_download.py --product hh --start 20210720 --end 20210720 --out ./imerg

# 直连慢时再加代理(默认直连)

python imerg_download.py --product df --start 20210720 --end 20210720 --out ./imerg \

 --proxy http://127.0.0.1:7890

NOTE

NASA 是靠 session cookie 放行文件的。脚本用 requests.Session() 自动维持会话;如果用 curl 手搓,必须 -c/-b cookies.txt,否则会在登录重定向里死循环。脚本里的魔数校验就是为了防止把登录页 HTML 当成数据存下来。

03

CMORPH

下载 CMORPH(免注册)

CMORPH 没有任何认证,URL 是规则拼接,直接下就行。脚本默认优先直连,直连失败才降级走代理——这是省流量又不牺牲成功率的最稳做法。

python

#!/usr/bin/env python3

# -*- coding: utf-8 -*-

"""CMORPH V1.0 下载(逐日 dly / 半小时 hh),无需注册,可直接运行。

策略:优先直连(省代理流量),直连失败自动降级走代理(--proxy)。

用法:

  python cmorph_download.py --product dly --start 20210720 --end 20210720 --out ./cmorph

  python cmorph_download.py --product hh  --start 20210720 --end 20210720 --out ./cmorph \

   --proxy http://127.0.0.1:7890

"""

import argparse

import os

import time

from datetime import date, datetime, timedelta

import requests

BASE = ("https://www.ncei.noaa.gov/data/"

  "cmorph-high-resolution-global-precipitation-estimates/access")

MAGIC = b"\x89HDF\r\n\x1a\n"

def urls_of_day(d: date, product: str):

 """逐日:1 个文件;半小时:24 个文件(每文件含 2 个半小时步)。"""

 if product == "dly":

  fn = f"CMORPH_V1.0_ADJ_0.25deg-DLY_00Z_{d:%Y%m%d}.nc"

  return [(f"{BASE}/daily/0.25deg/{d:%Y}/{d:%m}/{fn}", fn)]

 out = []

 for h in range(24):

  fn = f"CMORPH_V1.0_ADJ_8km-30min_{d:%Y%m%d}{h:02d}.nc"

  out.append((f"{BASE}/30min/8km/{d:%Y}/{d:%m}/{d:%d}/{fn}", fn))

 return out

def _try(url, dest, proxy, timeout=120):

 part = dest + ".part"

 sess = requests.Session()

 sess.trust_env = False                       # 默认忽略系统代理 = 直连

 if proxy:

  sess.proxies = {"http": proxy, "https": proxy}

 with sess.get(url, stream=True, timeout=timeout) as r:

  r.raise_for_status()

  with open(part, "wb") as f:

   for chunk in r.iter_content(1 << 20):

    f.write(chunk)

 with open(part, "rb") as f:

  if f.read(8) != MAGIC:

   raise RuntimeError("不是 NetCDF4/HDF5 文件")

 os.replace(part, dest)

def fetch(url, dest, proxy=None, retries=2):

 if os.path.exists(dest) and os.path.getsize(dest) > 0:

  return "skip", dest

 os.makedirs(os.path.dirname(dest), exist_ok=True)

 last = ""

 for k in range(1, retries + 1):

  for mode, px in (("直连", None), ("代理", proxy)):

   if mode == "代理" and not proxy:

    continue

   try:

    _try(url, dest, px)

    return ("ok" if mode == "直连" else "ok-proxy"), dest

   except Exception as e:

    last = f"{mode}: {e}"

    p = dest + ".part"

    if os.path.exists(p):

     os.remove(p)

  time.sleep(2 * k)

 return f"fail({last})", dest

def main():

 ap = argparse.ArgumentParser()

 ap.add_argument("--product", choices=["dly", "hh"], default="dly")

 ap.add_argument("--start", required=True, help="YYYYMMDD")

 ap.add_argument("--end", required=True, help="YYYYMMDD")

 ap.add_argument("--out", default="./cmorph")

 ap.add_argument("--proxy", default=None, help="直连不通时才用,如 http://127.0.0.1:7890")

 a = ap.parse_args()

 d0 = datetime.strptime(a.start, "%Y%m%d").date()

 d1 = datetime.strptime(a.end, "%Y%m%d").date()

 tasks = []

 d = d0

 while d <= d1:

  for url, fn in urls_of_day(d, a.product):

   sub = "daily/0.25deg" if a.product == "dly" else f"30min/8km/{d:%Y}/{d:%m}/{d:%d}"

   tasks.append((url, os.path.join(a.out, sub, fn)))

  d += timedelta(days=1)

 print(f"共 {len(tasks)} 个文件 -> {os.path.abspath(a.out)}")

 stat = {}

 for i, (u, p) in enumerate(tasks, 1):

  st, path = fetch(u, p, a.proxy)

  key = st.split("(")[0]

  stat[key] = stat.get(key, 0) + 1

  if key == "fail":

   print("[FAIL]", os.path.basename(path), st)

  if i % 10 == 0 or i == len(tasks):

   print(f"{i}/{len(tasks)}", stat)

 print("完成:", stat)

if __name__ == "__main__":

 main()

运行:

bash

# 逐日 0.25°(1 个文件/天)

python cmorph_download.py --product dly --start 20210720 --end 20210720 --out ./cmorph

# 半小时 8km(24 个文件/天,每文件含 2 个半小时步)

python cmorph_download.py --product hh --start 20210720 --end 20210720 --out ./cmorph

NOTE

三个容易踩的点:逐日变量名是 cmorph(别用 data_vars[0],会取到 time_bounds);经度是 0–360,用前先换算到 −180~180;半小时文件每天 24 个,不是 48 个。

关于代理:卫星降水站多在境外,先直连试试;直连能跑就别挂代理,速度慢到影响进度时再加 --proxy。两段脚本都按这个原则写的。

04

CASE

同一场暴雨,两种画法

各取 2021-07-20(UTC) 的日累积——对应北京时 7 月 20 日 08 时至 21 日 08 时,正是河南「7·20」最强的一天。

— 2021-07-20 日累积降水:左 IMERG(0.1°),右 CMORPH(0.25°)

两张图放在一起,能读出三个结论:

① 落区高度一致。强降水中心都锁定在河南中部、郑州一带,说明大尺度雨带的判断没有分歧。

② 量级与形态不同。河南区域最大日累积 CMORPH 约 415 mm,IMERG 约 313 mm;但 CMORPH 是 0.25° 网格,强中心更连片平滑,IMERG 0.1° 能勾出暴雨核心的精细结构。

③ 整体偏差不大。河南及周边逐格点相关约 0.95,RMSE 约 30 mm,相对 300 mm 级的暴雨属于可接受区间。

河南区域指标
IMERG
CMORPH
最大日累积 (mm)
313
415
区域平均 (mm)
47
50
>50 mm 面积占比
31%
28%
>100 mm 面积占比
17%
16%

要暴雨的精细结构,用 IMERG(0.1°);要 1998 年以来的长序列、又不想折腾账号,用 CMORPH;做定量评估,两者交叉验证最稳。

∞

EPILOGUE

写在最后

IMERG 难在认证,一次配好就顺;CMORPH 难在没有门槛,反而要注意经度和变量名。两段脚本都可以直接拿走用,换日期就能跑自己的个例。

END

我是 气象数据实战,持续分享气象数据获取与处理实战。

如果这篇对你有用,欢迎点赞、在看、转发三连,我们下篇见。

回首页,搜索你想要的文档

在首页搜索框输入关键词,快速定位资料

文库首页搜索指引 立即搜索

相关学习资料