基于Python earthaccess库批量下载全球MODIS GPP(MOD17A2HGF)数据

本文介绍基于脚本,快速、批量下载 Earthdata 中遥感影像数据的方法。

最近,需要下载 MODIS 的 GPP 数据,时间跨度从 2000 年到 2024 年,时间分辨率为 8 天,覆盖全球陆地范围。数据来源选择的是 NASA LP DAAC 提供的 MOD17A2HGF v6.1 产品------这是 MOD17A2H 的 Gap-Filled(间隙填充)版本,在年末阶段对 FPAR/LAI 输入质量较差的像元进行了清洁处理,能有效消除云污染导致的伪异常,适合做长时序分析。

之前的几篇文章中,我们多次介绍过不同的遥感影像批量下载方法,包括基于浏览器插件、本地下载器、谷歌地球引擎GEE平台等;但是,一直都没介绍过基于脚本的下载方法------而基于脚本下载,可能反而是最简单、最快捷的方法。

因此,这篇文章记录一下用 Python 批量下载这批数据的思路和完整代码------只要是需要批量下载 Earthdata 数据的,都可以参考本文思路。大家可以直接将本文发给 Agent,让 AI 一键部署本文所需的环境与脚本,真的就是点点鼠标就能批量下载了。


数据概况

本文以 MOD17A2HGF 数据为例来介绍(但 Earthdata 中的其他数据都可以用本文的方法)。MOD17A2HGF 是 Terra 卫星 MODIS 传感器生产的全球陆地总初级生产力(GPP)产品。

这一产品的主要参数如下:

项目 内容
产品名 MOD17A2HGF v6.1
时间分辨率 8 天合成
空间分辨率 500 m(原生分辨率)
数据格式 HDF4(.hdf)
空间覆盖 全球陆地,约 286~326 个 MODIS 瓦片/周期
数据时段 2000-02-18 至今
下载来源 NASA LP DAAC(earthaccess API)

全球完整下载一套(2000-2024)大约需要 1144 个 8 天周期 (2000 年从 MODIS 数据起始日 2 月 18 日开始共 40 个周期,2001~2024 年每年 46 个),原始 HDF 文件总量约 1100 GB。单靠手动从网页点击下载显然不现实,所以用脚本来做。


环境准备

主要依赖两个库:

bash 复制代码
pip install earthaccess tqdm

earthaccess 是 NASA 官方出品的 Python 库,专门用于搜索和下载 Earthdata(包括 MODIS 在内的所有 NASA 数据)。tqdm 用于显示下载进度条。

另外需要注册一个 NASA Earthdata 账号,地址是 https://urs.earthdata.nasa.gov,注册完成后在账号页面给 LP DAAC Data Pool 这个应用授权,否则下载会报 401 错误。

认证方面,推荐使用 .netrc 文件方式,在 Windows 上对应的文件路径是 C:\Users\<用户名>\_netrc,内容格式如下:

复制代码
machine urs.earthdata.nasa.gov
login 你的用户名
password 你的密码

这种方式最稳定,不依赖 Token API,在有代理软件的 Windows 环境下也能正常工作。如果系统装了 Clash、V2Ray 等工具,即使其是"关闭"状态,Windows 系统代理设置有时仍然生效,会导致 HTTPS 连接被拦截。解决方案是在脚本里显式设置 NO_PROXY="*",让 Python 的请求绕过系统代理。


代码设计思路

下载脚本的整体逻辑不复杂,核心是三步:

第一步,生成 MODIS 8 天周期列表。MODIS 的 8 天合成不是任意的 8 天,而是从每年第 1 天(1 月 1 日)开始,每 8 天一个周期,全年固定 46 个周期(最后一个周期可能不足 8 天)。所以需要先把起止日期对齐到 MODIS 的标准周期边界,再逐一生成周期列表。

第二步,按周期搜索并下载颗粒 。用 earthaccess.search_data() 搜索指定时间范围内的数据颗粒(Granule),每个颗粒对应一个 MODIS 瓦片的 HDF 文件。全球范围每个周期大约有 290 个颗粒。搜索完成后,用多线程并发下载,默认 8 个线程,实测速度稳定在每个周期 3~5 分钟,全部下载完大约需要 3 天左右。

