1. 这套方案到底在解决什么问题
哥白尼哨兵系列卫星数据(Sentinel)是目前公开遥感数据里更新最勤、覆盖最广的一批,尤其是 Sentinel-1 和 Sentinel-2,做地表变化检测、农作物长势监测、水体提取、城市扩张分析的人几乎都绕不开它。但真正下过数据的人都知道,最让人抓狂的不是处理算法,而是下载这一步。官方提供的下载渠道里,有一部分数据处于 offline 状态,也就是长期归档、不放在在线缓存里,你点下载它不会立刻给你,而是需要先提交一个“唤醒”请求,等后台把数据从磁带库搬回在线存储,通常要等几十分钟到几个小时不等,然后你才能拿到真正的下载链接。
sentinelsat这个 Python 包就是官方 API 的一个封装,它把搜索、查询、触发离线数据唤醒、获取下载链接这些动作都做成了函数调用,用起来比手点网页舒服得多。但它自带的下载函数是单线程的,速度慢、断点续传能力弱,遇到大文件(Sentinel-1 的 SLC 产品动辄 4~8 GB)经常下到一半就崩。而 IDM(Internet Download Manager)是 Windows 上老牌的多线程下载工具,支持分段并发、断点续传、失败自动重试,把这两者结合起来,就能实现“用 sentinelsat 负责搜索和唤醒,用 IDM 负责高速下载”的分工。
这套流程适合谁?适合需要批量获取历史存档 Sentinel 数据的研究生、遥感工程师、做时序分析的从业者,尤其是那些需要一次性拉几十上百景、又不想守着电脑一景一景点的人。下面我把整套思路、关键细节、踩过的坑全部拆开讲清楚。
2. 整体设计思路与方案选型
2.1 为什么是 sentinelsat 而不是纯网页操作
纯网页操作的问题在于:第一,搜索条件复杂时(比如按云量、按轨道号、按时间窗口、按相对轨道号组合筛选),网页端每次都要重新点一遍;第二,offline 数据的唤醒状态没法批量管理,你得一个个点进去看它是否已经 online;第三,拿到下载链接后还要手动复制到下载工具里,几十景就是几十次重复劳动。
sentinelsat把这些都 API 化了。它的核心对象是SentinelAPI,初始化时传入账号密码,然后query()方法接受一系列过滤参数,返回一个字典,key 是产品 UUID,value 是产品元信息。元信息里包含title、size、online(是否在线)、uuid等字段。最关键的是download()和download_all()这两个方法,以及trigger_offline_retrieval()这个专门用来唤醒离线数据的接口。
注意:sentinelsat 的
download()内部其实也是走 HTTP 流式下载,它支持断点续传(通过continue参数),但并发能力有限,单连接速度受服务器限速影响明显。这就是为什么我们要把下载环节交给 IDM。
2.2 为什么用 IDM 而不是 aria2 或 wget
aria2 和 wget 当然也能多线程,但 IDM 在 Windows 桌面环境下的优势是:图形界面直观、支持剪贴板监控(复制链接自动弹出下载框)、分段策略成熟、对不稳定网络的容错做得好。更重要的是,IDM 可以直接导入一个包含多条 URL 的文本文件,批量排队下载,这对我们“先批量拿到链接、再统一丢给下载器”的流程非常契合。
如果你在 Linux 服务器上跑,aria2 是更自然的选择,但本文以 Windows + IDM 为主线,因为这是提问者标题里明确点出的组合。Linux 用户可以把 IDM 替换成 aria2c,逻辑完全一样。
2.3 整体流程拆解
整套流程分四步:
- 搜索:用 sentinelsat 按条件查询产品,拿到 UUID 列表和元信息。
- 唤醒:筛选出
online=False的产品,调用trigger_offline_retrieval()提交唤醒请求。 - 轮询:定期检查这些产品是否已经变为 online,拿到最终下载链接。
- 下载:把链接写入文本文件,导入 IDM 批量下载。
这个分工的核心逻辑是:sentinelsat 负责“跟官方服务器打交道”,IDM 负责“跟文件传输打交道”。两者通过一个中间文件(URL 列表)解耦,互不干扰。
3. 核心细节解析与实操要点
3.1 sentinelsat 的安装与账号准备
安装很简单:
pip install sentinelsat但有几个细节要注意。sentinelsat 依赖geopandas和pandas,如果你用的是较新的 Python 版本(3.10+),建议先建虚拟环境,避免和系统里的科学计算包冲突。我实测下来,用 conda 建一个干净环境最稳:
conda create -n sentinel python=3.9 conda activate sentinel pip install sentinelsat账号方面,你需要一个哥白尼数据空间(Copernicus Data Space Ecosystem)的账号。注意,早期很多人用的是 SciHub 账号,但 SciHub 已经退役,现在统一走 Copernicus Data Space。sentinelsat 较新版本(1.1.0+)已经支持新的端点,初始化时不需要额外指定 URL,默认就是新端点。如果你用的是老版本,可能会连不上,建议直接升级到最新版。
from sentinelsat import SentinelAPI, read_geojson, geojson_to_wkt api = SentinelAPI('你的用户名', '你的密码')提示:不要把账号密码硬编码在脚本里然后传到公开仓库。用环境变量或者单独的配置文件读取,这是基本的安全习惯。
3.2 查询条件的构造技巧
query()方法的参数很多,常用的有:
platformname:'Sentinel-1'或'Sentinel-2'producttype:S2 常用'S2MSI1C'(L1C)或'S2MSI2A'(L2A),S1 常用'GRD'或'SLC'date:格式('20230101', '20230301'),注意是字符串元组cloudcover:S2 专用,格式(0, 20)表示云量 0~20%area:用 WKT 或 geojson 描述的空间范围relativeorbitnumber:相对轨道号,做时序分析时非常有用
一个典型的查询:
products = api.query( area=footprint, date=('20230101', '20230601'), platformname='Sentinel-2', producttype='S2MSI2A', cloudcover=(0, 15) )这里有个坑:area参数如果传的是一个很大的多边形,查询会返回大量结果,而且服务器端可能超时。我的经验是,尽量用相对轨道号 + 小范围 AOI 组合,把结果控制在几百景以内。如果确实要查大范围,分批次按月份或按轨道号查,别一次性拉。
3.3 判断 online 状态与触发唤醒
查询返回的products是一个字典,每个产品的元信息里有一个online字段。但要注意,这个字段在查询结果里可能不是实时准确的,有时候显示 online 但实际下载时才发现是 offline。更可靠的做法是调用api.is_online(uuid)逐个确认,或者直接尝试获取下载链接,看返回状态。
唤醒离线数据的接口是:
api.trigger_offline_retrieval(uuid)这个调用会向服务器提交一个唤醒请求。关键点来了:唤醒请求不是无限次提交的,官方对每个账号有配额限制。如果你一次性对几百景提交唤醒,可能会触发限流,导致部分请求失败。我的做法是分批提交,每批 20~30 景,中间 sleep 几秒。
import time offline_uuids = [uuid for uuid, info in products.items() if not info['online']] for i in range(0, len(offline_uuids), 25): batch = offline_uuids[i:i+25] for uuid in batch: try: api.trigger_offline_retrieval(uuid) except Exception as e: print(f'{uuid} 唤醒失败: {e}') time.sleep(5)注意:唤醒请求提交后,数据不会立刻变 online。官方文档说通常几十分钟到几小时,但实测下来,热门区域的数据可能几分钟就好了,冷门区域或者老数据可能要等半天甚至更久。所以轮询是必须的。
3.4 轮询状态与获取下载链接
轮询的逻辑是:每隔一段时间检查一次之前提交唤醒的产品,看online是否变为 True。一旦变为 True,就可以调用api.get_product_odata(uuid)拿到下载链接,或者直接用api.download(uuid)下载。
但我们要的是链接,不是直接下载。sentinelsat 提供了一个方法可以拿到产品的下载 URL:
odata = api.get_product_odata(uuid) download_url = odata['Download']['value']不过这个 URL 是带 token 的,有时效性。所以拿到链接后要尽快丢给 IDM 开始下载,别攒着。我的做法是:轮询到 online 后,立刻把链接追加写入一个urls.txt文件,IDM 那边设置成监控这个文件或者定时导入。
轮询脚本可以这样写:
import time pending = set(offline_uuids) while pending: for uuid in list(pending): odata = api.get_product_odata(uuid) if odata['Online']: url = odata['Download']['value'] with open('urls.txt', 'a') as f: f.write(url + '\n') pending.remove(uuid) print(f'{uuid} 已就绪,链接已写入') if pending: print(f'还剩 {len(pending)} 景等待中,60秒后重试') time.sleep(60)这里有个细节:get_product_odata返回的字段名可能是Online也可能是online,取决于 sentinelsat 版本。建议先打印一次看看实际结构,别照抄。
4. 实操过程与核心环节实现
4.1 完整脚本的搭建
把上面的片段串起来,形成一个完整的脚本。我习惯分成三个文件:config.py放账号和查询参数,search_and_trigger.py负责搜索和唤醒,poll_and_export.py负责轮询和导出链接。这样分工的好处是,搜索和唤醒可以跑一次就停,轮询可以挂着长期跑,互不影响。
config.py:
USERNAME = 'your_username' PASSWORD = 'your_password' AOI_GEOJSON = 'aoi.geojson' DATE_RANGE = ('20230101', '20230601') PLATFORM = 'Sentinel-2' PRODUCT_TYPE = 'S2MSI2A' CLOUD_COVER = (0, 15)search_and_trigger.py:
from sentinelsat import SentinelAPI, read_geojson, geojson_to_wkt import config import time api = SentinelAPI(config.USERNAME, config.PASSWORD) footprint = geojson_to_wkt(read_geojson(config.AOI_GEOJSON)) products = api.query( area=footprint, date=config.DATE_RANGE, platformname=config.PLATFORM, producttype=config.PRODUCT_TYPE, cloudcover=config.CLOUD_COVER ) print(f'共查询到 {len(products)} 景') offline = [u for u, i in products.items() if not i['online']] print(f'其中 {len(offline)} 景为离线状态,开始唤醒') for i in range(0, len(offline), 25): for uuid in offline[i:i+25]: try: api.trigger_offline_retrieval(uuid) except Exception as e: print(f'{uuid}: {e}') time.sleep(5) with open('pending_uuids.txt', 'w') as f: for uuid in offline: f.write(uuid + '\n')poll_and_export.py:
from sentinelsat import SentinelAPI import config import time api = SentinelAPI(config.USERNAME, config.PASSWORD) with open('pending_uuids.txt') as f: pending = set(line.strip() for line in f if line.strip()) while pending: for uuid in list(pending): try: odata = api.get_product_odata(uuid) if odata.get('Online') or odata.get('online'): url = odata['Download']['value'] with open('urls.txt', 'a') as f: f.write(url + '\n') pending.remove(uuid) print(f'就绪: {uuid}') except Exception as e: print(f'检查 {uuid} 出错: {e}') if pending: print(f'剩余 {len(pending)} 景,等待中...') time.sleep(120)4.2 IDM 的批量导入配置
拿到urls.txt后,打开 IDM,选择“任务”->“从文件导入”,选中这个文本文件,IDM 会自动把所有链接加入下载队列。这里有几个设置要调:
- 连接数:默认是 8,可以调到 16,但不要太高,官方服务器对单 IP 的并发连接有限制,调太高反而容易被限速。
- 重试次数:设成 5~10 次,网络抖动时自动重试。
- 保存路径:按产品类型分文件夹,比如
S2MSI2A/、S1GRD/,方便后续处理。 - UA 设置:IDM 默认的 User-Agent 有时会被服务器拒绝,可以在“选项”->“下载”里自定义 UA,模仿浏览器。
提示:IDM 导入的链接如果带 token,token 过期后下载会失败。所以导入后尽快开始下载,别让队列挂太久。如果发现大量 403 错误,说明 token 过期了,需要重新跑轮询脚本生成新链接。
4.3 参数计算与选择过程
这里重点说两个参数的选择逻辑。
并发连接数:官方服务器对单个账号的并发下载数没有明确文档,但实测下来,同时下载 3~5 个文件、每个文件 8~16 连接是比较稳的。如果你一次性把 100 个链接全丢进去让 IDM 同时下,大概率会被限速甚至临时封禁。我的做法是:IDM 队列里同时活跃的任务不超过 5 个,其余排队。
轮询间隔:太短会浪费请求配额,太长会延迟拿到链接。我的经验是,唤醒提交后的前 30 分钟,每 2 分钟查一次;30 分钟到 2 小时,每 10 分钟查一次;2 小时以上,每 30 分钟查一次。这样既能及时拿到,又不会过度请求。
def get_interval(elapsed_minutes): if elapsed_minutes < 30: return 120 elif elapsed_minutes < 120: return 600 else: return 18004.4 实操现场记录
我最近一次跑这套流程,是拉一批 2022 年春季的 Sentinel-2 L2A 数据,AOI 是某农业区,时间窗口 3 个月,云量 15% 以下,共查到 87 景。其中 62 景是 online,25 景是 offline。唤醒提交后,最快的 8 分钟就变 online 了,最慢的一景等了将近 4 小时。62 景在线数据直接用 sentinelsat 的download_all下了,25 景离线数据走唤醒 + IDM 流程。IDM 那边 5 个并发任务,平均每个文件 800 MB 左右,总耗时约 2 小时 40 分钟,比纯 sentinelsat 单线程快了大概 4 倍。
5. 常见问题与排查技巧实录
5.1 唤醒请求提交失败
最常见的报错是HTTP 429 Too Many Requests,说明你提交太快了。解决办法就是分批 + sleep,前面已经说了。还有一种情况是Invalid UUID,通常是因为你传的 UUID 格式不对,或者这个产品已经被删除。检查一下 UUID 是不是从products字典的 key 里直接取的,别手动拼。
5.2 轮询一直显示 offline
如果一景数据唤醒后超过 24 小时还是 offline,可能是唤醒请求没真正生效。可以登录 Copernicus Data Space 网页端,找到这个产品看看它的状态。有时候网页端会显示“正在检索中”,这种情况只能等。如果网页端显示的还是“离线”且没有检索任务,说明你的唤醒请求丢了,重新提交一次。
5.3 IDM 下载报 403
403 基本就是 token 过期。sentinelsat 拿到的下载链接里带一个token参数,有效期通常几小时。解决办法是重新跑轮询脚本,生成新链接。为了避免频繁过期,建议轮询到 online 后立刻下载,别攒。
5.4 下载速度忽快忽慢
这通常是服务器端限速,不是你的问题。可以尝试降低并发连接数,有时候连接数少反而更稳。另外,避开欧洲时间的白天(北京时间下午到晚上),那时候服务器负载高,速度会慢一些。北京时间凌晨到上午,速度明显更快。
5.5 常见问题速查表
| 问题现象 | 可能原因 | 解决办法 |
|---|---|---|
| 429 Too Many Requests | 唤醒提交过快 | 分批提交,每批 25 景,间隔 5 秒 |
| 轮询一直 offline | 唤醒请求丢失 | 网页端确认状态,重新提交 |
| IDM 403 | token 过期 | 重新轮询生成新链接 |
| 下载速度慢 | 服务器限速 | 降低并发,避开高峰时段 |
| 查询超时 | AOI 太大或时间跨度太长 | 分批次查询,加轨道号过滤 |
| 脚本报 KeyError | 字段名版本差异 | 打印 odata 结构确认字段名 |
5.6 独家避坑技巧
第一个技巧:把 UUID 和产品标题的映射存下来。轮询脚本里只处理 UUID,但下载下来的文件名是产品标题,后期对不上号很麻烦。建议在搜索阶段就存一个uuid -> title的 JSON 文件,后面处理数据时直接查。
第二个技巧:IDM 的临时文件别乱删。IDM 下载大文件时会生成.tmp文件,断点续传靠的就是它。如果你中途手动清理了临时文件,续传就失效了,得从头下。
第三个技巧:用requests先验证链接可用性。在把链接写入urls.txt之前,可以用requests.head(url)快速检查一下返回码,200 才写入,避免把失效链接丢给 IDM。
import requests def check_url(url): try: r = requests.head(url, timeout=10) return r.status_code == 200 except: return False第四个技巧:日志要记全。每次唤醒、每次轮询、每次链接生成,都写进日志文件。出问题的时候,日志是你唯一的线索。我习惯用 Python 的logging模块,按天切分日志文件。
这套流程我前后跑了大概半年,处理过几千景数据,最大的体会是:搜索和唤醒要稳,下载要快,两者解耦是关键。sentinelsat 的 API 调用要克制,别把配额浪费在无意义的轮询上;IDM 的队列要控制,别贪多导致被封。把这两点做好,批量下载离线 Sentinel 数据就不再是件折磨人的事。