简介:这份资源是一套面向遥感数据处理与地理信息分析人员的Python哨兵影像自动下载脚本,主要解决Sentinel卫星影像批量获取效率低、离线产品需手动触发检索等痛点。脚本支持离线产品下载,请求后自动从LTA检索并等待原始URL可用;具备断点续传能力,运行后可无人值守;同时提供矢量范围检索格式,便于按研究区边界精准筛选影像。使用前需安装sentinelsat库,并登录Copernicus Open Access Hub注册账号获取用户名与密码。资源包共3个文件,包含2个py源码与1个pyc编译文件,压缩包约4KB,核心逻辑与运行入口分离,结构紧凑便于二次修改。目前已有1017人学习下载,适合具备一定Python基础、需要批量下载哨兵影像的科研人员与GIS从业者参考使用。
1. 哨兵影像自动下载脚本:从手动点选到批量拉取的工程化路径
每个月都要登录哥白尼数据空间,按瓦片、按时间、按云量一遍遍筛选,再逐个点击下载——如果你做过遥感影像的时序分析,这套流程一定不陌生。哨兵影像自动下载脚本要解决的,就是把这条重复链路压缩成一条命令:给定研究区范围、时间窗口和云量阈值,脚本自动完成检索、筛选、排队、下载、校验,最后落到本地目录里可以直接被 ENVI 或 Python 读取。它适合做土地利用变化、农作物长势监测、水体提取这类需要几十上百景影像的从业者,也适合刚接触遥感数据、想把 Python 自动化跑通的新手。核心不在脚本本身多复杂,而在于把认证、检索、限流、断点续传这几个环节串成一条稳定管线。
2. 下载链路拆解:认证、检索、排队、落盘四段怎么接
2.1 为什么不能直接爬下载链接
哨兵影像的获取入口是哥白尼数据空间,它提供的是带身份校验的 OData 检索接口和基于令牌的下载通道。直接拿浏览器里复制出来的下载链接去 requests.get,大概率返回 401 或 403,因为链接背后绑定了会话令牌,且有效期很短。常见做法是走官方提供的 Python 客户端库,由它负责 OAuth 令牌的申请与刷新,脚本只关心检索条件和下载目录。这样做的另一个好处是,客户端库内部已经处理了分页和限流退避,不用自己从零实现重试逻辑。
选型上,我一般会区分两种场景:如果只是偶尔拉几景,用官方客户端库的交互式认证就够了;如果要挂到服务器上定时跑,就得用离线令牌或者服务账号,避免每次都要人工点授权。这一步选错,后面脚本再优雅也跑不起来。
2.2 检索条件怎么写才不漏不滥
检索的核心是四个维度:地理范围、时间区间、产品级别、云量上限。地理范围用 WKT 或 GeoJSON 描述,注意坐标系要统一到 WGS84,否则会出现明明范围对但检索结果为空的情况。时间区间用 ISO 8601 格式,起止都包含。产品级别按需求选 L1C 还是 L2A,L2A 已经做过大气校正,做植被指数直接用 L2A 省事,但数据量更大。云量上限不是越小越好,设成 10% 以下在雨季可能一景都搜不到,我一般先设 30% 看返回数量,再逐步收紧。
# 检索条件构造示例,参数按实际研究区替换 search_params = { "collection": "SENTINEL-2", "bbox": [116.0, 39.5, 116.8, 40.2], # 西,南,东,北,WGS84 "start_date": "2024-03-01T00:00:00Z", "end_date": "2024-06-30T23:59:59Z", "product_type": "S2MSI2A", # L2A 产品 "cloud_cover_max": 30, # 云量百分比上限 "limit": 100 # 单次返回条数 }这段字典是检索的输入,bbox 四个值顺序不能错,写反了会搜到地球另一头。cloud_cover_max 是百分比整数,不是小数。limit 控制单页返回量,实际使用时配合分页循环取全量。
2.3 排队与限流:别把接口当自家硬盘
检索到结果后,下载环节最容易翻车。官方通道对并发连接数有隐性限制,同时开十几个线程去拉,轻则速度骤降,重则触发临时封禁。稳妥的做法是控制并发在 2 到 4 之间,并在每次请求之间加一个短延迟。更关键的是断点续传:下载中断后重新跑脚本,已经完整的文件要跳过,不完整的要能续上。判断依据可以用文件大小比对,也可以用校验和,前者快但不够严谨,后者稳但要多一次读取。
import os import time from pathlib import Path def download_with_resume(client, product_id, out_dir, retries=3): out_path = Path(out_dir) / f"{product_id}.zip" for attempt in range(retries): try: if out_path.exists() and out_path.stat().st_size > 0: # 已存在且非空,跳过,实际项目可加校验和比对 return str(out_path) client.download(product_id, str(out_path)) return str(out_path) except Exception as e: wait = 2 ** attempt # 指数退避 time.sleep(wait) raise RuntimeError(f"下载失败: {product_id}")函数先检查目标文件是否已存在且非空,满足就跳过,这是断点续传最简实现。重试次数设为 3,等待时间按 2 的幂次递增,避免密集重试加重限流。实际部署时可以把 product_id 换成带唯一标识的命名,防止不同产品覆盖同名文件。
3. 把脚本跑成定时任务:目录规划、日志与失败重试
3.1 目录结构决定后期能不能维护
影像下载不是一锤子买卖,按月按季度持续跑,目录乱了后期找数据会非常痛苦。我一般按「产品级别/年份/月份/产品ID」四级组织,产品ID 里自带日期和瓦片编号,天然可排序。下载临时文件放单独的 tmp 目录,校验通过后再移动到正式目录,避免半截文件混进数据集。日志单独放 logs 目录,按天切分,记录每次检索返回条数、成功下载数、失败列表,出问题先看日志而不是翻控制台。
# 目录初始化,放在脚本启动处执行 BASE_DIR=/data/sentinel2 mkdir -p $BASE_DIR/L2A/{2024/{03,04,05,06}} $BASE_DIR/tmp $BASE_DIR/logs这段 shell 在脚本开头跑一次即可,mkdir -p 保证已存在时不报错。年份月份按实际时间窗口调整,tmp 和 logs 与数据目录平级,方便清理和归档。
3.2 日志要记到什么粒度
日志不是越详细越好,但几个关键字段不能少:时间戳、检索条件摘要、返回产品数、每个产品的下载状态和耗时、异常堆栈。用 Python 标准库 logging 配置按天轮转,格式里带上 level 和 funcName,排查时能直接定位到函数。失败产品单独写一个 failed.txt,下次跑脚本先读这个文件重试,比全量重跑省时间。
import logging from logging.handlers import TimedRotatingFileHandler handler = TimedRotatingFileHandler( "logs/download.log", when="midnight", backupCount=30, encoding="utf-8" ) handler.setFormatter(logging.Formatter( "%(asctime)s %(levelname)s %(funcName)s %(message)s" )) logger = logging.getLogger("sentinel") logger.addHandler(handler) logger.setLevel(logging.INFO)TimedRotatingFileHandler 每天午夜切分日志,保留 30 天。backupCount 按磁盘空间调整,影像下载日志增长不快,30 天足够回溯。encoding 指定 utf-8 防止中文路径写日志报错。
3.3 失败重试与告警的边界
重试不是无限次的,网络抖动重试两三次能恢复,但如果是认证过期或产品已下架,重试一百次也没用。我的做法是区分异常类型:网络类异常重试,认证类异常直接刷新令牌后重试一次,产品不存在类异常记录后跳过。告警可以简单到往一个文本文件追加,也可以接邮件,但不要每失败一次就发一封,攒到一轮结束汇总发。
4. 避坑与排查:认证、云量、磁盘、命名四个高频翻车点
4.1 认证令牌过期导致批量下载中途全挂
现象:脚本前几景下载正常,跑到一半全部返回 401,日志里堆满认证失败。原因:令牌有有效期,长时间批量下载会中途失效,而脚本没有刷新机制。解决:在下载循环里捕获认证异常,触发令牌刷新后重试当前产品,而不是让整个任务崩掉。更稳的做法是每下载 N 景主动检查一次令牌剩余时间,提前刷新。
4.2 云量阈值设太低,检索结果为零
现象:检索条件看着没问题,但返回产品数为 0,反复检查范围和时间都对。原因:云量上限设成了 5% 甚至更低,而目标区域在目标时段恰好多云,没有产品满足。解决:先把云量上限放到 50% 看有没有结果,确认检索链路通了再逐步收紧。另外注意云量字段在不同产品级别里含义可能不同,L1C 的云量是粗略估计,L2A 的更准。
4.3 磁盘写满导致下载文件损坏
现象:下载看似成功,但打开影像发现文件截断,或者解压报错。原因:磁盘空间不足,写入过程中断,但脚本没有检查写入完整性。解决:下载前检查目标分区剩余空间,按单景影像大小乘以待下载数量估算,留出 20% 余量。下载后比对文件大小与接口返回的预期大小,不一致就标记为失败重下。
4.4 产品ID含特殊字符导致路径异常
现象:某些产品下载后文件名乱码,或者移动文件时报路径不存在。原因:产品ID里可能包含冒号、斜杠等文件系统敏感字符,直接拼路径会出问题。解决:对产品ID做一次安全替换,把敏感字符换成下划线,同时保留原始ID到日志里,方便追溯。命名规则一旦定下就不要中途改,否则历史数据和新增数据对不上。
5. 进阶技巧:用清单文件驱动增量下载与校验
跑到后期,你会发现每次全量检索再比对本地文件很浪费。更高效的方式是维护一份清单文件,记录已下载产品的ID、下载时间、文件大小和校验和。脚本启动时先读清单,检索结果里排除已在清单中的产品,只下载新增的。清单用 CSV 或 SQLite 都行,CSV 简单直观,SQLite 查询快,数据量上万景后 SQLite 优势明显。
import sqlite3 conn = sqlite3.connect("downloaded.db") conn.execute(""" CREATE TABLE IF NOT EXISTS products ( product_id TEXT PRIMARY KEY, download_time TEXT, file_size INTEGER, checksum TEXT ) """) def is_downloaded(product_id): cur = conn.execute( "SELECT 1 FROM products WHERE product_id = ?", (product_id,) ) return cur.fetchone() is not None建表时把 product_id 设为主键,天然去重。is_downloaded 查询返回单条记录,存在即跳过。checksum 字段可以存 MD5 或 SHA256,校验时重新计算比对,能发现文件被意外修改的情况。这套清单机制配合前面的断点续传,基本能做到脚本随便中断、重跑不重复不遗漏。
我自己的习惯是每次跑完脚本,手动翻一眼 failed.txt 和日志末尾,确认没有异常再关终端。这个动作花不了一分钟,但能避免第二天发现数据缺了一大块还得重新排查。希望帮到你。
本文还有配套的精品资源,点击获取