第三步,断点续传与完整性校验 。下载过程中用一个 JSON 文件记录已完成的周期。每次完成一个周期后,会进行三重校验:文件数是否正好 290 个、所有 HDF 文件的 MODIS 周期标签(如 A2001001)是否与目录名完全一致。只有三项全部通过,才标记该周期完成并写入进度文件。下次运行时加 --resume 参数即可从断点继续,不达标的周期会被自动跳过重新下载。

此外,每个颗粒的下载支持最多 3 次指数退避重试,下载时先写入 .tmp 临时文件,完成后重命名,防止意外中断导致的不完整文件被当作有效文件跳过。


完整代码

python 复制代码
#!/usr/bin/env python3
"""
MODIS MOD17A2HGF v6.1 GPP 全球下载脚本(纯下载版)
=====================================================
仅从 NASA LP DAAC 下载 HDF 文件,按 MODIS 标准 8 天周期组织目录存储。
下载完成后,使用本地 ArcPy 将 HDF 批量转为 GeoTIFF。

特性:
  - 多线程下载:并行下载多个瓦片,大幅提升速度
  - 断点续传:记录每个已完成周期的下载状态,可随时恢复
  - 完整性校验:文件数 = 290 + 标签一致性双重验证
  - 跨年过滤:自动排除跨年日期范围匹配到的非目标周期颗粒
  - 交互式/环境变量/ netrc 认证:灵活适配不同运行环境
  - 详细日志:同时输出到控制台和日志文件

使用方法:
  # 交互式登录(首次需输入用户名密码,自动保存凭据到 ~/.netrc)
  python lpdaac_gpp_download_only.py

  # 环境变量认证
  set EARTHDATA_USERNAME=myuser
  set EARTHDATA_PASSWORD=mypass
  python lpdaac_gpp_download_only.py

  # 恢复中断的下载
  python lpdaac_gpp_download_only.py --resume

  # 指定日期范围
  python lpdaac_gpp_download_only.py --start 2010-01-01 --end 2015-12-31

  # 调整并行线程数(默认 8,NASA 限制约 10-15)
  python lpdaac_gpp_download_only.py --workers 12

  # 只检查哪些周期缺失,不实际下载
  python lpdaac_gpp_download_only.py --dry-run

依赖:
  pip install earthaccess tqdm
"""

import argparse
import json
import logging
import os
import re
import sys
import time
from concurrent.futures import ThreadPoolExecutor, as_completed
from datetime import datetime, timedelta
from pathlib import Path

import earthaccess
from tqdm import tqdm

# ============================================================
# 日志配置
# ============================================================
logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s [%(levelname)s] %(message)s",
    handlers=[
        logging.StreamHandler(sys.stdout),
    ],
)
logger = logging.getLogger(__name__)

# ============================================================
# 常量
# ============================================================
MODIS_PRODUCT = "MOD17A2HGF"
MODIS_VERSION = "061"
MODIS_TERRA_START = datetime(2000, 2, 18)

# 默认路径
DEFAULT_RAW_DIR = r"F:\MODIS_GPP\raw_hdf"
PROGRESS_FILE = r"F:\MODIS_GPP\gpp_download_progress.json"
LOG_FILE = r"F:\MODIS_GPP\gpp_download.log"

# 每个 8 天周期预计瓦片数(全球覆盖约 286~326 个)
# MOD17A2HGF 全球陆地范围约 290 个瓦片
EXPECTED_TILES = 290  # 标准瓦片数(MOD17A2HGF 全球陆地覆盖)


# ============================================================
# 辅助函数
# ============================================================

def generate_8day_periods(start_date, end_date):
    """生成 MODIS 标准 8 天合成周期列表,按年对齐。

    MODIS 8 天周期每年从 Day 001 开始,周期标签为 YYYYDDD:
      - 2000 年首个可用周期 Day 049(2000-02-18)
      - 2001 年起首个周期 Day 001(2001-01-01)
      - 每年 46 个周期(365/8 ≈ 45.6),最后一周期跨年

    Returns:
        list of (period_start: datetime, period_end: datetime, period_key: str)
        period_key 格式为 YYYYDDD,如 "2001001"
    """
    periods = []

    for year in range(start_date.year, end_date.year + 1):
        first_doy = 49 if year == 2000 else 1

        for doy_start in range(first_doy, 367, 8):
            try:
                period_start = datetime(year, 1, 1) + timedelta(days=doy_start - 1)
            except (ValueError, OverflowError):
                break  # 闰年溢出

            if period_start.year != year:
                break  # 已超出当年

            if period_start < start_date:
                continue

            if period_start > end_date:
                return periods

            period_end = period_start + timedelta(days=7)
            period_key = f'{year}{doy_start:03d}'
            periods.append((period_start, period_end, period_key))

    return periods


