1. STAC 规范到底在解决什么麻烦
第一次接触 STAC 规范的人,多半是被一串文件名劝退的:一堆影像、一堆元数据文本、几个说明文档,还有一份不知道谁维护的 Excel 清单。要回答"某块区域在某个时间段有哪些可用的数据"这种最朴素的问题,往往得靠人肉翻目录、比对文件名里的日期字符串、再打开 GIS 软件逐个确认。STAC 规范就是为了终结这种状态而存在的——它把地理空间数据(遥感影像、栅格瓦片、点云、矢量切片都算)的时空信息、文件位置、附加属性,统一收敛成一套基于 JSON 的描述格式,让机器能像读网页一样读数据目录,也让不同团队之间的数据交换有一个共同的语言。
我给它的定位很直白:STAC 是空间数据界的"商品目录标准"。你不需要打开商品本身,就能知道它是什么、在哪儿、什么时候生产的、什么规格、去哪儿取货。整套工具链目前都是开源免费的,从生成、校验到发布,一分钱不花就能跑通全流程,这也是我愿意在项目里推开它的重要原因。这篇内容适合三类人看:手里攒了几十到几十万景影像、需要一个统一目录的工程同学;做数据平台、需要对外交付元数据接口的研发;以及刚上手遥感数据处理、还没搞清楚元数据该怎么组织的新人。不管你之前用不用 Python,只要理解了它的字段逻辑,剩下的都是体力活。
1.1 从"一堆文件夹"到"一份人和机器都能读懂的清单"
传统做法里,一个数据集的组织形态通常是"目录树 + 命名约定 + 一份 README"。这套东西对人还算友好,对程序就不行了:命名约定一旦有人不遵守,解析就崩;README 里的字段是自然语言,程序没法可靠提取。更麻烦的是跨团队交换——两个单位各自维护一套命名规则,数据一合并就得写一大堆胶水代码做字段映射。
STAC 的破局点在于把"约定"升级成"规范"。它规定了字段名、字段类型、取值范围、必填与选填,还规定了对象之间的链接关系。于是元数据从"给人看的说明"变成了"给程序用的接口"。你可以用同一段代码去读 A 团队的目录和 B 团队的目录,只要双方都遵循规范。这个转变带来的收益是复利的:校验器能自动查错,客户端能自动发现资源,搜索引擎能自动建索引,可视化工具能自动渲染足迹。
我特别想强调一个容易被忽视的点:STAC 描述的是"资产"而不是"数据本身"。它不关心你的影像用什么压缩算法、内部波段怎么排布,只关心这份资产在哪儿、是什么格式、什么角色。这种"轻描述"的设计让它可以串联起极其异构的底层存储——对象存储、HTTP 静态服务、本地磁盘,甚至归档磁带库,都可以挂在同一个目录下。
1.2 四个核心对象的层次关系
刚看规范文档时,Catalog、Collection、Item、ItemCollection 这四个名词容易把人绕晕。我用一个电商类比来讲清楚:Catalog 是"商城",Collection 是"某个品类的货架",Item 是"货架上的单件商品",ItemCollection 是"一次购物车里的商品集合"。
| 对象 | 类比 | 作用 | 关键字段 |
|---|---|---|---|
| Catalog | 商城 | 一个容器,用 links 把子目录串起来 | id、description、links |
| Collection | 品类货架 | 具有共同属性的 Item 集合,带整体时空范围 | extent、license、summaries |
| Item | 单件商品 | 最小描述单元,对应一份或一组资产 | geometry、bbox、properties、assets |
| ItemCollection | 购物车 | 查询结果的一次性封装,不落盘 | type、features |
理解这个层次的关键在于links。Catalog 和 Collection 本身几乎不承载数据,它们存在的意义就是"链接下去"。规范定义了 rel 类型:root指向顶层层级、parent指向上级、child指向下级、item指向具体条目、self指向自身。有了这套关系,一个庞杂的目录可以被拆成成千上万个静态小文件,客户端从根节点出发逐层下钻,不需要任何后端服务参与。
这里有个实践中的取舍:层级不要做得太深。我见过有人按"年度/月份/日期/传感器/轨道/产品级别"分了六七层,结果客户端要发几十次请求才能定位到一个 Item,体验极差。常规做法是控制在两到三层,把细分维度放进 Item 的 properties 里,交给搜索去过滤。
1.3 为什么是 JSON 和 GeoJSON,而不是数据库表结构
有人会问:既然要建目录,为什么不直接上 PostgreSQL 建表?答案是分层的。STAC 规范刻意只定义了一层轻量的文件格式,不强绑定任何存储引擎。你可以把它写成一堆静态 JSON 扔在静态服务器上,也可以塞进关系库、搜索引擎、图数据库里。规范定义的是"交换格式",不是"存储格式"。
选 JSON 的另一个理由是生态。JSON 能被几乎所有语言零成本解析,GeoJSON 又是地理信息领域的既有事实标准,Item 直接复用 GeoJSON 的 Feature 结构,geometry 用标准几何对象、bbox 用四元组,等于白捡了一整套现成工具链。你拿任何一个 GeoJSON 查看器都能把 Item 画到地图上,这个即插即用的能力在跨团队协作时价值极高。
至于 STAC 1.1.0 之后放开了 Item 必须为 GeoJSON 的限制,让点云、三维瓦片这类非二维几何的数据也能更自然地表达,这是规范在向更多数据类型延伸。如果你现在从零起步,建议直接按 1.1.0 写,遇到老工具不兼容再降级。
2. 字段层面的核心细节与写作纪律
规范看懂了不等于能写出合格的 Item。我审过不少团队产出的 STAC 目录,校验器能过的占大多数,但"能过校验"和"好用"之间还差着一段距离。这一节拆开讲字段怎么写、哪里容易翻车、哪些坑我亲自踩过。
2.1 Item 的骨架:六个字段决定生死
一个最小可用的 Item 长这样:
{ "type": "Feature", "stac_version": "1.0.0", "id": "scene-20240415-0001", "geometry": { "type": "Polygon", "coordinates": [[[100.0, 20.0], [100.5, 20.0], [100.5, 20.3], [100.0, 20.3], [100.0, 20.0]]] }, "bbox": [100.0, 20.0, 100.5, 20.3], "properties": { "datetime": "2024-04-15T03:20:00Z" }, "links": [], "assets": {} }逐条说。type固定为Feature,这是 GeoJSON 的约定,别自作主张改。stac_version标你遵循的规范版本,工具会据此选择校验规则,写错了会引发一连串莫名其妙的报错。id在同一个 Collection 内必须唯一,长度建议控制在 64 字符以内,只允许字母数字和少量安全符号——我见过用中文加空格的 id,客户端 URL 编码后惨不忍睹。
geometry要注意闭合环:Polygon 的第一个点和最后一个点必须相同,这是 GeoJSON 的硬性要求。bbox的顺序是[west, south, east, north],也就是经度在前、纬度在后,和 GeoJSON coordinates 的[x, y]顺序一致。这条看似废话,但它是新手翻车率最高的一处,下面还会细讲。
properties.datetime是必填项,除非你用了 start/end 时间区间替代。格式必须是 RFC 3339,带时区偏移。links和assets可以为空对象,但一个没有任何 assets 的 Item 基本没有实用价值。
注意:
id一旦对外发布就不要随意更改。下游很可能用它做缓存键或数据库主键,改一次 id 等于让整条链路失效一次。
2.2 时间字段:最容易翻车的地方
时间处理是元数据里最容易出错的部分,没有之一。规范要求datetime是 RFC 3339 格式,形如2024-04-15T03:20:00Z或2024-04-15T11:20:00+08:00。我见过的问题包括:用2024/04/15这种斜杠格式、用本地时间却不带时区、用2024-04-15 03:20:00带空格。这些在校验器眼里都是硬错误。
更隐蔽的问题是采集时间的语义。一景影像的成像时间往往是一个时刻,但如果产品经过了多时相合成,或者是一个长时间序列的立方体,单点时间就不够用了。这时要改用start_datetime和end_datetime组合,并且datetime字段必须置为null。很多人只填了区间不置 null,结果校验器报"datetime 与区间互斥"。
还有一个常见分歧:时间到底记什么?成像时刻、处理完成时刻、还是数据覆盖的时间范围?我的建议是只记成像时刻,处理时间放到自定义属性里。原因是搜索场景下用户几乎总是按"数据什么时候拍的"来筛,把处理时间混进datetime会让搜索结果失真。
时区统一用 UTC,存储时转成Z结尾。本地时区信息在展示层再转换。这条纪律看起来死板,但能省掉大量跨时区协作时的对账工作。
2.3 assets 的写法与 roles 的取舍
assets 是 Item 里信息密度最高的部分。每个 asset 至少要有一个 href 指向文件位置,可选的还有type(媒体类型)、title、roles、description。我见过最敷衍的写法是只给 href,客户端拿到之后不知道这是影像、缩略图还是元数据文件,只能靠扩展名猜。
roles是解决这个问题的关键。规范预定义了data、metadata、thumbnail、overview、visual等值,含义是"这份资产在整体里扮演什么角色"。一个典型的影像 Item 通常有:主数据资产标data、浏览图标thumbnail、云掩膜标data或自定义角色、元数据 XML 标metadata。客户端据此就能自动挑选合适的资源来展示。
媒体类型也要写对。GeoTIFF 用image/tiff; application=geotiff,COG 用image/tiff; application=geotiff; profile=cloud-optimized,PNG 用image/png。这些参数化的媒体类型是 STAC 生态里的通行做法,写对了客户端才知道能不能直接在浏览器里渲染。
我个人的经验是:每个 Item 至少配一张缩略图。这不是锦上添花,而是刚需。用户在搜索结果里看到几百个灰色多边形和看到几百张缩略图,体验差距是天壤之别。生成缩略图的成本极低,一次批处理就搞定,但收益贯穿整个数据集生命周期。
2.4 扩展字段:用多少合适
STAC 的核心规范刻意做得很薄,专业信息靠扩展(Extension)承载。常见的扩展有:eo记录云量、太阳高度角、波段信息;view记录入射角、方位角;proj记录投影参数和变换;raster记录波段统计和重采样;sat记录卫星平台和轨道;processing记录处理链信息。
扩展的价值在于它让不同团队对同一个概念用同一个字段名。比如云量,核心规范里没有,但eo:cloud_cover几乎是事实标准,所有做遥感检索的工具都认这个字段。如果各写各的cloud、cloudPercent、cloud_cover_rate,互操作性就没了。
但扩展也不宜贪多。我见过一个 Item 塞了十几个扩展、上百个字段,其中一大半从来没被查询过。这带来的直接后果是文件体积膨胀、校验变慢、维护成本上升。我的原则是:只有会被用来筛选或展示的字段才值得放进 properties,纯存档性质的信息塞进资产或外部文件。
另外一个细节:自定义字段必须带命名空间前缀,形如myorg:fieldname。不带前缀的自定义字段会破坏规范的向前兼容性,将来规范升级时可能和新字段撞名。
3. 从零跑通一个本地 STAC 目录
理论讲完,进入动手环节。这一节带你从空目录开始,做出一个能被校验、能被浏览、能被简单查询的 STAC 目录。整套流程全部使用开源免费的库,不需要任何商业授权。
3.1 环境准备与依赖选择
Python 环境推荐 3.9 以上。核心依赖三个:
pip install pystac pystac-client stac-validatorpystac是官方维护的读写库,负责构建对象、序列化、链接解析;stac-validator是独立的校验工具,能对着官方 JSON Schema 逐字段检查;pystac-client是客户端库,用于查询 API 服务,做静态目录遍历时也用得上。
如果你要处理影像本身的元数据(从 GeoTIFF 里读投影、波段、空间范围),再加两个:
pip install rasterio rio-stacrasterio是 GDAL 的 Python 封装,读栅格头信息很稳;rio-stac是个小工具,能从一份栅格直接生成 Item 骨架,省掉手写 geometry 和 bbox 的功夫。不过它生成的是骨架,属性和资产还得自己补全,别指望一键搞定。
提示:如果你在离线环境里工作,提前把 wheel 包下载好。这几个库的依赖链条不算短,现场装容易卡在编译环节。
3.2 手写第一个 Item 并校验
先用代码生成一个 Item,比手写 JSON 更不容易错:
import datetime import pystac bbox = [100.0, 20.0, 100.5, 20.3] geometry = { "type": "Polygon", "coordinates": [[ [100.0, 20.0], [100.5, 20.0], [100.5, 20.3], [100.0, 20.3], [100.0, 20.0] ]] } item = pystac.Item( id="scene-20240415-0001", geometry=geometry, bbox=bbox, datetime=datetime.datetime(2024, 4, 15, 3, 20, tzinfo=datetime.timezone.utc), properties={"platform": "demo-sat", "myorg:processing_level": "L2A"}, ) item.add_asset( "visual", pystac.Asset( href="./assets/scene-20240415-0001.tif", media_type="image/tiff; application=geotiff; profile=cloud-optimized", roles=["data"], title="正射影像", ), ) item.add_asset( "thumbnail", pystac.Asset( href="./assets/scene-20240415-0001-thumb.png", media_type="image/png", roles=["thumbnail"], ), ) item.set_self_href("./catalog/scene-20240415-0001/scene-20240415-0001.json") item.save_object() print("已写出:", item.get_self_href())这段代码跑完,磁盘上就多了一个 JSON 文件。接着用校验器查一遍:
stac-validator ./catalog/scene-20240415-0001/scene-20240415-0001.json输出里如果出现VALID就说明结构没问题。如果有报错,别急着改,先把报错信息里的instance path找出来,它精确指向出问题的字段位置。
set_self_href这一步很多人会漏。漏了会怎样?对象本身能序列化,但里面的链接会全部变成相对路径解析失败,导致后续拼 Catalog 时出现一堆坏链。养成"先设 self href 再保存"的习惯,能省掉大量调试时间。
3.3 批量构建 Collection
单个 Item 好办,真正的活是批量。逻辑很简单:遍历文件清单,逐个生成 Item,塞进同一个 Collection,最后保存。
import pystac collection = pystac.Collection( id="demo-imagery", description="示例影像集合,用于验证 STAC 目录构建流程", extent=pystac.Extent( spatial=pystac.SpatialExtent([[100.0, 20.0, 100.5, 20.3]]), temporal=pystac.TemporalExtent([[ datetime.datetime(2024, 1, 1, tzinfo=datetime.timezone.utc), datetime.datetime(2024, 12, 31, tzinfo=datetime.timezone.utc), ]]), ), license="proprietary", ) for record in scan_files("./raw"): item = build_item_from_record(record) collection.add_item(item) collection.normalize_hrefs("./catalog") collection.save(catalog_type=pystac.CatalogType.SELF_CONTAINED)这里几个参数值得展开。extent的空间范围是 Collection 整体的包络框,写小了会导致客户端认为落在范围外的 Item 不属于这个集合;写大了则会让搜索时多召回无关集合。实践中建议先用脚本把所有 Item 的 bbox 并起来算一次,再回填。
normalize_hrefs会自动给每个对象分配一个规范的路径,避免文件全部堆在一个目录里。路径规则默认是"id 做目录名",如果你的 id 里有斜杠或者特殊字符,提前做一次清洗。
save的catalog_type有三个取值:SELF_CONTAINED把所有子对象内联到父文件里(一个大 JSON),INHERITING让子对象继承父级属性,ABSOLUTE_PUBLISHED生成绝对链接适合对外发布。本地调试用 SELF_CONTAINED 最简单,一个大文件搞定,但数据量上千之后就别用了,文件会大到编辑器打不开。对外发布用ABSOLUTE_PUBLISHED,链接全是绝对地址,客户端从任何入口进来都能正确解析。
3.4 目录结构与本地预览
生成的目录大致长这样:
catalog/ ├── catalog.json ├── demo-imagery/ │ ├── collection.json │ ├── scene-20240415-0001/ │ │ └── scene-20240415-0001.json │ └── scene-20240416-0002/ │ └── scene-20240416-0002.json └── assets/ ├── scene-20240415-0001.tif └── scene-20240415-0001-thumb.png每个 JSON 里都带self、root、parent链接,形成一张完整的图。想验证链接是否都通,起一个本地静态服务即可:
python -m http.server 8000 --directory ./catalog浏览器访问http://localhost:8000/catalog.json,能看到 JSON 就说明静态发布没问题。但注意:file://协议下很多客户端会因为跨域策略拒绝加载,一定要走 HTTP。这是我在早期调试时浪费了半个下午才想明白的事。
3.5 用客户端验证可用性
目录建好之后,最关键的验证是"别人能不能用"。拿 pystac 走一遍遍历,模拟真实客户端的访问路径:
import pystac cat = pystac.Catalog.from_file("http://localhost:8000/catalog.json") for item in cat.get_all_items(): print(item.id, item.datetime, item.bbox) for key, asset in item.assets.items(): print(" ", key, asset.roles, asset.href)如果这段能顺利跑完并打印出所有 Item,说明链接层级、字段格式、self href 全都是对的。跑不通的话,根据异常信息定位——最常见的是相对路径层级算错,或者某层缺了child链接导致遍历断了。
关于搜索:静态目录本身没有查询能力,pystac-client对纯静态目录的支持也有限。如果你只需要按属性做轻量过滤,自己写几行代码遍历一遍再筛,完全够用:
from datetime import datetime, timezone hits = [ it for it in cat.get_all_items() if it.datetime >= datetime(2024, 4, 1, tzinfo=timezone.utc) and it.properties.get("myorg:processing_level") == "L2A" ]数据量在几千以内,这种暴力遍历的速度是可以接受的。上万之后就得考虑上 API 服务了,下一节展开讲。
4. 静态目录与 API 服务的选型对比
很多团队在这一步纠结:既然规范都规定好了,那到底该发布一堆静态 JSON,还是上一套 API 服务?这不是技术优劣问题,而是使用场景问题。
4.1 静态目录的真实优势
静态目录最大的优势是零运维。一堆 JSON 扔进对象存储或者静态服务器,配个访问策略就完事了,没有进程要守护、没有数据库要备份、没有并发要调优。我曾经把一个包含几万个 Item 的目录直接放进对象存储,前面挂个静态服务,稳定跑了很久,几乎没出过问题。
第二个优势是成本。静态文件按存储和流量计费,查询不走服务端计算,边际成本极低。相比之下,API 服务要常驻内存、要建索引、要处理并发,长期成本高出一个量级。
第三个优势是缓存友好。静态 JSON 天然支持 HTTP 缓存、CDN 分发。用户第一次拉完目录之后,后续访问基本走本地缓存,响应速度极快。
代价也很明确:查询能力弱。用户只能按目录层级逐层下钻,或者自己把整个目录拉下来在本地过滤。对于"搜索某区域某时间段"这类需求,静态方案体验很差。
4.2 API 带来的能力与代价
STAC API 是在规范之上定义的一组 REST 接口,核心是/search,支持按空间范围、时间范围、属性条件、分页游标组合查询。它的价值在于把筛选下推到服务端,客户端只拿结果,不拉全量目录。
实现上,社区有现成的开源方案可以直接用,基本形态是"一个 API 服务 + 一个搜索后端"。搜索后端负责建空间索引和时间索引,API 层负责把 STAC 查询参数翻译成后端查询语句,再组装成 ItemCollection 返回。
代价有三块。一是运维复杂度:多了一个服务、一个数据库,监控、备份、升级都要跟上。二是数据一致性:静态目录改一次文件就生效,API 模式下数据入库有延迟,需要设计同步机制。三是成本:常驻服务和索引存储是持续的支出。
我的判断标准很简单:如果用户的核心诉求是"浏览",静态目录足够;如果核心诉求是"检索",必须上 API。浏览指的是"我想看看这个数据集里都有什么",检索指的是"我要找某块区域在某段时间的全部数据"。
4.3 混合部署的实践路径
实际情况里,两者往往并存。我的做法是:底层维护一份规范的静态目录作为"唯一事实来源",API 服务从这份目录里灌数据。这样有两个好处:一是静态目录可以独立分发给需要离线使用的合作方;二是 API 挂了的时候,用户还能退回到静态浏览,不至于完全不可用。
同步策略上,建议以 Collection 为单位做增量。每次静态目录更新后,计算受影响 Collection 的哈希,只重新灌变化的集合,避免全量重建索引。这个技巧能把日常同步耗时从小时级压到分钟级。
还有个小细节:静态目录里的links要保留完整,别为了 API 模式就把child、item链接删了。留着它们,客户端才能在不依赖 API 的情况下完成遍历,等于给自己留了条后路。
5. 常见问题与排查技巧实录
这一节是我在实际项目里攒下来的问题清单,基本都是校验器能过、但用起来出问题的那一类。
5.1 校验报错的高频类型
先说几个最常撞的报错,以及它们背后的真实原因。
'datetime' is a required property—— 这条不代表你真的没写 datetime,很可能是你写了 write 到properties外面去了,或者用了start_datetime但忘了把datetime显式置为null。
'bbox' does not match geometry—— 你的 bbox 和 geometry 算出来的包络对不上。常见原因是 bbox 用的是投影坐标,geometry 用的是经纬度。两者必须同一坐标系,规范要求经纬度。
Additional properties are not allowed—— 你在 properties 里写了没加命名空间前缀的自定义字段,或者扩展字段用了但没在 Item 顶层声明stac_extensions。后者尤其常见,扩展字段不加声明,严格的校验器会当成非法字段。
Invalid type for 'stac_extensions'—— 这个字段是数组,很多人写成字符串。而且数组里放的是 schema 的完整 URL,不是扩展名。写错的话,所有基于扩展的字段校验都会失效。
5.2 坐标顺序与几何细节
bbox 顺序的问题值得单独拎出来说。STAC 用的是[west, south, east, north],也就是先经度后纬度,而且是最小值在前、最大值在后。但不同来源的数据坐标系习惯差异巨大:有的库给的是[minx, miny, maxx, maxy],有的给的是[minlat, minlon, maxlat, maxlon],还有的给的是[minx, miny, minz, maxx, maxy, maxz]三维六元组。
我踩过最典型的一次坑是:从某个栅格库里读出来的边界是(lon_min, lat_min, lon_max, lat_max),直接塞进 bbox 看着没问题,但切换到南半球的数据就全乱了——因为纬度最小值在数值上更小,排序后位置变了,但我的代码按固定索引取值,结果错位。正确做法是先算出四至,再显式按 west/south/east/north 组装,不要指望库的返回值顺序。
几何闭合也是高频错误。Polygon 的坐标环首尾必须相同,差一个点校验器就报错。另外,跨 180 度经线的多边形要小心,直接用经纬度画会发现它横跨了整个地球,需要做切分。
注意:geometry 里不要带 Z 值。带高程的坐标在部分客户端会解析失败,高程信息放进 properties 更稳妥。
5.3 链接解析失败的三类原因
链接问题在静态目录里出现频率最高,症状是客户端能打开根目录,但下钻时断在某一层。
第一类是相对路径层级算错。Item 存在collection/item-id/item.json,它引用资产时用./assets/xxx.tif,实际解析后指向collection/item-id/assets/xxx.tif,但资产其实在collection/assets/。修法是用绝对路径,或者用set_self_href之后让库帮你算相对路径。
第二类是发布地址变更后没重算链接。本地调试时链接指向 localhost,发布到正式地址后忘了重新生成,客户端自然全部 404。解决办法是生成流程里不要硬编码主机名,从配置文件读。
第三类是大小写和 URL 编码。id 里带大写字母或特殊字符,某些文件系统不区分大小写,某些又区分,跨平台搬运时链接就断了。最保险的写法是 id 全部用小写字母、数字和连字符。
5.4 大规模目录的性能处理
Item 超过一万之后,几个问题会集中爆发。
目录遍历变慢:客户端逐层下钻,每层都要发请求,层级越深越慢。对策是把 Collection 拆得粗一些,同时给每个 Collection 加summaries,把常用筛选维度的取值范围直接列在 Collection 里,客户端不用下钻就能知道"这个集合里有 2023、2024 两年的数据",很多场景下第一次请求就能过滤掉大半集合。
存储体积膨胀:每个 Item 里冗余的字段太多。对策是把公共属性上移到 Collection 的summaries或共享属性里,Item 里只存差异部分。规范支持的"继承"机制就是干这个用的,但要注意客户端的兼容性,不是所有工具都完整实现了继承。
校验耗时过长:全量校验几万个文件要跑很久。对策是抽样校验加增量校验——改动过的 Item 全量校验,未改动的跳过。
5.5 常见问题速查表
| 现象 | 可能原因 | 排查方向 |
|---|---|---|
| 校验通过但客户端遍历断链 | self href 未设置 | 检查每个对象的 self 链接是否指向自身实际路径 |
| 搜索命中不到应有的 Item | bbox 范围偏小或坐标系错误 | 用 geometry 重算包络,确认是经纬度 |
| 缩略图不显示 | roles 或 media_type 写错 | 检查是否标了 thumbnail、类型是否为 image/* |
| 时间筛选结果偏差一天 | 时间戳缺时区或用了本地时间 | 统一转 UTC,格式带 Z 结尾 |
| 云量筛选无效 | 字段名不统一或未声明扩展 | 统一用 eo 扩展的 cloud_cover,声明 stac_extensions |
| 扩展字段校验报错 | 未声明扩展 URL | 在 stac_extensions 数组里补上对应 schema 地址 |
| 大批量生成内存溢出 | 一次性把全部 Item 加载进内存 | 改成分批构建、逐 Collection 保存 |
| 发布后相对路径全失效 | 目录结构被重组 | 重新执行路径规范化并重算链接 |
5.6 几条不太好写在文档里的经验
第一条:先定 id 规则,再写代码。id 是整条链路的锚点,一旦发出去就很难改。我建议的规则是"传感器_日期_轨道号_处理级别",全小写、下划线分隔,定下来之后写进校验脚本里强制检查。
第二条:属性字段宁少勿多。每加一个字段,问自己三个问题:会用来筛吗?会用来展示吗?下游真的需要吗?三个都否,就别加。我接手过一个目录,properties 里有四十多个字段,实际被查询过的不到五个,剩下的全是历史包袱。
第三条:缩略图和浏览图尽早生成。它的收益不是线性的,而是贯穿整个数据集生命周期。用户在检索页面看到缩略图的那一秒,决定了后面的所有体验。
第四条:校验要进 CI。人工跑校验器必然漏,把校验脚本挂到构建流程里,任何一次提交都自动跑一遍,坏掉当场发现。这个投入的回报比任何优化都高。
第五条:留一份"人类可读"的说明。规范解决了机器可读的问题,但新同事上手还是需要一份说明文档,讲清楚这个目录里有什么、字段含义是什么、资产怎么取。这份文档不用长,一页纸就够,但能省掉无数次重复答疑。
我个人在实际操作中的体会是,STAC 这类规范的价值不在于它规定得多严格,而在于它把"约定"变成了可以自动检查的东西。一旦有了校验器兜底,团队内部的争论就从"我觉得应该这样写"变成了"校验器说这样不行",沟通成本直接下降一个量级。真正花时间的从来不是写 JSON,而是想清楚你的数据到底该暴露哪些维度、给谁用、怎么用——这件事规范帮不了你,只能靠对业务的理解。