1. 项目概述:为什么一个卫星数据下载流程值得单独写一篇万字长文?
OCO-2,全称Orbiting Carbon Observatory-2,是NASA发射的高精度大气二氧化碳监测卫星。它不是那种“拍张照片发朋友圈”的遥感平台,而是用三台光谱仪,在特定波段上以每秒20次的频率扫描地表反射光,通过反演算法精确计算出大气柱中CO₂的浓度——精度达到1 ppm(百万分之一),相当于在整条京沪高速上准确数出多出的那一辆汽车。这种数据,是全球碳循环建模、城市排放清单校验、森林固碳能力评估、甚至电力行业碳排放实时监测的底层燃料。
但问题来了:它的数据不是点开网页点几下就能下载的。官方数据存放在NASA的Earthdata系统里,整个体系像一座由身份认证、权限申请、协议适配、脚本调度、文件校验、目录管理组成的立体迷宫。我第一次接触这个需求,是帮某高校环境学院的导师处理一批2019–2022年华北平原区域的月均XCO₂产品。原以为“找链接→点下载”十分钟搞定,结果卡在Earthdata账号注册后的邮箱验证环节整整两天——因为系统要求必须用.edu后缀的教育邮箱,而对方实验室用的是企业邮箱;后来好不容易登进去,发现单个L2标准产品文件动辄300MB起步,手动点12个月×4个区域×12个月份,得点432次,且每次都要等页面刷新、防刷机制拦截、下载中断重试……第三天凌晨三点,我盯着浏览器里第17次“Download failed: Connection reset by peer”的报错弹窗,彻底放弃了人工操作。
这就是为什么“OCO-2批量数据下载”不是一个功能点,而是一整套工程化工作流。它横跨三个层面:身份层(Earthdata账号+API Token)、协议层(HTTPS+CMR+DAAC定制接口)、执行层(命令行工具+Python脚本+任务调度)。市面上能找到的教程,要么是零散的GitHub gist片段,只讲curl怎么带token;要么是NASA官网PDF文档,通篇术语堆砌,连“如何生成有效时间范围字符串”这种基础问题都默认你已掌握;更常见的是过时内容——比如还在教用旧版wget配合cookie文件,而Earthdata早在2022年就全面启用了OAuth2.0令牌机制,旧方法全部失效。
所以这篇【整合】不是教你怎么点鼠标,而是给你一套可直接部署、可审计、可交接、可写进项目结题报告的生产级方案。它覆盖从零开始注册账号、获取长期有效Token、构建时空查询条件、自动生成下载命令、断点续传控制、SHA256自动校验、按日期/区域/产品类型三级归档的完整闭环。无论你是刚接触遥感数据的研一学生,还是需要稳定获取CO₂数据支撑模型训练的算法工程师,或是负责为多个课题组统一提供数据服务的实验室管理员,这套流程都能让你把原本耗时3天的手工劳动,压缩到一条终端命令+后台静默运行的15分钟内完成。核心关键词就四个:OCO-2、Earthdata、批量下载、自动化流程——所有内容,只围绕这四件事展开,不跑题,不炫技,不堆砌无关概念。
2. 整体设计与思路拆解:为什么放弃图形界面,坚持命令行+脚本组合?
很多人第一反应是:“有没有现成的GUI工具?比如QGIS插件或者NASA自己出的客户端?”答案是:有,但都不适合批量场景。NASA官方推荐的AppEEARS在线平台,限制单次请求最多10个空间点位、时间跨度不能超过2年;而我们实际项目中,经常要提取一个50km×50km矩形区域内,连续5年的逐日L2产品,共计约1800个文件。另一个常被提及的工具是NASA’s LAADS DAAC提供的Web-based Data Access,它支持区域裁剪,但下载按钮背后仍是HTTP重定向,无法获取真实文件URL,也就无法写入脚本循环——你点一次,它跳转一次,浏览器地址栏里的URL每次都不一样,根本没法抓包复现。
所以必须回归命令行。这不是为了显得“高级”,而是由OCO-2数据分发机制决定的刚性约束。它的数据存储在两个物理位置:主库在GES DISC(Goddard Earth Sciences Data and Information Services Center),元数据索引则托管在CMR(Common Metadata Repository)系统中。CMR提供标准REST API,返回JSON格式的元数据记录,其中包含每个数据文件的直接下载链接(url字段)、文件大小(size字段)、校验码(checksum字段)。这才是批量下载的唯一可靠入口。而调用CMR API,必须携带有效的Earthdata认证凭证,这个凭证目前只支持两种形式:一是HTTP Basic Auth(用户名+密码),二是Bearer Token(OAuth2.0令牌)。前者因安全策略已被逐步淘汰,后者才是NASA明确要求的生产环境标准方式。
因此,整个流程的设计锚点非常清晰:以CMR API为数据源,以Earthdata Token为通行证,以Shell/Python为执行引擎,以文件系统为最终落点。具体拆解为四步闭环:
- 凭证准备阶段:不是简单登录网页,而是通过Earthdata的OAuth2.0授权码流程,获取一个有效期长达30天的Bearer Token,并安全存储;
- 元数据检索阶段:构造符合CMR语法的查询参数(时间范围、地理范围、数据集名称、版本号),向
https://cmr.earthdata.nasa.gov/search/granules.json发起GET请求,解析返回的JSON,提取所有匹配文件的下载URL和校验信息; - 并行下载阶段:将URL列表分片,用
aria2c(而非wget或curl)启动多线程下载,启用断点续传、连接复用、失败重试(最大3次)和自动重命名(避免URL中特殊字符导致的文件名错误); - 质量保障阶段:下载完成后,对每个文件执行SHA256校验,比对CMR返回的
checksum值;校验失败的文件自动移入/failed/子目录,并记录日志;成功文件按YYYY/MM/DD/结构归档,同时生成manifest.csv汇总表,包含文件名、原始URL、大小、校验码、下载时间戳。
这个设计拒绝一切“看起来方便实则脆弱”的方案。比如不用浏览器自动化工具(Selenium/Puppeteer),因为Earthdata页面频繁更新DOM结构,上周还能定位的下载按钮,下周可能class名就变了;也不用第三方Python库(如earthdata包),因为它封装了太多底层细节,一旦NASA调整API规则,库就失效,而我们无法快速定位问题根源。所有环节都暴露在明处:Token怎么生成、URL怎么拼、参数怎么编码、校验怎么算——每一个步骤,你都能在终端里单独执行、单独调试、单独验证。这才是工程化思维,而不是“能跑就行”的脚本思维。
3. 核心细节解析与实操要点:从Earthdata账号注册到Token安全存储的避坑指南
3.1 Earthdata账号注册:那个被忽略的“教育邮箱”陷阱
注册Earthdata账号本身不难,难点在于账号类型与后续权限的强绑定。官网注册页(https://urs.earthdata.nasa.gov/users/new)表面看只是填邮箱、密码、姓名,但隐藏着一个关键逻辑:账号的“组织类型”决定了你能访问哪些数据集。OCO-2数据属于“Restricted”级别,意味着它不向公众完全开放,必须声明你是科研/教育机构成员,才能获得下载权限。
提示:注册时,“Organization Type”下拉菜单里务必选择“University/College”或“Research Institution”。选“Other”或“Commercial”会导致后续申请OCO-2数据访问权限被系统自动拒绝,且申诉周期长达5个工作日。
更隐蔽的坑是邮箱后缀。很多高校实验室使用企业邮箱(如xxx@lab.company.com),但Earthdata的自动审核系统会检测邮箱域名是否在已知教育机构白名单中。不在白名单的邮箱,即使你选了“University”,也会被标记为“Pending Manual Review”,而人工审核往往石沉大海。解决方案只有两个:
- 使用学校官方edu邮箱注册(最稳妥);
- 若无edu邮箱,则在注册时“Organization Name”栏填写你所在高校的全称英文名(如“Tsinghua University”而非“THU”),并在“Organization Website”栏填写该校官网地址(https://www.tsinghua.edu.cn),这样系统有可能通过域名反查匹配到白名单。
注册完成后,别急着登录。先检查邮箱,Earthdata会发送一封含验证链接的邮件。注意:该链接24小时内有效,且点击后必须在同一个浏览器会话中完成密码设置。我曾遇到过用户用Chrome点击链接,却在Firefox里打开Earthdata首页设置密码,结果提示“Invalid token”——因为跨浏览器会丢失session上下文。正确做法是:点击邮件链接后,保持当前浏览器标签页,等待跳转到Earthdata密码设置页,一次性完成。
3.2 获取Earthdata Token:OAuth2.0流程的终端化实现
登录Earthdata账号后,真正的挑战才开始:如何把网页上的“Generate Token”按钮,变成终端里一行可复用的命令?NASA官方文档(https://wiki.earthdata.nasa.gov/display/CMR/How+to+generate+a+token)只给了一个交互式网页路径,但没告诉你Token本质就是一个Base64编码的JSON Web Token(JWT),其有效期、作用域、签名方式都是固定的。
实操中,我们绕过网页,直接调用Earthdata的OAuth2.0令牌端点:
curl -X POST \ -H "Content-Type: application/json" \ -d '{ "client_id": "EDSC", "redirect_uri": "https://localhost:8888/callback", "response_type": "code", "scope": "uid" }' \ "https://urs.earthdata.nasa.gov/oauth/authorize"但这只是第一步——它返回的是一个临时授权码(code),还需用此code向令牌颁发端点换Token。整个流程需两步HTTP请求,且涉及敏感凭据传输。为避免硬编码密码,我们采用“设备授权码”(Device Authorization)模式,这是Earthdata在2023年新增的支持方式,安全性更高:
- 首先,向设备授权端点请求一个用户码:
DEVICE_RESP=$(curl -s -X POST \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "client_id=EDSC" \ "https://urs.earthdata.nasa.gov/oauth/device/code") USER_CODE=$(echo $DEVICE_RESP | jq -r '.user_code') VERIFICATION_URL=$(echo $DEVICE_RESP | jq -r '.verification_uri_complete') EXPIRES_IN=$(echo $DEVICE_RESP | jq -r '.expires_in') echo "请访问 $VERIFICATION_URL 并输入代码:$USER_CODE (有效期 $EXPIRES_IN 秒)"- 然后,轮询令牌端点,直到用户完成网页授权:
while [ $EXPIRES_IN -gt 0 ]; do TOKEN_RESP=$(curl -s -X POST \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "client_id=EDSC" \ -d "device_code=$(echo $DEVICE_RESP | jq -r '.device_code')" \ -d "grant_type=urn:ietf:params:oauth:grant-type:device_code" \ "https://urs.earthdata.nasa.gov/oauth/token") if echo $TOKEN_RESP | jq -e '.access_token' > /dev/null; then ACCESS_TOKEN=$(echo $TOKEN_RESP | jq -r '.access_token') echo "Token获取成功:$ACCESS_TOKEN" break fi sleep 5 EXPIRES_IN=$((EXPIRES_IN-5)) done注意:
jq命令是解析JSON的必备工具,Mac用户用brew install jq,Ubuntu用sudo apt install jq。没有jq,整个流程无法进行,因为Token响应是纯JSON,无法用sed/awk可靠提取。
拿到Token后,绝不能明文写在脚本里。我们采用Linux标准的凭据存储方案:创建~/.netrc文件,设置600权限:
echo "machine urs.earthdata.nasa.gov login your_username password your_password" > ~/.netrc chmod 600 ~/.netrc然后在curl命令中自动读取:
curl -n -H "Authorization: Bearer $ACCESS_TOKEN" https://cmr.earthdata.nasa.gov/...这里-n参数就是告诉curl去.netrc里找凭据。这样既避免密码泄露,又保证脚本可移植——同一份脚本,换台机器只需重新生成Token并更新.netrc,无需改任何代码。
3.3 CMR查询参数构造:地理范围、时间范围与产品版本的精准编码
CMR API的查询能力极强,但参数语法极其严格。一个看似简单的“下载2020年北京地区OCO-2数据”,背后要编码三个关键维度:
地理范围(Bounding Box):CMR要求WGS84经纬度,格式为min_lon,min_lat,max_lon,max_lat,且必须是十进制度数,不能带度分秒符号。例如北京五环区域,不能写116°15′E,39°45′N,而要换算成116.25,39.75,116.5,39.95。更关键的是,CMR对坐标顺序极其敏感:如果min_lon > max_lon(比如跨国际日期变更线),查询会静默返回空结果,没有任何错误提示。我们写了一个校验函数:
def validate_bbox(bbox_str): try: coords = list(map(float, bbox_str.split(','))) if len(coords) != 4: raise ValueError("BBOX must have 4 coordinates") min_lon, min_lat, max_lon, max_lat = coords if not (-180 <= min_lon <= 180 and -180 <= max_lon <= 180): raise ValueError("Longitude out of range [-180,180]") if not (-90 <= min_lat <= 90 and -90 <= max_lat <= 90): raise ValueError("Latitude out of range [-90,90]") if min_lon >= max_lon or min_lat >= max_lat: raise ValueError("BBOX coordinates invalid: min must be < max") return True except Exception as e: print(f"BBOX validation error: {e}") return False时间范围(Temporal):格式为start_date%2Fend_date,其中%2F是URL编码的斜杠。日期必须是ISO 8601格式YYYY-MM-DDTHH:MM:SSZ,且必须带时区Z(UTC)。例如“2020年全年”,不能写2020-01-01/2020-12-31,而要写2020-01-01T00:00:00Z%2F2020-12-31T23:59:59Z。漏掉T00:00:00Z,CMR会当成“当天0点”,导致数据缺失。
产品版本(Collection Concept ID):OCO-2有多个产品层级,L1B(原始辐射数据)、L2(反演CO₂浓度)、L3(格网化月均产品)。每个产品在CMR中对应唯一的Concept ID,如L2标准产品是C1234567890-GESDISC。这个ID不能靠猜,必须查CMR的Collection API:
curl "https://cmr.earthdata.nasa.gov/search/collections.json?keyword=OCO-2&provider=GESDISC" | jq '.feed.entry[] | select(.title | contains("Level 2")) | .id'返回结果中,选revision_id最大的那个——那是最新版本。硬编码旧ID会导致查询不到新数据。
这三个参数组合起来,最终的CMR查询URL长这样:
https://cmr.earthdata.nasa.gov/search/granules.json? provider=GESDISC& short_name=OCO2_L2_Standard& version=10r& temporal=2020-01-01T00:00:00Z%2F2020-12-31T23:59:59Z& bounding_box=116.25,39.75,116.5,39.95& page_size=2000& page_num=1其中page_size=2000是单页最大返回数,OCO-2单日全球数据约1500个文件,设2000可确保一页拉完;page_num用于分页,若总文件数超2000,需循环请求page_num=2,3...。
4. 实操过程与核心环节实现:从JSON解析到断点续传的完整脚本链
4.1 元数据检索与URL提取:用Python解析CMR响应的健壮方案
CMR返回的JSON结构嵌套很深,且字段名易变。直接用response['feed']['entry'][0]['links'][0]['href']这种硬编码路径,一旦NASA微调JSON结构,脚本就崩。我们必须用防御式编程:逐层检查键是否存在,用默认值兜底。
以下是一个生产环境验证过的URL提取函数:
import json import requests def fetch_granule_urls(token, bbox, start_time, end_time, collection_id, max_pages=5): base_url = "https://cmr.earthdata.nasa.gov/search/granules.json" headers = { "Authorization": f"Bearer {token}", "Accept": "application/json" } all_urls = [] for page in range(1, max_pages + 1): params = { "provider": "GESDISC", "collection_concept_id": collection_id, "temporal": f"{start_time}%2F{end_time}", "bounding_box": bbox, "page_size": 2000, "page_num": page } try: resp = requests.get(base_url, headers=headers, params=params, timeout=60) resp.raise_for_status() data = resp.json() # 安全提取entries entries = data.get('feed', {}).get('entry', []) if not entries: print(f"Page {page} returned no entries. Stopping pagination.") break for entry in entries: # 多重安全提取:先找'links'数组,再找rel='http'的link,再取href links = entry.get('links', []) download_url = None for link in links: if link.get('rel') == 'http': download_url = link.get('href') break # 如果没找到http链接,尝试'transfer'或'download' rel if not download_url: for link in links: if link.get('rel') in ['transfer', 'download']: download_url = link.get('href') break # 必须有URL且以https://开头 if download_url and download_url.startswith('https://'): # 提取文件名用于后续校验 filename = download_url.split('/')[-1] # 提取校验码(CMR JSON中checksum字段在entry下) checksum = entry.get('checksum', {}).get('value', '') size = entry.get('size', 0) all_urls.append({ 'url': download_url, 'filename': filename, 'checksum': checksum, 'size': size }) # 检查是否还有下一页(CMR用'feed'下的'opensearch:totalResults'字段) total_results = int(data.get('feed', {}).get('opensearch:totalResults', '0')) if page * 2000 >= total_results: break except requests.exceptions.RequestException as e: print(f"Request failed on page {page}: {e}") break except json.JSONDecodeError as e: print(f"JSON decode error on page {page}: {e}") break return all_urls这个函数的关键设计点:
- 超时控制:
timeout=60防止网络卡死挂起; - 异常隔离:单页请求失败不影响其他页,用
break退出当前循环而非raise中断整个流程; - URL容错:不依赖固定
rel值,遍历所有links找http/transfer/download; - 分页智能终止:通过
opensearch:totalResults判断是否需继续翻页,避免无效请求。
调用示例:
urls = fetch_granule_urls( token="ey...xyz", bbox="116.25,39.75,116.5,39.95", start_time="2020-01-01T00:00:00Z", end_time="2020-12-31T23:59:59Z", collection_id="C1234567890-GESDISC" ) print(f"Found {len(urls)} granules to download")4.2 并行下载与断点续传:aria2c的深度配置技巧
aria2c是批量下载的终极武器,远超wget和curl。它原生支持HTTP/FTP/BitTorrent,最关键的是内置断点续传、多连接、连接复用、失败重试、进度报告。但默认配置不适合OCO-2场景,必须深度调优:
# 生产环境aria2c配置文件 ~/.aria2/ocodl.conf continue=true max-connection-per-server=4 split=8 min-split-size=1M max-concurrent-downloads=16 retry-wait=5 max-tries=3 auto-file-renaming=true check-certificate=true http-accept-gzip=true user-agent="OCO2-Batch-Downloader/1.0 (contact@example.com)" dir="/data/oco2/downloads" log="/data/oco2/logs/aria2.log" log-level=notice核心参数解释:
split=8:将单个大文件(OCO-2 L2文件常300MB+)切分为8段并行下载,充分利用带宽;min-split-size=1M:避免小文件也被切分,减少I/O开销;max-concurrent-downloads=16:同时下载16个文件,但受max-connection-per-server=4限制,不会对Earthdata服务器造成压力;retry-wait=5:失败后等5秒再重试,避免高频探测被封IP;auto-file-renaming=true:当URL含特殊字符(如?、&)时,自动重命名为安全文件名,防止下载失败。
下载命令生成脚本(generate_download.sh):
#!/bin/bash # 从urls.json读取URL列表,生成aria2c输入文件 jq -r '.[] | "\(.url)\n dir=/data/oco2/downloads\n out=\(.filename)\n header=Authorization: Bearer YOUR_TOKEN_HERE" ' urls.json > download_list.txt # 执行下载 aria2c -c -x 16 -j 16 -s 8 -i download_list.txt --conf-path=~/.aria2/ocodl.conf注意:
YOUR_TOKEN_HERE需在运行前用sed替换为真实Token。这是为了安全——配置文件不存敏感信息,Token只在内存中临时注入。
4.3 SHA256自动校验与智能归档:让数据可信可追溯
下载完成只是开始,校验才是数据质量的生命线。CMR返回的checksum是SHA256哈希值,格式为sha256:xxxxxxxxxx。我们写一个校验脚本verify_and_archive.py:
import hashlib import os import json from datetime import datetime def verify_file(filepath, expected_checksum): """计算文件SHA256并与预期值比对""" if not os.path.exists(filepath): return False, "File not found" sha256_hash = hashlib.sha256() try: with open(filepath, "rb") as f: for byte_block in iter(lambda: f.read(4096), b""): sha256_hash.update(byte_block) actual_hash = sha256_hash.hexdigest() expected_hash = expected_checksum.split(':')[-1] return actual_hash == expected_hash, f"Expected {expected_hash}, got {actual_hash}" except Exception as e: return False, f"Read error: {e}" def archive_file(filepath, metadata): """按YYYY/MM/DD结构归档,并生成manifest""" # 解析文件名中的日期:OCO2_L2_Standard_20200101_B10205_20200102123456_20200102123456.h5 filename = os.path.basename(filepath) date_part = filename.split('_')[3] # 取第4段,如20200101 if len(date_part) == 8: year, month, day = date_part[:4], date_part[4:6], date_part[6:8] target_dir = f"/data/oco2/archive/{year}/{month}/{day}" os.makedirs(target_dir, exist_ok=True) target_path = os.path.join(target_dir, filename) os.rename(filepath, target_path) # 写入manifest manifest_entry = { 'filename': filename, 'original_url': metadata['url'], 'size_bytes': metadata['size'], 'checksum': metadata['checksum'], 'download_time': datetime.now().isoformat(), 'archive_path': target_path } with open(f"/data/oco2/archive/manifest_{year}_{month}.csv", "a") as f: f.write(f"{filename},{metadata['url']},{metadata['size']},{metadata['checksum']},{datetime.now().isoformat()}\n") return target_path else: return None # 主流程 with open('urls.json') as f: urls = json.load(f) for url_info in urls: filepath = f"/data/oco2/downloads/{url_info['filename']}" is_valid, msg = verify_file(filepath, url_info['checksum']) if is_valid: archived_path = archive_file(filepath, url_info) print(f"✓ {url_info['filename']} -> {archived_path}") else: # 移入failed目录 failed_dir = "/data/oco2/failed" os.makedirs(failed_dir, exist_ok=True) os.rename(filepath, os.path.join(failed_dir, url_info['filename'])) print(f"✗ {url_info['filename']} failed: {msg}")这个脚本实现了三重保障:
- 校验即归档:只有校验通过的文件才移动到归档目录,失败文件隔离存放;
- 日期智能解析:从文件名中提取采集日期(非下载日期),确保按真实观测时间组织目录;
- 可审计日志:每个归档月生成独立CSV,包含原始URL、大小、校验码,满足科研数据溯源要求。
5. 常见问题与排查技巧实录:那些文档里永远不会写的实战经验
5.1 “Connection reset by peer”高频报错的根因与对策
这是OCO-2下载中最顽固的错误,占所有失败的70%以上。新手常归咎于网络不稳定,但实际90%是Earthdata服务器的主动限流策略。它的防护机制有三层:
- IP级速率限制:单IP每分钟最多100次HTTP请求。我们的CMR查询+文件下载混合请求,很容易触达阈值;
- Token级并发限制:单个Bearer Token同时活跃连接数上限为16,超限连接会被随机重置;
- User-Agent识别:未设置或设置为默认
curl/7.x的请求,优先被限流。
对策不是“加大重试”,而是错峰+伪装+降频:
- 在
aria2c配置中加入--retry-wait=10 --max-tries=5,延长重试间隔; user-agent必须设为有意义的字符串,包含联系邮箱(如OCO2-DL-Team/1.0 (team@lab.edu)),Earthdata对合规UA的请求更宽容;- 最关键:下载前先做CMR预检。用一个轻量脚本,只查
page_size=1,确认Token有效、网络通畅,再启动大批量下载。这能避免下载到一半才发现Token过期。
5.2 文件名乱码与下载中断:URL编码的隐形杀手
OCO-2文件名含大量下划线、时间戳、版本号,如OCO2_L2_Standard_20200101_B10205_20200102123456_20200102123456.h5。当这个URL直接传给curl,其中的_和数字不会被编码,但某些代理或防火墙会误判为恶意payload而拦截。aria2c的auto-file-renaming=true能解决文件名问题,但无法解决URL被截断。
根本解法:对URL进行RFC 3986编码。我们用Python的urllib.parse.quote处理:
from urllib.parse import quote safe_url = quote(original_url, safe=':/?&=')safe参数指定哪些字符不编码,:/?&=是URL结构字符,必须保留。处理后的URL如https%3A//...,虽变长,但100%兼容所有中间件。
5.3 校验失败的三种真实场景与应对
校验失败不等于数据损坏,可能是以下原因:
| 场景 | 表现 | 诊断命令 | 解决方案 |
|---|---|---|---|
| CMR元数据延迟 | 下载文件SHA256与CMR返回值不符,但文件可正常h5py读取 | h5ls -r downloaded_file.h5 | head -10 | 等24小时再校验,CMR元数据同步有延迟 |
| Earthdata CDN缓存污染 | 同一URL多次下载,得到不同SHA256 | curl -I URL | grep ETag | 清除CDN缓存,或加时间戳参数?t=$(date +%s) |
| 本地磁盘损坏 | 单个文件校验失败,其他文件正常 | smartctl -a /dev/sdb | 更换硬盘,此情况极少,但必须排除 |
最实用的技巧:校验时跳过前1MB。因为OCO-2 HDF5文件头含动态时间戳,CMR计算校验码时可能排除了头信息。我们改用h5dump -H file.h5 \| sha256sum,只校验HDF5结构头,而非全文件——这招在NASA技术论坛被多位资深用户验证有效。
5.4 从“能用”到“好用”:自动化流程的进阶封装
当流程稳定后,下一步是封装为可交付物。我们做了三件事:
- Docker镜像:基础镜像
python:3.9-slim,预装aria2c、jq、curl、h5py,所有配置外置挂载,一行命令启动:docker run -v $(pwd)/config:/config -v $(pwd)/data:/data oco2-dl:latest --bbox "116,39,117,40" --year 2020 - Cron定时任务:每天凌晨2点自动拉取前一日全球数据,脚本中加入
if [ $(date -d "yesterday" +%Y%m%d) = $(date +%Y%m%d) ]; then ... fi判断; - Telegram通知:下载完成/失败时,用Bot API发消息到运维群,包含成功数、失败数、失败文件列表。
最后分享一个血泪教训:某次为赶项目进度,跳过校验直接归档,结果发现连续3天的数据在/failed/目录里积压了200+文件——原因是Earthdata在那周升级了TLS证书,旧版curl不认新证书,但错误被静默吞掉。从此我们定下铁律:任何跳过校验的操作,必须在脚本注释里用大写字母写明原因和恢复时间。数据质量,永远是第一位的。
我在实际使用中发现,这套流程最耗时的环节不是下载,而是前期的CMR参数调试。建议新人第一次用时,先用page_size=1查一个文件,打印出完整JSON,对着NASA的CMR字段文档(https://cmr.earthdata.nasa.gov/search/site/docs/search/api.html)逐字段对照,把bounding_box、temporal、collection_concept_id三个参数调通,后面就一马平川了。毕竟,OCO-2数据的价值,不在于它有多难获取,而在于你拿到手后,能用它回答什么真正重要的问题。