def authenticate():
    """使用 earthaccess 进行 NASA Earthdata 认证。

    优先级: .netrc 文件 > 环境变量 > 交互式登录
    earthaccess v0.18 的 strategy="environment" 会调用 token API(需要 SSL),
    在无代理环境下可能失败,因此优先使用 .netrc(直接使用 HTTP Basic Auth 下载)。

    同时设置 NO_PROXY 环境变量,防止 Windows 系统代理干扰连接。
    """
    # 防止 Windows 系统代理(如 Clash/V2Ray)干扰 HTTPS 连接
    os.environ["NO_PROXY"] = "*"
    os.environ["no_proxy"] = "*"

    # 优先级1: .netrc 文件(最稳定,不依赖 token API)
    try:
        earthaccess.login(strategy="netrc")
        logger.info("[OK] 使用 .netrc 文件认证成功")
        return
    except Exception as e:
        logger.debug(f"netrc 认证失败: {e}")

    # 优先级2: 环境变量(EARTHDATA_TOKEN 或 EARTHDATA_USERNAME/PASSWORD)
    try:
        earthaccess.login(strategy="environment")
        logger.info("[OK] 使用环境变量认证成功")
        return
    except Exception as e:
        logger.debug(f"环境变量认证失败: {e}")

    # 优先级3: 交互式登录
    try:
        earthaccess.login(strategy="interactive")
        logger.info("[OK] NASA Earthdata 交互式认证成功(凭据已保存到 .netrc)")
    except Exception as e:
        logger.error(f"[FAIL] 认证失败: {e}")
        logger.error("请前往 https://urs.earthdata.nasa.gov 注册 NASA Earthdata 账号")
        logger.error("或确认 ~/.netrc(Windows 下为 ~/_netrc)文件配置正确")
        sys.exit(1)


def download_single_granule(granule, output_dir, max_retries=3):
    """下载单个数据颗粒,支持重试。

    Args:
        granule: earthaccess 颗粒对象
        output_dir: 输出目录
        max_retries: 最大重试次数

    Returns:
        (granule, local_path, success: bool)
    """
    for attempt in range(max_retries):
        try:
            links = granule.data_links()
            if not links:
                return (granule, None, False)

            url = links[0]
            filename = os.path.basename(url)
            local_path = os.path.join(output_dir, filename)

            # 检查文件是否已存在且大小合理(> 100KB 认为有效)
            if os.path.exists(local_path) and os.path.getsize(local_path) > 102400:
                return (granule, local_path, True)

            # 下载
            session = earthaccess.get_requests_https_session()
            response = session.get(url, stream=True, timeout=120)
            response.raise_for_status()

            # 写入临时文件,完成后重命名(防止部分写入)
            temp_path = local_path + ".tmp"
            with open(temp_path, "wb") as f:
                for chunk in response.iter_content(chunk_size=8192):
                    if chunk:
                        f.write(chunk)

            os.rename(temp_path, local_path)
            return (granule, local_path, True)

        except Exception as e:
            if attempt < max_retries - 1:
                wait = 2 ** attempt
                logger.debug(f"  下载重试 {attempt+1}/{max_retries} (等待{wait}s): {e}")
                time.sleep(wait)
            else:
                logger.warning(f"  下载失败 {filename}: {e}")
                return (granule, None, False)

    return (granule, None, False)


