年初换了台 Linux 工作站之后,我把以前在 Windows 上折腾的数据处理流程整个搬了过来。绕了一大圈,最后让我彻底留在 Linux 下的原因,不是 Vim 也不是终端,而是 xarray 这套处理多维数据的方式。尤其是当我把文件读取、坐标处理、重采样、批量合并这些高频操作全部封成一个工具类之后,日常处理带维度坐标的数据文件,工作量直线下降。这篇东西不是 xarray 的 API 文档复读,是我自己在 Linux 环境下做数据清洗和预处理的真实项目总结,从设计思路到代码实现再到踩坑记录,一次性讲清楚。
1. 项目整体设计与思路拆解
1.1 为什么非要自己封装一套 xarray 工具类
如果你只处理过单张 CSV,可能觉得 xarray 是杀鸡用牛刀。但一旦你开始接触气象数据、海洋模式输出、遥感反演产品,或者任何带“维度”和“坐标”的科学数据集,就会发现 pandas 的单表结构根本装不下这类数据。一个典型的 NetCDF 文件,里面可能有温度、降水、风场好几个变量,每个变量又共享时间、纬度、经度、高度四五个维度,这种结构用 DataFrame 硬拆,拆完基本就丢了原始坐标语义。
xarray 的设计初衷就是解决这个问题。它提供了 DataArray 和 Dataset 两个核心数据结构,让多维数组自带维度名和坐标值。听起来挺美好,实际用起来却有个问题:光一个数据集读取,可能就要写open_dataset、sel、isel、resample一堆方法,而且每次项目都要重复这些步骤。我统计过自己一个月的工作量,至少有四成时间浪费在“这套数据要用什么方式打开、坐标要不要翻转、缺失值怎么处理、输出成什么格式”这种重复劳动上。
所以我决定写一个 XarrayTools 工具类,把这些“套路化”的能力固化下来。它的目标很简单:把复杂冗长的 xarray 操作封装成一个个语义清晰的业务方法,让调用方专注于数据本身,而不是每天重新解决同样的问题。
1.2 工具类的功能定位与设计原则
这个工具类的定位是“数据处理中间层”,不是万能框架,所以设计上我一直坚持几个原则。第一,方法职责单一,一个方法只做一件事,比如读文件单独一个方法,插值单独一个方法,不搞大而全的“一站式处理”。第二,统一容错和日志,任何异常都要给出人能看懂的报错信息,比如文件路径不对、维度不存在、坐标越界,不能直接扔一个晦涩的 traceback。第三,允许链式调用,把所有返回 Dataset 的方法设计成可以连续操作,提高日常交互效率。
目前这个类主要覆盖几个功能块:数据读取与快速查看、维度坐标调整、缺失值处理、时间重采样、多文件合并、分组统计、滚动计算、结果输出。每个功能块对应的都是一个实际工作中反复出现的高频场景。比如“时间重采样”这个方法,做气象数据处理的人每天都要用,日数据转月数据、小时数据转日数据,翻来覆去就是那几种聚合方式,封装好之后直接一行调用。
1.3 为什么在 Linux 环境下做这件事
Xarray 本身是跨平台的,但我在实际体验后发现,Linux 环境对这套工具类来说几乎是天然主场。原因有几个:一是绝大多数科学计算底层库,比如 netCDF-C、HDF5、OpenMPI,在 Linux 上的二进制支持和性能表现最稳定;二是 conda 的 conda-forge 频道在 Linux 上对 xarray 及其依赖链的兼容性处理得最好,基本不会出现 Windows 上常见的 DLL 缺失问题;三是后续如果要上 dask 分布式或者大规模并行,Linux 服务器几乎是唯一选择。
后面所有环境配置和代码演示,我都是基于一台 Ubuntu 22.04 的机器完成的。你如果是 CentOS/Rocky Linux,命令上的差异主要集中在包管理器,conda 部分的流程是完全一致的。
2. Linux 环境准备与依赖安装
2.1 Python 环境与虚拟环境准备
我强烈建议在 Linux 上不要直接使用系统自带的 Python 来装科学计算库,尤其不要用pip install --system这种硬怼的方式。系统 Python 往往被系统包管理工具锁定,装坏了会影响其他软件。我自己的习惯是先装一个 Miniconda,然后在里面单独创建虚拟环境,把科学计算的所有依赖锁在独立环境里。
wget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh bash Miniconda3-latest-Linux-x86_64.sh安装过程中一路回车加 yes 就行,默认装到$HOME/miniconda3。装完之后记得重开终端或者执行source ~/.bashrc,让 conda 命令生效。然后创建一个专门跑数据处理的环境:
conda create -n xenv python=3.11 -y conda activate xenvPython 版本选 3.11 是因为目前 xarray 和大部分依赖库对 3.11 的兼容性非常好,又没有 3.12 那么激进,属于最稳妥的选择。如果你手里的旧项目还锁在 Python 3.8,也不是不能用,但建议还是尽量往新版本迁移,老版本的 xarray 在性能上优化差很多。
2.2 核心依赖选型与安装
Xarray 本身只是个“壳”,真正干活的是它屁股后面那一堆底层库。我日常这个工具类用到的最核心依赖是这些:
- xarray:核心数据结构与操作方法
- numpy、pandas:底层数组与表格数据支撑
- netCDF4、h5netcdf:NetCDF 格式读写后端
- dask:分块计算与内存优化
- bottleneck:加速 nan-aware 统计计算
- matplotlib、cartopy:结果可视化
- zarr:高效云原生产出格式
安装建议直接用 conda-forge 频道,一次性装齐:
conda install -c conda-forge xarray dask netCDF4 h5netcdf bottleneck matplotlib cartopy zarr -y为什么不直接pip install xarray?因为 netCDF4 和 cartopy 这两个包在 pip 下需要编译或下载预编译轮子,如果你系统里缺 libnetcdf、GEOS 之类的基础库,很容易踩编译坑。conda-forge 把底层依赖都处理好了,装完就能用。这也是我在 Linux 上最推荐的方式。
2.3 快速验证环境是否正常
装完之后不要急着开发,先跑一段最简单的代码验证 xarray 能否正常读取和创建数据:
import xarray as xr import numpy as np print("xarray version:", xr.__version__) # 创建一个简单的 DataArray data = np.random.rand(10, 20) da = xr.DataArray( data, dims=["time", "station"], coords={"time": np.arange(10), "station": np.arange(20)}, ) print(da)如果你能看到 DataArray 的结构信息,包括维度、坐标和变量内容,说明核心链路已经通了。为了保险,我还会额外执行一个命令,确认 NetCDF 后端可用:
ds = xr.Dataset({"a": (("x",), [1, 2, 3])}) ds.to_netcdf("/tmp/test_xr.nc") print(xr.open_dataset("/tmp/test_xr.nc"))如果这两段都能顺利通过,环境就算彻底准备好了。
3. 核心实现:XarrayTools 工具类
3.1 类结构与初始化
整个工具类的代码风格我尽量保持朴素,不引入过多设计模式,类就是一个大容器,按功能块组织方法。初始化的时候传入数据根目录和一个可选的日志文件路径,方便定位问题。
import xarray as xr import numpy as np import pandas as pd import logging from pathlib import Path class XarrayTools: def __init__(self, data_dir: str = "./", log_file: str = None): self.data_dir = Path(data_dir) self.logger = self._init_logger(log_file) def _init_logger(self, log_file): logger = logging.getLogger("XarrayTools") logger.setLevel(logging.INFO) fmt = logging.Formatter("%(asctime)s - %(levelname)s - %(message)s") if log_file: fh = logging.FileHandler(log_file) fh.setFormatter(fmt) logger.addHandler(fh) ch = logging.StreamHandler() ch.setFormatter(fmt) logger.addHandler(ch) return logger日志这里我加了双输出,既打到控制台又落到文件。在 Linux 服务器上跑批处理任务,如果每个步骤都有完整日志,排查问题会轻松很多。这个设计看起来不起眼,实际使用中帮我省过不少时间。
3.2 数据读取与快速查看
数据读取是最基础也最高频的操作。我的open_data方法支持单文件和多文件两种情况,多文件时直接走open_mfdataset自动合并,同时可以通过参数控制是否延迟加载。
def open_data(self, file_pattern: str, engine: str = "netcdf4", chunks: dict = None, combine: str = "by_coords", **kwargs): """打开单个或多个 NetCDF 文件 Args: file_pattern: 文件路径或通配符,比如 data/*.nc engine: 后端引擎,默认 netcdf4 chunks: 分块字典,如 {"time": 10} combine: 多文件合并方式 """ try: files = sorted(self.data_dir.glob(file_pattern)) if not files: raise FileNotFoundError(f"未找到匹配文件: {file_pattern}") if len(files) == 1: ds = xr.open_dataset(files[0], engine=engine, chunks=chunks, **kwargs) else: ds = xr.open_mfdataset(files, engine=engine, chunks=chunks, combine=combine, **kwargs) self.logger.info("成功打开 %d 个文件", len(files)) return ds except Exception as e: self.logger.error("打开数据失败: %s", e) raise这里有个关键参数chunks,如果传入{"time": 10},xarray 会生成一个 dask 数组的 Dataset,文件数据不会一次性全部读入内存,而是按块延迟加载。几十 GB 的批量数据,不传这个参数根本跑不动。
快速查看数据也值得封装,因为我发现很多人拿到数据集第一反应是print(ds),数据一多整个终端就刷屏。我的quick_look方法做的是摘要式展示,只输出维度大小、变量列表、时间范围和缺失值概况:
def quick_look(self, ds: xr.Dataset): """快速查看数据集的核心信息""" self.logger.info("维度信息:") for dim, size in ds.sizes.items(): self.logger.info(" %s: %d", dim, size) self.logger.info("变量列表:") for var in ds.data_vars: self.logger.info(" %s", var) if "time" in ds.coords: self.logger.info("时间范围: %s ~ %s", str(ds["time"].values.min()), str(ds["time"].values.max())) total_missing = int(ds.isnull().sum().sum()) self.logger.info("缺失值总数: %d", total_missing)3.3 维度与坐标调整
坐标系和维度名处理是 xarray 使用中比较绕的部分。不同来源的数据风格差异很大,有的用latitude、longitude,有的用lat、lon,还有的把维度顺序搞反,必须统一。我在工具类里封装了rename_coords和exchange_dims两个方法:
def rename_coords(self, ds: xr.Dataset, name_map: dict) -> xr.Dataset: """统一维度坐标名称,比如 latitude -> lat""" ds = ds.rename(name_map) self.logger.info("重命名坐标: %s", name_map) return ds def exchange_dims(self, ds: xr.Dataset, *dim_order) -> xr.Dataset: """调整维度顺序,便于后续计算和绘图""" ds = ds.transpose(*dim_order) self.logger.info("调整维度顺序为: %s", dim_order) return ds坐标重命名看着简单,但实际操作里特别容易踩坑,因为rename会把匹配到的变量名也一并改掉。比如你把latitude重命名为lat之后,数据里不可变坐标会跟着变,这没问题;但如果某个变量名也叫latitude,它也会被连带修改,需要留意。
3.4 缺失值处理与插值
真实数据几乎不可能没有缺失值,有的表现为 NaN,有的表现为_FillValue,有的直接是离谱的数值,比如 -9999。我封装了一个fill_missing方法,统一处理这些情况,并提供多种填充策略:
def fill_missing(self, ds: xr.Dataset, var_names: list = None, fill_value: float = None, method: str = "linear"): """填充缺失值 Args: ds: 输入数据集 var_names: 需要填充的变量列表,默认所有 fill_value: 若指定,则用该值直接填充 method: 插值方法,linear/nearest/zero """ vars_to_fill = var_names or list(ds.data_vars) # 如果存在 _FillValue 属性,先转为 NaN for var in vars_to_fill: if var in ds and "_FillValue" in ds[var].attrs: fill_attr = ds[var].attrs.pop("_FillValue") ds[var] = ds[var].where(ds[var] != fill_attr) for var in vars_to_fill: if var not in ds: self.logger.warning("变量 %s 不存在,跳过", var) continue if fill_value is not None: ds[var] = ds[var].fillna(fill_value) self.logger.info("变量 %s 使用常数值 %s 填充", var, fill_value) else: # 按时间维度线性插值 ds[var] = ds[var].interpolate_na(dim="time", method=method) self.logger.info("变量 %s 使用 %s 方法在时间维插值", var, method) return ds很多文件里的“缺失值”不是 NaN 而是掩码值,比如海洋模式输出里陆地格点可能是 -32767。直接在fillna之前先做一次掩码转 NaN,是非常关键的一步,少了它后面所有统计都会出错。
3.5 时间重采样与聚合
时间维的聚合是我个人用 xarray 用的最多的功能。日数据转月平均、小时数据转日累计,云数据处理里每天都在干。这个resample_time方法我做了较大扩展,支持均值、求和、最大最小等常见聚合方式:
def resample_time(self, ds: xr.Dataset, freq: str = "M", agg: str = "mean", var_names: list = None): """时间维重采样 Args: ds: 输入数据集 freq: 重采样频率,M=月,D=日,Y=年,Q=季度 agg: 聚合方式,mean/sum/max/min/std """ if "time" not in ds.coords: raise ValueError("数据集缺少 time 坐标,无法重采样") vars_to_agg = var_names or list(ds.data_vars) resampled = ds[vars_to_agg].resample(time=freq) if agg == "mean": out = resampled.mean() elif agg == "sum": out = resampled.sum() elif agg == "max": out = resampled.max() elif agg == "min": out = resampled.min() elif agg == "std": out = resampled.std() else: raise ValueError(f"不支持的聚合方式: {agg}") self.logger.info("时间维重采样为 %s,聚合方式 %s", freq, agg) return out这里有个容易被忽略的细节:resample之后的输出会丢掉非维度坐标以外的属性信息,比如单位、长名称,导致下游脚本拿不到变量属性。我一般会在返回前重新把常用属性写回去,或者让调用方知道这个行为。
3.6 多文件合并与分组统计
产品数据经常按时间或者区域拆成很多小文件,比如一天一个 NC。open_mfdataset已经能处理按维度拼接的场景,但实际工作中我遇到更多的是文件之间维度不一致、坐标顺序不一致的情况,需要在合并前做预处理。我在工具类里提供了preprocess参数入口:
def _standardize_coords(self, ds: xr.Dataset) -> xr.Dataset: """合并前的标准化处理""" coords_rename_map = { "latitude": "lat", "longitude": "lon", "levels": "lev", } ds = ds.rename(coords_rename_map) if "lat" in ds.coords and ds["lat"].values[0] > ds["lat"].values[-1]: ds = ds.isel(lat=slice(None, None, -1)) return ds def open_many_with_standardize(self, file_pattern: str, **kwargs): """读取多文件前先统一坐标""" files = sorted(self.data_dir.glob(file_pattern)) if not files: raise FileNotFoundError(f"未找到匹配文件: {file_pattern}") ds = xr.open_mfdataset( files, combine="by_coords", preprocess=self._standardize_coords, **kwargs ) self.logger.info("多文件合并完成,共 %d 个文件", len(files)) return ds纬度降到升的检测是这里最有实用价值的一段逻辑。很多模式的经纬度是从北往南排的,比如 90 到 -90,如果你不做翻转,后面画图时地图整体上下颠倒,区域裁剪的结果也会完全反过来。这种问题排查起来特别隐蔽,我第一次遇到时浪费了整整一下午。
分组统计同样高频,比如算逐站点多年平均、逐季节气候态。xarray 的groupby可以按时间标签分组,我封装成:
def groupby_climatology(self, ds: xr.Dataset, freq: str = "month", agg: str = "mean", var_names: list = None): """按时间分组计算气候态 Args: freq: month/year/season agg: mean/std/sum """ time_dim = ds["time"] if freq == "month": group = time_dim.dt.month elif freq == "year": group = time_dim.dt.year elif freq == "season": group = time_dim.dt.season else: raise ValueError("freq 只支持 month/year/season") vars_to_agg = var_names or list(ds.data_vars) grouped = ds[vars_to_agg].groupby(group) out = getattr(grouped, agg)() self.logger.info("按 %s 分组,聚合方式 %s 完成", freq, agg) return out3.7 结果保存与输出
处理完的数据最终要落盘,我封装了write_output方法,支持 NetCDF 和 Zarr 两种格式。NetCDF 适合常规交换,Zarr 更适合大数据和云环境,因为它天然分块且支持并发写。
def write_output(self, ds: xr.Dataset, save_path: str, format: str = "netcdf", **kwargs): """保存结果数据 Args: ds: 输入数据集 save_path: 输出路径 format: netcdf 或 zarr """ save_path = Path(save_path) if format == "netcdf": save_path.parent.mkdir(parents=True, exist_ok=True) ds.to_netcdf(save_path, engine="netcdf4", **kwargs) elif format == "zarr": save_path.parent.mkdir(parents=True, exist_ok=True) ds.to_zarr(save_path, mode="w", **kwargs) else: raise ValueError("format 只能为 netcdf 或 zarr") self.logger.info("数据已保存到 %s (%s)", save_path, format)4. 实操案例:批量处理日尺度数据并做月平均
理论说再多不如跑一个完整例子。假设我有一批 2023 年全年的逐日数据,目录结构是data/2023/*.nc,每个文件是当天的全球温度场,坐标是lat和lon,变量是temperature。我希望能快速得到两个结果:一是全年的月平均温度场,二是每个网格点相对于全年平均的逐月距平。
使用工具类的完整流程可以压缩成下面这段脚本:
from xarray_tools import XarrayTools tools = XarrayTools(data_dir="./data/2023", log_file="process.log") # 1. 读取全年数据,按时间分块减少内存占用 ds = tools.open_data("*.nc", chunks={"time": 30}) # 2. 快速查看数据结构 tools.quick_look(ds) # 3. 统一坐标并翻转纬度(如果需要) ds = tools.rename_coords(ds, {"latitude": "lat", "longitude": "lon"}) if ds["lat"].values[0] > ds["lat"].values[-1]: ds = ds.isel(lat=slice(None, None, -1)) # 4. 填充缺失值(这里使用线性插值) ds = tools.fill_missing(ds, var_names=["temperature"], method="linear") # 5. 月平均 monthly = tools.resample_time(ds, freq="M", agg="mean", var_names=["temperature"]) # 6. 全年平均 annual_mean = monthly.mean(dim="time", keep_attrs=True) # 7. 逐月距平 anomaly = monthly - annual_mean # 8. 保存结果 tools.write_output(monthly, "result/monthly_temperature.nc") tools.write_output(anomaly, "result/temperature_anomaly.nc")这段脚本里,第 3 步的纬度翻转很关键,我在真实数据里遇到过多次,原因就是有人生成的 NC 文件纬度坐标是从北到南排列的。如果你不做翻转,后面用sel(lat=slice(20, 40))做区域裁剪时,返回的维度顺序和范围会完全错乱,跟预期完全对不上。
第 6 步我特意加了keep_attrs=True,否则计算完均值后,变量的单位和长名称等元数据会被丢掉,输出文件给同事看的时候会少很多信息。这是个非常容易忽略的小细节,但专业的数据处理工具里必须考虑。
跑完这份脚本,你会得到一个monthly_temperature.nc和一个temperature_anomaly.nc,前者 12 个时间步,后者也 12 个时间步,坐标完整、属性完整,可以直接交给下游画图或者量化分析。
如果想进一步做可视化,可以继续在工具类上叠加一个绘图方法:
import matplotlib.pyplot as plt import cartopy.crs as ccrs def plot_field(self, da, title=None, cmap="RdYlBu_r", save_path=None): fig = plt.figure(figsize=(12, 6)) ax = plt.axes(projection=ccrs.PlateCarree()) da.plot(ax=ax, transform=ccrs.PlateCarree(), cmap=cmap) ax.coastlines() if title: ax.set_title(title) if save_path: plt.savefig(save_path, dpi=150, bbox_inches="tight") plt.close(fig)把这个方法加进工具类后,直接调用plot_field(monthly.isel(time=0))就能画出第一月的全球温度场。可视化这部分我通常单独封装,避免让工具类过于臃肿,但核心逻辑完全一致。
5. 常见问题与排查技巧实录
工具类写完之后,我在使用和给同事配置环境的工程中积累了整整一页错误排查清单,下面这些都是真实出现过的典型问题。
5.1 ImportError 底层库缺失
最常见的报错长这样:
ImportError: libnetcdf.so.13: cannot open shared object file: No such file or directory原因基本就是 netCDF-C 系统库缺失或版本不匹配。Linux 下建议不要手动去系统里装 libnetcdf-dev,最好直接用 conda 环境自带的库。如果已经踩了这个坑,最简单的解决办法是:
conda install -c conda-forge netcdf4 hdf5 libnetcdf重新安装后 conda 会帮你把依赖库对齐,一般就能解决。Windows 上有时候还要折腾 PATH 环境变量,Linux 下基本不需要。
5.2 内存爆炸:读大文件直接卡死
很多人第一次用 xarray 读取几十 GB 的数据,直接默认参数一顿操作,然后内存被打满,系统直接卡死。问题出在没有分块读取。解决方法是打开文件时传入chunks参数:
ds = xr.open_dataset("large.nc", chunks={"time": 10})这样 xarray 内部会使用 dask 数组延迟计算。注意chunks参数在open_mfdataset里同样适用,多文件场景常用chunks={"time": 1},每个时间步一个块。块大小选择有讲究:太大内存控制不住,太小则计算调度开销暴涨。我的经验是单块数据量控制在 100MB 到 500MB 之间比较合理。
5.3 坐标对齐错误:运算结果全变 NaN
xarray 在做运算时会对齐坐标,比如两个数据集纬度坐标不一致,哪怕只差 0.001 度,运算结果也可能全是 NaN。很多人遇到这种情况第一反应是数据坏了,其实是坐标没对齐。排查方法很简单:
print(ds1["lat"].values[:5]) print(ds2["lat"].values[:5])如果发现坐标确实有不一致,可以用interp做插值统一坐标:
ds2 = ds2.interp(lat=ds1["lat"], lon=ds1["lon"])5.4 _FillValue 还在导致统计值异常
我第一次处理 GRIB 转换来的 NetCDF 文件时,怎么算都感觉平均值偏大,后来才发现陆地点全是 -32767 这种掩码值,虽然显示为 NaN,但底层属性里还是带着_FillValue,后面的聚合没把它过滤掉。我的工具类里已经在填充前统一做了转换,但你如果自己写脚本,一定要记住下面这段逻辑:
for var in ds.data_vars: if "_FillValue" in ds[var].attrs: fill_value = ds[var].attrs["_FillValue"] ds[var] = ds[var].where(ds[var] != fill_value)5.5 时间编码与乱码问题
有些 NC 文件的时间坐标是字符型或者非标准单位,比如hours since 1900-01-01,xarray 有时会识别不了,导致resample直接报错。这时候可以强制转换时间坐标:
ds["time"] = xr.decode_cf(ds)热词里提到“Linux 解压文件乱码”的问题,这跟 xarray 不直接相关,但在数据处理里也有类似坑:如果文件名或变量名含有中文,且系统 locale 没设好,xarray 读取文件时同样可能出现编码问题。Linux 下建议统一设置 UTF-8 编码环境:
export LANG=en_US.UTF-8 export LC_ALL=en_US.UTF-8如果你收到的是 Windows 压缩包传来的数据文件,解压乱码可以先通过unzip -O GBK file.zip指定编码解决,避免文件名乱码传递到 xarray 里。
5.6 dask 分布式调度缓慢
单机跑open_mfdataset时,默认使用的 dask 是线程调度。有时候线程数设得过多,反而因为 GIL 和 IO 竞争变慢。我的经验是,如果数据处理以 IO 为主,可以适当调低线程数;如果以计算为主,比如做大量的逐网格点回归,再提升并行度。可以通过修改 dask 配置来控制:
import dask dask.config.set({"array.chunk-size": "256MiB", "scheduler": "threads"})6. 性能优化与扩展方向
工具类基本成型之后,我又花了一些时间做性能优化。首先是分块策略,我专门写了一个方法来自动估算建议的 chunk 大小,保证读入数据时不会因为 block 太大而发生内存抖动,也不会因为 block 太小导致调度开销过大。
def suggest_chunks(self, ds: xr.Dataset, target_mb: float = 256): """根据目标内存估算推荐 chunk""" size_info = {} for var in ds.data_vars: var_size = ds[var].nbytes / 1024 / 1024 # MB chunks = max(1, int(target_mb / max(var_size, 1e-6))) size_info[var] = chunks self.logger.info("变量 %s 原始大小 %.2f MB, 建议 chunk: %d", var, var_size, chunks) return size_info其次是输出格式,如果下游还要反复读取和切片,NetCDF 一次性全量写出可能不够高效。Zarr 格式的优势在这里体现得很明显,它天然分块存储,可以快速按需读取某一部分,不需要加载整个文件。如果你的数据量级达到几十 GB,我强烈建议用to_zarr替代to_netcdf。
还有一个我一直在做的扩展方向是把工具类改造成命令行接口,方便直接在终端调用。比如:
python xarray_tools.py --action monthly_mean --input data/2023/*.nc --output result.nc这样团队里不熟悉 Python 的人也能用上这套工具,不用每天来找你帮忙跑数据。把方法封装成click命令后,配合 Linux crontab 还能实现定时任务,数据到了自动处理,省掉手动调度。
最后分享一点我自己的体会
从最初只是写几个 xarray 脚本,到最终整理成这套工具类,最大的感受是:所谓“工具类”并不是把一堆方法随便堆到一起,而是逼着你去提炼重复劳动里的共同模式。我在开始封装之前,把自己一个月的数据处理操作全部过了一遍,标记出哪些步骤是每周都会重复的,才确定下第一版的功能清单。后面每次遇到新需求,我会先判断它是不是一个通用场景,是就加进工具类,不是就写成一次性脚本,保持工具类的简洁和稳定。
另外一个小建议,工具类这种东西一定要写文档,哪怕只是简单的 docstring。我见过太多人写完工具类就“完工”,三个月后自己都忘了方法参数是什么意思。我的习惯是每个方法至少写清楚输入输出和异常触发条件,这样即使同事接手也只需要看几分钟就能上手。Xarray 本身的学习曲线不算陡,但把日常操作工程化、工具类化,才是真正让工作效率产生质变的那一步。