2. 观测数据文件格式解析

海洋气象观测数据从传感器到业务系统,往往要跨越多种文件格式。浮标CTD剖面常以NetCDF存储多维数组,船舶报和自动站分钟数据多以CSV交换,气象卫星与探空产品大量使用BUFR,数值预报场和再分析资料则以GRIB为主。能否准确解析这些格式,直接决定了后续质控能否拿到正确的变量、单位、时间戳和空间坐标。本节围绕“结构—读取—元数据提取”三个环节,逐一拆解四类常见格式,并给出可复用的读取代码与元数据核对方法。

一、四类格式的结构差异

NetCDF是一种自描述的多维数组格式,内部由维度(dimension)、变量(variable)和全局属性(global attribute)构成。一个典型的浮标剖面文件会定义 time、depth、lat、lon 四个维度,变量 TEMP 的维度顺序为 (time, depth),并通过 _FillValue、scale_factor、add_offset 属性描述缺测值与打包压缩规则。读取时必须先解析这些属性,否则会把整型存储的原始值误当作物理量。

CSV是纯文本表格,结构信息几乎全部依赖表头和约定。海洋气象CSV常见的问题是表头命名不统一(temp、WTMP、水温并存)、时间列格式混杂(ISO 8601、YYYYMMDDHH、Unix时间戳)、缺测用 -999、9999 或空串表示。读取时不能只靠 pandas.read_csv 的默认推断,必须显式指定 dtype、na_values 和 parse_dates。

BUFR是二进制表格驱动格式,其结构由“段”组成:指示段给出总长度和编辑版本,标识段携带观测类型、站点、时间等元数据,数据段则按“描述符”序列编码。描述符分三类:F(表B,要素描述符,如011001表示气温)、X(表C,操作符,如改变宽度或复用)、Y(表D,本地序列,如310002表示某类探空序列)。要读懂BUFR,必须同时持有WMO的表B、表C、表D,否则只能看到一串数字。

GRIB同样是二进制自描述格式,GRIB2由第0段(指示段)、第1段(标识段)、第2段(本地使用段)、第3段(网格定义段)、第4段(产品定义段)、第5段(数据表示段)、第6段(位图段)和第7段(数据段)组成。网格类型、投影参数、层次类型、参数类别与编号、打包方式都写在对应段中。GRIB1结构更简单但参数表较老,业务中仍会遇到。

四类格式的结构层次与读取入口 NetCDF 维度 time/depth/lat/lon 变量 TEMP(time,depth) 属性 _FillValue/scale 读取入口:netCDF4 / xarray CSV 表头命名不统一 时间格式混杂 缺测 -999/9999/空串 读取入口:pandas.read_csv BUFR 指示段 + 标识段 数据段:描述符序列 表B/表C/表D 驱动 读取入口:ecCodes / pybufr GRIB2 第3段 网格定义 第4段 产品定义 第5段 数据表示 读取入口:cfgrib / wgrib2 统一元数据清单 变量名 · 单位 · 时间基准 · 空间坐标 · 缺测值 · 打包规则 质检前置判断 单位是否换算到SI · 时间是否统一UTC · 坐标是否对齐 · 缺测是否识别 任一不通过 → 回退修正读取参数,不进入质控算法

二、读取方法与关键参数

NetCDF推荐使用 xarray 打开,它能自动解析维度坐标和属性,并支持惰性加载。读取时务必检查 ds.variables 的 units 与 _FillValue,对打包变量调用 mask_and_scale=True(默认开启)。若变量维度顺序与预期不符,用 transpose 显式调整,避免后续按轴索引时错位。

import xarray as xr
ds = xr.open_dataset("buoy_20240601.nc", mask_and_scale=True)
print(ds["TEMP"].attrs)          # 查看 units / _FillValue / scale_factor
temp = ds["TEMP"].transpose("time", "depth")
temp_c = temp.where(temp != temp.attrs.get("_FillValue", -9999))

CSV读取的核心是把“隐式约定”显式化。下面示例把常见缺测标记统一为 NaN,并强制时间列按UTC解析:

import pandas as pd
df = pd.read_csv("ship_obs.csv",
                 na_values=["-999", "9999", ""],
                 parse_dates=["obs_time"],
                 dtype={"station_id": str, "temp": float})
df["obs_time"] = pd.to_datetime(df["obs_time"], utc=True)

BUFR与GRIB建议统一走ECMWF的ecCodes体系。命令行用 bufr_dump -p 查看BUFR的描述符展开结果,用 grib_ls -p shortName,units,level,dataDate,dataTime 快速列出GRIB字段;Python侧用 eccodes 或 cfgrib 读取。GRIB转NetCDF可用 cfgrib 的 xr.open_dataset(engine="cfgrib"),但要注意同一文件含多个层次或时效时需加 backend_kwargs={"filter_by_keys": {...}} 过滤,否则会因超集冲突报错。

三、元数据提取实战

元数据提取不是“打印一下属性”,而是把分散在各格式中的关键信息归一到同一张清单,供质控规则消费。以一次浮标温度数据接入为例,需要提取并核对以下字段:变量标准名、原始单位、时间基准与覆盖范围、经纬度、深度层次、缺测标记、数据打包方式、文件版本与生成时间。

实际操作中,先写一个格式无关的提取函数,返回统一字典,再逐格式填充。例如NetCDF从 ds.attrs 和变量属性取,CSV从表头映射表和首末行取,BUFR从标识段的 dataCategory、typicalDate、typicalLatLon 取,GRIB从第1段和第3、4段取。提取完成后做三项一致性检查:单位是否为开尔文或摄氏度并明确标注、时间是否全部为UTC、经纬度是否落在合理海域范围。任何一项不通过,都应回到读取参数修正,而不是带着错误元数据进入质控。

经验做法:为每类格式维护一份“字段映射表”,把原始字段名、标准名、单位换算系数、缺测值写死。映射表随数据源版本更新,比在代码里散落判断更易维护,也便于交接和复核。
常见陷阱:NetCDF的 scale_factor 与 add_offset 未应用,导致温度出现整型量级;CSV把 9999 当有效值参与统计;GRIB未按 filter_by_keys 过滤,读到错误的层次或时效。这三类错误在质控前必须排除。

完成元数据提取后,建议把结果落成一份可核对的清单文件(如JSON或CSV),记录每个变量的来源、单位、时间范围与缺测数量。这样在后续质控中一旦发现异常,能快速判断是观测本身的问题,还是读取环节把单位、时间或坐标弄错了。格式解析的终点不是“能读出来”,而是“读出来的每个字段都可追溯、可核对、可直接进入质控流程”。