def download_granules_parallel(granules, output_dir, max_workers=8):
    """多线程下载颗粒。

    Args:
        granules: earthaccess 颗粒列表
        output_dir: 输出目录
        max_workers: 最大线程数

    Returns:
        (downloaded_list, failed_count)
    """
    os.makedirs(output_dir, exist_ok=True)

    downloaded = []
    failed = 0

    with ThreadPoolExecutor(max_workers=max_workers) as executor:
        futures = {
            executor.submit(download_single_granule, g, output_dir): g
            for g in granules
        }

        with tqdm(total=len(futures), desc="  下载瓦片", unit="瓦片",
                  ncols=80, leave=False) as pbar:
            for future in as_completed(futures):
                granule, path, success = future.result()
                if success and path:
                    downloaded.append(path)
                else:
                    failed += 1
                pbar.update(1)
                pbar.set_postfix(ok=len(downloaded), fail=failed)

    logger.info(f"  下载完成: {len(downloaded)} 成功, {failed} 失败")
    return downloaded, failed


def count_hdf_files(directory):
    """统计目录中 .hdf 文件数量。"""
    if not os.path.exists(directory):
        return 0
    return len([f for f in os.listdir(directory) if f.lower().endswith(".hdf")])


def validate_period_dir(directory, expected_key):
    """验证目录中的 HDF 文件是否完整且标签一致。

    Args:
        directory: 目录路径
        expected_key: 期望的 MODIS 周期标签(如 "2001001")

    Returns:
        (is_valid: bool, total_count: int, mismatch_count: int)
    """
    if not os.path.exists(directory):
        return False, 0, 0

    files = [f for f in os.listdir(directory) if f.lower().endswith(".hdf")]
    total = len(files)

    if total != EXPECTED_TILES:
        return False, total, 0

    # 检查每个文件的 MODIS 标签是否与目录一致
    pattern = f"A{expected_key}."
    mismatches = 0
    for f in files:
        if pattern not in f:
            mismatches += 1

    return (mismatches == 0), total, mismatches


def load_progress(progress_file):
    """加载下载进度。"""
    if os.path.exists(progress_file):
        with open(progress_file, "r") as f:
            return json.load(f)
    return {"completed_periods": [], "start_time": datetime.now().isoformat()}


def save_progress(progress, progress_file):
    """保存下载进度。"""
    os.makedirs(os.path.dirname(progress_file), exist_ok=True)
    with open(progress_file, "w") as f:
        json.dump(progress, f, indent=2)


def estimate_total_size(periods, granules_per_period=290, mb_per_granule=2.2):
    """估算总下载量。"""
    total_mb = len(periods) * granules_per_period * mb_per_granule
    return total_mb / 1024  # GB


# ============================================================
# 主流程
# ============================================================

