简介:一份专为 GIS 教学和海洋空间分析准备的矢量数据资源,内容为中国海洋面状要素,文件采用 Shapefile 标准格式封装,可直接在 ArcGIS、QGIS 等常用平台加载。数据涵盖大陆海岸带、主要海域及岛礁边界,打开属性表即可检索海域名称、类型、面积等字段,适合用于海洋功能区划、海岸线变迁分析、保护区选划以及多图层叠加的专题制图;对高校地理信息课程、科研团队和海洋管理者均有实用价值,也可作为毕业设计或课程实验的基础数据。压缩包内共 7 个文件,以 .shp 几何文件为核心,配合 .dbf 属性表、.prj 坐标参考、.shx 空间索引、.sbn/.sbx 检索索引及 .xml 元数据,其中 .prj 明确定义了坐标系,可保证数据在叠加其它图层时位置准确,整体构成一套结构完整的矢量数据集,也可当作学习 Shapefile 各组成文件的实例;整包仅 27KB,传输与存储都很方便。目前已有 2690 人学习下载,无论是学生练习空间分析,还是专业人员用作研究底图,都能快速上手。 拿到“中国海洋_面.rar”这类压缩包,很多人第一反应是双击解压,然后把里面文件全拖进绘图软件。我的建议相反:先别解压,先当一包“未知货物”做体检。因为“_面”这个词太模糊,可能是海表面温度场、海面高度异常,甚至是一组垂直断面观测;数据结构不同,预处理链路就完全不同,用错一套流程基本返工。
这篇文章就围绕“拿到这个包之后怎么落地”展开:安全解压与格式识别、结构判别与标准化、最小代码出图与序列分析,最后给一段可放进流程的自动化巡检脚本。
适合正在做海洋遥感分析、数值模拟后处理,或刚拿到一批海洋面数据不知道从哪下手的同学。看完你至少知道下一步具体敲什么命令。
2. 打开压缩包之前:命令行体检与数据格式识别
2.1 用 lsar 和 unar 先列清单再解压
双击解压看着省事,但会把全部文件一次性释放到当前目录,文件名编码、深层目录结构都是黑匣子。数据包越老,越容易出现中文文件名乱码或嵌套多层文件夹的情况。我一般先列清单,确认结构之后再释放,顺带避免压缩包里藏着../这类越界路径。
# 列清单,-L 会同时打印文件名编码 lsar -L 中国海洋_面.rar | head -40 # 结构确认后,解压到 data_raw,-f 表示冲突时重命名,避免覆盖旧数据 unar -o data_raw -f 中国海洋_面.rar逻辑说明:lsar是unar配套的列表工具,-L会在输出里标注每个文件名的原始编码。如果看到GBK或Big5字样,说明压缩包内文件名不是 UTF-8,直接用系统解压很容易变成乱码。unar解压时会按原编码还原文件名,比手动改编码省事。
参数说明:-o data_raw指定输出目录,保持包内原有目录结构;-f表示遇到同名文件时自动改名,适合重复解压时保留旧副本。不带-f时默认跳过已存在文件,反而容易漏文件。如果环境里没有unar,用bsdtar也能完成同样的列清单和解压操作:
bsdtar -tf 中国海洋_面.rar | head -40 bsdtar -xf 中国海洋_面.rar -C data_raw-tf只列清单,-xf释放文件,-C指定目标目录。解压完成后,先ls data_raw看一眼目录层级,不要急着进下一步。
2.2 用 file 与 xxd 识别真实格式:文件头比扩展名诚实
海洋数据的扩展名经常乱标:.dat里面可能是 NetCDF3,.bin里面可能是 HDF5。判别依据不是后缀,而是二进制文件头,也就是 magic number。
| 文件头(十六进制) | 真实格式 | 说明 |
|---|---|---|
43 46 01/43 46 02 | NetCDF3 classic / 64-bit | 老数据常见 |
89 48 44 46 | HDF5 / NetCDF4 | 新数据主流 |
49 49 2A 00 | GeoTIFF | 卫星栅格产品 |
47 52 49 42 | GRIB1 / GRIB2 | 数值模式输出 |
| 可读 ASCII 文本 | CSV / ASCII | 站点序列或表格 |
# 对目录内所有文件做类型判断 file data_raw/* # 想看具体二进制头,读前 32 字节 xxd -l 32 data_raw/目标文件.nc说明:file命令跨平台可用,输出会顺带显示是否被 gzip 或 zip 包裹。如果看到gzip compressed data,说明外层还有一层压缩,先解包再识别内层格式。.nc后缀不代表一定能用 NetCDF 工具打开,先跑一遍file能省掉后面大量报错排查。
参数说明:xxd -l 32控制只读取前 32 字节,文件再大也不会卡住。如果输出是data这种通用结果,说明文件头不在常见白名单里,下一步用 Python 读原始字节判断。
2.3 用 xarray 盘点变量、维度和坐标
拿到文件后不要急着画图,先打印数据集结构。xarray 是处理海洋格点数据最顺手的工具,一条命令就能把维度、变量、单位看清楚。
import xarray as xr ds = xr.open_dataset("data_raw/目标文件.nc", engine="netcdf4") print(ds) # 逐个变量看维度、形状和单位 for name in ds.data_vars: var = ds[name] print(f"{name}: dims={var.dims} shape={var.shape} units={var.attrs.get('units')}")说明:xr.open_dataset会自动解码 CF 约定的缺省值、缩放因子和时间坐标。print(ds)输出里重点看两行:Dimensions和Data variables。如果变量里出现time, lat, lon,就是典型格点场;如果只有station, depth, zlev,那就是站点或剖面数据,后续处理链路完全不同,不能走二维插值流程。
参数说明:engine="netcdf4"适合 NetCDF3/4 文件;读经典 NetCDF3 时也可以改成engine="scipy"。如果文件是 HDF5 但内部不是 NetCDF4 的组织方式,xarray会报错,这时回头用file确认格式,再决定用h5py还是直接找数据源换文件。
提示:
print(ds)里看到大量NaN不一定代表缺测,可能只是scale_factor没被解析。先检查每个变量的encoding,再决定要不要清洗。
3. 搞清楚“_面”是哪一种面:结构判别与标准化
3.1 三种海洋“面”数据的判别方法
把变量清单打印出来后,第一件事是判断它属于哪一类。“面”可能是海表面温度场,也可能是海面高度场,甚至是一组垂直断面。三类数据的维度结构差别很大:
| 数据类别 | 典型变量名 | 典型维度 | 常用单位 |
|---|---|---|---|
| 海表面温度场 | sst/tos | time, lat, lon | degC / K |
| 海面高度场 | ssh/sla/adt | time, lat, lon | m |
| 断面 / 剖面观测 | temp/salt | time, station, depth | degC / psu |
判别方法很直接:坐标里有lat/lon且没有depth/lev,是水平面场。出现depth或lev维度时,哪怕变量名叫sst,也是三维场,不能直接当“面”处理。出现station维度时,数据是不规则分布的浮标或走航观测点,也不是规则格点场。
has_depth = ("depth" in ds.dims) or ("lev" in ds.dims) has_station = "station" in ds.dims if has_depth: print("剖面/三维数据:先决定垂直方向处理方式") elif has_station: print("站点序列:不能按经纬度格点直接插值") else: print("水平面场:可以走常规网格化流程")说明:这段判断决定后面是做二维插值还是做深度整合。很多做海温分析的同学在三维数据上直接isel(zlev=0)当作海表,结果把混合层温度当成表层,也算一种常见翻车。
参数说明:有的文件用lev、zlev、deptht表示深度维,建议把几个常见名字都查一遍再下结论。拿到数据先跑这个三行判断,能避免后面整套流程走错方向。
3.2 时间维标准化与缺省值清洗
时间轴是海洋数据里最容易出问题的地方。有的时间坐标会被 xarray 自动解码成datetime64,有的则因为缺少单位属性变成一串数字。
import xarray as xr import pandas as pd import numpy as np ds = xr.open_dataset("data_raw/目标文件.nc", engine="netcdf4") # 1) 如果时间坐标没被正确解码,尝试手动指定单位 if not np.issubdtype(ds["time"].dtype, np.datetime64): units = ds["time"].attrs.get("units", "") if "hours" in units: ds = ds.assign_coords(time=pd.to_datetime( ds["time"].values, unit="h", origin="1900-01-01" )) # 2) 把每个变量的 _FillValue 清洗成 NaN for var in ds.data_vars: fill = ds[var].encoding.get("_FillValue") if fill is not None: ds[var] = ds[var].where(ds[var] != fill) print(ds)说明:时间维最常见的问题是units属性写成hours since 1900-01-01、days since 2000-01-01等。xarray 的decode_cf通常能自动处理;一旦缺了units,时间坐标会以数值形式出现,后续groupby时间全部报错。上面这个手动分支是补丁。
参数说明:unit="h"对应hours,如果文件里写的是days since...,改成unit="D";origin常见是1900-01-01,但也有老数据用1800-01-01,需要看文件头说明,不要死记。缺省值方面,有的变量用-9999,有些海冰数据用9.96921e+36;如果属性里没写_FillValue,可以按变量物理范围做硬过滤,例如 SST 只保留[-5, 45]之间的值。
3.3 空间标准化:经度统一与规则网格重采样
不同来源的数据,经度范围可能完全不同。有的用0~360°,有的用-180~180°,还有的是不规则网格。统一空间坐标是出图和分析前必须完成的一步。
import numpy as np import xarray as xr ds = xr.open_dataset("data_raw/目标文件.nc").isel(time=0) # 1) 0~360 度经度转成 -180~180 度,避免跨日界线撕裂 if ds["lon"].max() > 180: lon_new = ((ds["lon"] + 180) % 360) - 180 ds = ds.assign_coords(lon=lon_new).sortby("lon") # 2) 构造目标规则网格,步长 0.25 度 target_lon = np.arange(99.5, 125.0, 0.25) target_lat = np.arange(0.5, 40.0, 0.25) # 3) 最近邻插值到目标网格 ds_regrid = ds.interp(lat=target_lat, lon=target_lon, method="nearest") print(ds_regrid)说明:为什么要先做经度转换?因为很多再分析资料的经度范围是0~360°,绘图时按-180~180°显示会在 180° 附近出现一条空白带,数据本身连续,看起来却像被切开。转换后sortby保证坐标单调递增,后续插值和绘图都更稳。
参数说明:target_lon/target_lat的范围和步长要根据实际研究区修改。method="nearest"保留原始值,适合做统计一致性要求高的分析;method="linear"平滑但会把陆地缺测值带入,插值后必须重新掩膜。如果原始数据本身是站点散点,interp不可用,要换成scipy.interpolate.griddata。
注意:如果数据里有经度
360°的重叠点,先执行ds = ds.where(ds.lon < 360, drop=True)再转换,否则 0° 和 360° 会重叠在同一列。
4. 让数据说话:出图、点位序列与气候态平均
4.1 单时次面场出图:SST 的色标与陆地掩膜
预处理做完,先画一张单时次图看看整体分布。这里最容易翻车的是色标范围和缺测值处理。
import matplotlib.pyplot as plt import xarray as xr ds = xr.open_dataset("data_raw/目标文件.nc") field = ds["sst"].isel(time=0) # 缺测转 NaN,避免把 FillValue 画进色标 field = field.where(field.notnull()) fig, ax = plt.subplots(figsize=(8, 6)) pc = ax.pcolormesh(field.lon, field.lat, field, cmap="RdBu_r", vmin=10, vmax=30) fig.colorbar(pc, ax=ax, shrink=0.8, label="SST (degC)") # 陆地/无数据区域显示为浅灰 ax.set_facecolor("#d9d9d9") fig.savefig("sst_first_frame.png", dpi=200, bbox_inches="tight")说明:用pcolormesh而不是imshow,因为海洋数据的经纬度网格不一定等间距,pcolormesh会按实际坐标放置色块,imshow只按行列拉伸,容易出现变形。vmin/vmax一定要手动设定,SST 数据里只要有少数异常点,自动色标会把整个色带拉爆。
参数说明:cmap="RdBu_r"适合温度类要素,冷蓝暖红。vmin/vmax按要素实际物理范围设置,目标海域海表面温度一般用10~30°C,全球尺度可用-2~30°C。不用再叠加海岸线,很多发布数据本身自带陆地点位,额外加岸线反而会出现边界对不齐的视觉误差。
如果要批量输出所有时次,套一层循环即可:
for t in range(ds.sizes["time"]): fig, ax = plt.subplots(figsize=(8, 6)) ax.pcolormesh(ds["lon"], ds["lat"], ds["sst"].isel(time=t), cmap="RdBu_r", vmin=10, vmax=30) ax.set_facecolor("#d9d9d9") fig.savefig(f"sst_{t:03d}.png", dpi=150, bbox_inches="tight") plt.close(fig)4.2 点位时间序列:最近邻取点并计算月平均
面场图看空间分布,点位序列看时间变化。直接从格点数据里提取某个固定点位,做月平均是最常见的需求。
import pandas as pd import xarray as xr ds = xr.open_dataset("data_raw/目标文件.nc") sst = ds["sst"] # 目标点位:某近岸站点,按实际需要改 target_lon, target_lat = 122.5, 30.5 # 最近邻格点 lat_idx = abs(sst.lat - target_lat).argmin() lon_idx = abs(sst.lon - target_lon).argmin() series = sst.isel(lat=lat_idx, lon=lon_idx).squeeze() # 检查取到的是否为缺测,必要时扩大搜索范围 valid = series.dropna(dim="time") print("有效观测数:", valid.size, "/ 总时次:", series.size) # 转 DataFrame 并算月平均 df = series.to_dataframe(name="sst").dropna() monthly = df["sst"].resample("MS").mean() print(monthly.head())说明:最近邻取点比双线性插值稳妥,因为它不会混入周边网格的值。如果目标点恰好落在陆地掩膜里,最近邻会取到NaN,这时先看打印出的有效观测数,再做局部搜索。有人会用整个研究区域的空间平均代替点位序列,这适合区域整体变化分析,但会掩盖近岸和远海的差异,两种做法可以同时保留、分开出图。
参数说明:resample("MS")以每月第一天为分组标签,再对组内取平均即得到月平均;如果要按自然日对齐,改成resample("1D")。argmin()在 1D 坐标上找最小差位置,避免手写循环遍历。
4.3 气候态月平均场与距平计算
单年数据看完,需要算多年平均的“气候态”,拿它做基准算距平。xarray 的groupby最方便。
# 去掉时间维的年际波动,得到 12 张月平均场 clim = ds["sst"].groupby("time.month").mean(dim="time") # 输出 12 张月平均场 for month in range(1, 13): field = clim.sel(month=month) fig, ax = plt.subplots(figsize=(8, 6)) ax.pcolormesh(field.lon, field.lat, field, cmap="RdBu_r", vmin=10, vmax=30) ax.set_facecolor("#d9d9d9") fig.savefig(f"clim_{month:02d}.png", dpi=150, bbox_inches="tight") plt.close(fig) # 某年 8 月减去气候态,得到距平场 anom = ds["sst"].sel(time="2020-08", method="nearest") - clim.sel(month=8)说明:groupby("time.month")是 xarray 里做气候态最直接的写法,前提是time坐标已经是datetime64类型,否则会报错。sel(time="2020-08", method="nearest")取离该时间最近的时次;如果数据是日平均,最好先resample到月再减,否则单日距平会叠加日内噪声。距平场是后续分析海洋异常和极端事件的基础输入。
5. 避坑:海洋面数据处理的 5 个常见翻车现场
下面 5 条基本覆盖海洋面数据从解压到出图的主要翻车点,每条都按现象、原因、解决列出来。
5.1 读取与解码类:值域爆炸、时间错位、中文乱码
1. 海温图变成一锅“开水”,色标顶部是 32767
现象:画出来的海温场大部分是深红色,色标最大值为 32767,图完全没有区分度。
原因:文件把缺省值写成了32767或-9999这样的特征值,读取后没有转成NaN。
解决:读取时明确mask_and_scale=True;如果文件属性不规范,按变量把_FillValue转NaN。出图前再加一个兜底过滤:
field = ds["sst"].where((ds["sst"] > -5) & (ds["sst"] < 45))2. 时间轴是一串大整数,月平均算出来年份错乱
现象:X 轴刻度不是日期而是 30000 左右的数字,距平算出来年份标签完全对不上。
原因:时间变量以hours since 1900-01-01的数字存储,没走 CF 解码。
解决:检查time的dtype是否是datetime64;如果不是,用xr.decode_cf(ds)或手动指定单位:
ds = ds.assign_coords(time=pd.to_datetime(ds.time.values, unit="h", origin="1900-01-01"))3. RAR 文件名解压成乱码,脚本匹配不到文件
现象:解压后出现一堆�开头的文件名,glob通配符*匹配不到目标。
原因:压缩包内文件名是 GBK 编码,当前系统按 UTF-8 解释。
解决:解压前先lsar -L看编码,用unar解压会自动转码;如果已经乱码,用convmv批量修复:
convmv -f GBK -t UTF-8 --notest -r data_raw/5.2 空间变换类:经度撕裂、插值出假信号
4. 经度跨日界线,海面“裂开”一道口子
现象:等值线在 180° 附近整齐断裂,两侧颜色不一致但数值连续。
原因:数据经度范围是0~360°,画图坐标系按-180~180°显示。
解决:统一转成-180~180°并排序:
ds = ds.assign_coords(lon=(((ds.lon + 180) % 360) - 180)).sortby("lon")5. 双线性插值之后,陆地长出了“暖水舌头”
现象:海岸线附近等值线向陆地凸出,像舌头一样伸进陆地区域。
原因:插值把陆地缺测值与海洋有效值做加权平均,制造了不存在的过渡带。
解决:插值前先掩膜,插值后用原始掩膜再过滤一次:
mask = ds["sst"].notnull() regrid = ds["sst"].interp(lat=target_lat, lon=target_lon, method="linear") regrid = regrid.where(mask.interp(lat=target_lat, lon=target_lon, method="nearest").notnull())掩膜插值用nearest而不是linear,否则掩膜本身也会产生过渡带,陆地边缘照样被污染。
6. 进阶:把数据处理做成可重复的自动化巡检
数据不是一次性的,同一个包往往会更新版本。手工反复检查每个文件很耗神,而且容易漏掉坏帧。我一般会把前面几章的检查逻辑收拢成一个巡检脚本,放在所有预处理流程的最前端。
巡检脚本只做五件事:文件能否打开、维度是否齐全、时间是否连续、值域是否合理、是否存在全NaN剖面。写成一个独立文件check_pkg.py,以后每次拿到新数据先跑一遍:
import glob import numpy as np import xarray as xr def check(path): problems = [] try: ds = xr.open_dataset(path, mask_and_scale=True) for dim in ("time", "lat", "lon"): if dim not in ds.dims: problems.append(f"missing {dim}") time = ds["time"].values if len(time) > 1 and np.any(np.diff(time) != np.diff(time)[0]): problems.append("time uneven") sst = ds.get("sst") if sst is not None: v = sst.values if np.nanmin(v) < -5 or np.nanmax(v) > 45: problems.append("sst range") if bool(sst.isnull().all()): problems.append("sst all nan") except Exception as exc: problems.append(str(exc)) return path, problems for p in sorted(glob.glob("data_raw/*.nc")): path, problems = check(p) status = "OK " if not problems else "FAIL" print(status, path, "; ".join(problems))脚本里mask_and_scale=True这一步很重要,会把_FillValue变成NaN,值域检查也顺带完成了。阈值按要素物理范围调:SST 用-5~45,海面高度一般用-5~5。输出到控制台后,坏文件会直接标成FAIL,不会混进后面的融合流程。
有一次我处理一批跨年的海温资料,时间轴有两个年份的长度不均,我直接拿去算月平均,出图后 2 月方差大得离谱,查了半天才发现是时间索引错位。后来我把这段巡检放在流程最前端,同类的坑再没踩过。希望帮到你。
本文还有配套的精品资源,点击获取