def main():
    parser = argparse.ArgumentParser(
        description="MODIS MOD17A2HGF v6.1 GPP 全球 HDF 下载脚本(纯下载版)",
        formatter_class=argparse.RawDescriptionHelpFormatter,
    )
    parser.add_argument("--start", default="2000-02-18",
                        help="起始日期 (YYYY-MM-DD), 默认 2000-02-18")
    parser.add_argument("--end", default="2024-12-31",
                        help="结束日期 (YYYY-MM-DD), 默认 2024-12-31")
    parser.add_argument("--raw-dir", default=DEFAULT_RAW_DIR,
                        help=f"HDF 文件存储目录, 默认 {DEFAULT_RAW_DIR}")
    parser.add_argument("--workers", type=int, default=8,
                        help="并行下载线程数, 默认 8(NASA 限制约 10-15)")
    parser.add_argument("--resume", action="store_true",
                        help="从上次中断处恢复下载")
    parser.add_argument("--dry-run", action="store_true",
                        help="仅统计周期数和预估数据量,不实际下载")
    parser.add_argument("--force", action="store_true",
                        help="强制重新下载已完成的周期(补全缺失文件)")

    args = parser.parse_args()

    # 初始化目录
    os.makedirs(args.raw_dir, exist_ok=True)

    # 配置文件日志
    os.makedirs(os.path.dirname(LOG_FILE), exist_ok=True)
    file_handler = logging.FileHandler(LOG_FILE, encoding="utf-8")
    file_handler.setFormatter(logging.Formatter("%(asctime)s [%(levelname)s] %(message)s"))
    logger.addHandler(file_handler)

    start_date = datetime.strptime(args.start, "%Y-%m-%d")
    end_date = datetime.strptime(args.end, "%Y-%m-%d")

    # 认证
    logger.info("=" * 60)
    logger.info("MODIS MOD17A2HGF v6.1 GPP 全球 HDF 下载(纯下载版)")
    logger.info("=" * 60)
    authenticate()

    # 生成周期列表
    periods = generate_8day_periods(start_date, end_date)
    logger.info(f"总 8 天周期数: {len(periods)}")
    logger.info(f"时间范围: {periods[0][0].strftime('%Y-%m-%d')} ~ "
                f"{periods[-1][1].strftime('%Y-%m-%d')}")
    logger.info(f"并行线程数: {args.workers}")
    logger.info(f"HDF 存储目录: {args.raw_dir}")

    # 预估数据量
    total_gb = estimate_total_size(periods)
    logger.info(f"预估下载总量: ~{total_gb:.0f} GB({len(periods)} 周期 × ~290 瓦片/周期)")
    logger.info(f"预计存储占用: ~{total_gb:.0f} GB(HDF 原始文件)")

    if args.dry_run:
        logger.info("=== Dry Run 模式,不实际下载 ===")
        for i, (ps, pe, pk) in enumerate(periods[:8]):
            logger.info(f"  周期 {i+1}: {ps.strftime('%Y-%m-%d')} ~ {pe.strftime('%Y-%m-%d')} [{pk}]")
        if len(periods) > 8:
            logger.info(f"  ... 共 {len(periods)} 个周期")
        # 检查已有文件
        completed = 0
        for ps, pe, pk in periods:
            period_dir = os.path.join(args.raw_dir, pk)
            is_valid, n, mismatches = validate_period_dir(period_dir, pk)
            if is_valid:
                completed += 1
        logger.info(f"本地已有完整周期: {completed}/{len(periods)}")
        return

    # 加载进度
    progress = load_progress(PROGRESS_FILE) if args.resume else {
        "completed_periods": [], "start_time": datetime.now().isoformat()
    }
    completed = set(progress.get("completed_periods", []))
    if completed and args.resume:
        logger.info(f"恢复模式: 已完成 {len(completed)}/{len(periods)} 个周期")

    # 统计
    total_downloaded = 0
    total_failed = 0
    start_time = time.time()

    # 逐周期下载
    for i, (period_start, period_end, period_key) in enumerate(periods):
        period_dir = os.path.join(args.raw_dir, period_key)

        # 检查是否已下载完成(目录存在、290 文件、标签全部一致)
        if not args.force:
            if period_key in completed or os.path.exists(period_dir):
                is_valid, total, mismatches = validate_period_dir(period_dir, period_key)
                if is_valid:
                    if period_key not in completed:
                        completed.add(period_key)
                        progress["completed_periods"] = sorted(completed)
                        save_progress(progress, PROGRESS_FILE)
                    logger.info(f"  周期 {period_key} 已完成 ✓ (290 文件, 标签一致),跳过")
                    continue
                elif total > 0:
                    # 有文件但不达标:显示详情
                    if mismatches > 0:
                        logger.warning(f"  周期 {period_key} 已有 {total} 个文件但 {mismatches} 个标签不一致,重新下载")
                    else:
                        logger.warning(f"  周期 {period_key} 仅 {total} 个文件(需 290),重新下载")
                    completed.discard(period_key)
                    progress["completed_periods"] = sorted(completed)
                    save_progress(progress, PROGRESS_FILE)

        # 预估剩余时间
        elapsed = time.time() - start_time
        if total_downloaded > 0:
            avg_time = elapsed / total_downloaded
            remaining = avg_time * (len(periods) - len(completed))
            eta_str = f" (ETA: {remaining/3600:.1f}h)"
        else:
            eta_str = ""

        logger.info(f"\n--- 周期 {i+1}/{len(periods)}: "
                    f"{period_start.strftime('%Y-%m-%d')} ~ "
                    f"{period_end.strftime('%Y-%m-%d')}{eta_str} ---")

        # Step 1: 搜索颗粒
        try:
            granules = earthaccess.search_data(
                short_name=MODIS_PRODUCT,
                version=MODIS_VERSION,
                temporal=(period_start, period_end),
            )
        except Exception as e:
            logger.error(f"  搜索颗粒失败: {e}")
            total_failed += 1
            continue

        if not granules:
            logger.warning(f"  未找到颗粒,跳过此周期")
            completed.add(period_key)
            progress["completed_periods"] = sorted(completed)
            save_progress(progress, PROGRESS_FILE)
            continue

        logger.info(f"  搜索到 {len(granules)} 个颗粒")

        # 关键步骤:按 MODIS 周期标签 A{YYYYDDD} 过滤
        # 跨年日期范围(如 12-27 ~ 01-03)可能匹配到多个 MODIS 周期,
        # 必须只保留文件名含 "A{period_key}." 的颗粒
        pattern = f"A{period_key}."
        filtered = []
        for g in granules:
            try:
                links = g.data_links()
                if links and pattern in links[0]:
                    filtered.append(g)
            except Exception:
                pass

        if len(filtered) != len(granules):
            logger.info(f"  过滤后保留 {len(filtered)} 个(排除 {len(granules) - len(filtered)} 个非同周期颗粒)")
        else:
            logger.info(f"  全部 {len(filtered)} 个颗粒均为目标周期")

        if not filtered:
            logger.warning(f"  过滤后无颗粒,跳过此周期")
            completed.add(period_key)
            progress["completed_periods"] = sorted(completed)
            save_progress(progress, PROGRESS_FILE)
            continue

        granules = filtered
        logger.info(f"  开始 {args.workers} 线程下载...")

        # Step 2: 多线程下载
        os.makedirs(period_dir, exist_ok=True)
        downloaded, failed_count = download_granules_parallel(
            granules, period_dir, max_workers=args.workers
        )

        # Step 3: 校验下载完整性(文件数 = 290 且标签全部一致)
        is_valid, total, mismatches = validate_period_dir(period_dir, period_key)
        if is_valid:
            logger.info(f"  [OK] 周期 {period_key} 下载完成且验证通过 "
                        f"({total} 文件, 标签一致)")
            completed.add(period_key)
            progress["completed_periods"] = sorted(completed)
            save_progress(progress, PROGRESS_FILE)
            total_downloaded += 1
        elif total > 0:
            mismatch_info = f", {mismatches} 标签不一致" if mismatches > 0 else ""
            logger.warning(f"  [WARN] 周期 {period_key} 下载不完整 "
                          f"({total}/290 文件{mismatch_info}),下次运行将自动补全")
            total_failed += 1
        else:
            logger.warning(f"  [WARN] 周期 {period_key} 下载失败 (0 文件)")
            total_failed += 1

        # 每 10 个周期输出一次汇总
        if (i + 1) % 10 == 0:
            elapsed = time.time() - start_time
            speed = total_downloaded / (elapsed / 3600) if elapsed > 0 else 0
            logger.info(f"  >>> 进度: {len(completed)}/{len(periods)} 完成, "
                        f"速度: {speed:.1f} 周期/小时")

    # 最终汇总
    total_time = (time.time() - start_time) / 3600 if start_time else 0
    logger.info("\n" + "=" * 60)
    logger.info("下载完成!")
    logger.info(f"总耗时: {total_time:.1f} 小时")
    logger.info(f"成功: {len(completed)}/{len(periods)} 个周期")
    logger.info(f"失败/不完整: {total_failed}")
    logger.info(f"HDF 文件存储: {os.path.abspath(args.raw_dir)}")

    # 统计本地文件
    total_hdf = 0
    for d in os.listdir(args.raw_dir):
        dpath = os.path.join(args.raw_dir, d)
        if os.path.isdir(dpath):
            n = len([f for f in os.listdir(dpath) if f.lower().endswith(".hdf")])
            total_hdf += n
    total_size_mb = sum(
        os.path.getsize(os.path.join(root, f))
        for root, _, files in os.walk(args.raw_dir)
        for f in files if f.lower().endswith(".hdf")
    ) / (1024 * 1024)
    logger.info(f"本地 HDF 文件总数: {total_hdf}")
    logger.info(f"本地 HDF 文件总大小: {total_size_mb:.0f} MB ({total_size_mb/1024:.1f} GB)")

    # 列出未完成的周期
    incomplete = []
    for ps, pe, pk in periods:
        if pk not in completed:
            incomplete.append(pk)
    if incomplete:
        logger.info(f"未完成周期 ({len(incomplete)}):")
        for pk in incomplete[:10]:
            logger.info(f"  {pk}")
        if len(incomplete) > 10:
            logger.info(f"  ... 还有 {len(incomplete)-10} 个")

    logger.info("=" * 60)
    logger.info("下一步: 使用 hdf2tif 脚本将 HDF 批量转为 GeoTIFF")


if __name__ == "__main__":
    main()

关键参数说明

脚本中几个容易需要根据实际情况调整的变量如下:

参数 默认值 说明
DEFAULT_RAW_DIR F:\MODIS_GPP\raw_hdf HDF 文件存储目录,按需修改到有足够空间的磁盘
--start / --end 2000-02-18 / 2024-12-31 下载的时间范围
--workers 8 并行下载线程数,NASA 服务器一般限制约 10~15 个并发,不建议超过 15
EXPECTED_TILES 290 认为一个周期"下载完整"所需的标准文件数,MOD17A2HGF 全球陆地覆盖约为 290 个瓦片

命令行用法如下:

bash 复制代码
# 完整下载 2000-2024(首次运行)
python lpdaac_gpp_download_only.py

# 断点续传(中途中断后恢复)
python lpdaac_gpp_download_only.py --resume

# 先检查本地已有多少,不实际下载
python lpdaac_gpp_download_only.py --dry-run

# 调整线程数为 12,加快下载
python lpdaac_gpp_download_only.py --workers 12

# 自定义时间范围
python lpdaac_gpp_download_only.py --start 2010-01-01 --end 2020-12-31

几个踩坑记录

关于 MOD17A2HGF 与 MOD17A2H 的选择:如果做长时序分析,建议优先选 GF 版本。标准版 MOD17A2H 在年末几个周期会因为 FPAR/LAI 质量差而出现明显的低估异常,在时序曲线上表现为突然的负值或极低值,用 GF 版本可以规避这个问题。

关于 Windows 代理干扰问题 :这个问题比较隐蔽。即便在 Clash 或 V2Ray 里点击了"关闭系统代理",Windows 注册表里的代理设置有时候不会立即清除,导致 Python 的 requests 库仍然走代理,在代理不稳定或没有开启 TUN 模式时会出现 SSL 握手失败(SSLEOFError)。解决方法是在脚本里显式设置 os.environ["NO_PROXY"] = "*"os.environ["no_proxy"] = "*",强制让所有请求绕过代理直连。

关于 earthaccess v0.18 认证接口变更 :早期版本的 earthaccess.login() 支持 strategy="password" 直接传用户名密码,v0.18 之后这个策略被移除,改成了 strategy="netrc"(读取 .netrc 文件)、strategy="environment"(读取环境变量)和 strategy="interactive"(交互式引导)三种方式。如果用老版本代码会报 ValueError: Invalid strategy,换用上面的方式就好。

至此,大功告成。

欢迎关注:疯狂学习GIS

相关推荐
至乐活着1 小时前
用DeepSeek打造你自己的智能问答系统:从零到一的完整指南
python·deepseek·ai应用开发·智能问答系统·api教程
AI创界者1 小时前
【解压即用】Scail-2 视频动作迁移一键整合包:8G显存通吃50系,长视频/多人/精准目标替换全攻略
人工智能·python·aigc·音视频
花月C2 小时前
AI驱动的竞品分析多Agent协作系统设计理论
人工智能·python·ai·agent·ai编程
abcy0712132 小时前
python InsecureClient 上传下载查看删除实例
python·hdfs
MATLAB代码顾问2 小时前
Python NumPy数值计算核心指南
开发语言·python·numpy
FBI HackerHarry浩2 小时前
修改Pycharm2023.2.5连接数据库创建的SQL文件保存的默认位置
python·pycharm
老徐聊GEO2 小时前
AI搜索获客:亲测有效的实践案例分享
大数据·人工智能·python
HKkuaidou2 小时前
基于深度学习的药用草本植物识别系统
pytorch·python·深度学习·resnet
码云骑士2 小时前
05-Python字典底层原理-Hash表与有序性的真相
开发语言·python·哈希算法