基于Java的NetCDF文件解析

近期在做的项目中,需要使用Java语言进行NetCDF文件的解析。

然而,当在寻找资料时,发现基于Java语言的资料相较于Python少了很多,而且现有的基于Java解析NetCDF文件到CSV的资料中,功能并不足以满足需要,代码注释也不太清晰,故而最终决定根据各类处理NetCDF文件的产品官方说明文档,自行进行所需功能的编写。

谨以此文,记录个人对于NetCDF文件的理解,和解析过程。

目录

NetCDF文件说明

什么是NetCDF数据?

如何查看NetCDF文件?

图形用户界面

命令行界面

NetCDF文件的数据组成结构

维度 Dimensions

变量 Variables

坐标变量 Coordinate Variavles

属性 Attributes

如何理解NetCDF文件中各数据组成部分?

NetCDF文件解析

解析语言及涉及依赖

解析过程说明

读取nc文件

解析各类变量

写入CSV

涉及到的函数体

其他需注意事项

相关测试结果记录

测试样例一:

测试样例二:

参考资料


NetCDF文件说明

什么是NetCDF数据?

NetCDF文件格式以栅格化形式存储数据,是大气和海洋科学中最常见的数据格式和文件类型,几乎所有天气、气象、海洋模型的数据都以NetCDF文件格式进行存储,卫星数据也常为NetCDF格式。

NetCDF格式文件(全称网络通用数据格式文件(Network Common Data Form))采用二进制格式存储数据,并以数值对的形式对其属性进行自描述。

如何查看NetCDF文件?

图形用户界面

  • Ncview:NetCDF可视化浏览器,允许用户可视化地查看NetCDF数据文件。
  • IDL NetCDF Reader:IDL中的NetCDF浏览器。
  • Panoply:NASA开发的用于查看NetCDF文件的Java应用程序。
  • NcBrowse:提供灵活的交互式图形显示数据及其属性的Java应用程序。

命令行界面

  • Ncdump:NetCDF文件读取器,与Unidata的NetCDF产品捆绑。
  • NetCDF Operators(NCO):操作符程序,其中每个操作符为一个在UNIX命令提示符下执行的独立命令行程序。
  • Climate data operatores(CDO):命令行操作符,用于操作和分析NetCDF数据。

NetCDF文件的数据组成结构

  • 维度 Dimensions

NetCDF维度是用于指定NetCDF文件中包含的一个或多个多维变量形状(shape)的命名整数,可以用来表示真实的物理维度,例如时间、纬度、经度或高度;或者更抽象的数量,如车站或模型运行ID。

每个NetCDF维度都有名称和大小,且区分大小写。

  • 维度名称:以字母开头的包含字母或数字字符(以及下划线字符和连字符)的任意序列。
  • 维度大小:一个正整数,但是netCDF文件中的一个维度的大小可以是UNLIMITED。这种维度称为无限维度或记录维度。具有无限维度的变量可以沿该维度增长到任意长度。
  • 变量 Variables

NetCDF文件中,实际存储数据的是变量。变量的名称、数据类型和形状(shape)由创建变量时指定的维度列表描述。维度的数量就是秩(也称为维),标量变量的秩是0,向量的秩是1,矩阵的秩是2。变量还可以具有关联的属性,可以在创建变量后添加、删除或更改这些属性。

变量的例子有:温度、盐度、氧气等。

  • 坐标变量 Coordinate Variavles

与维度名称相同的一维变量是坐标变量,与一个或多个数据变量的维度相关联,通常定义与该维度对应的物理坐标。二维坐标域将不会被定义为维度。

坐标变量对于NetCDF库没有特殊的意义,但是,使用这个库的软件应该以专门的方式处理坐标变量。

  • 属性 Attributes

NetCDF属性用于存储辅助数据或元数据,提供关于特定变量的信息。

提供关于整个NetCDF文件信息的属性是全局属性,可通过属性名与空白变量名或特殊的空变量ID识别。

注:以上为Introduction to netCDF – Data Carpentry for Oceanographers中的内容,显然,单看解释是很绕的,下面我们来说人话。

如何理解NetCDF文件中各数据组成部分?

下图为NetCDF-Java Online Tutorial | netCDF-Java Documentation中关于坐标的框架图,虽然是与Unidata的产品绑定的,但对于各类数据的理解很有帮助。

从图中,我们可以知道,变量(Variable)是基于坐标轴(Axis)变量的,而坐标轴变量的类型很多,图中列出的有Time、Lat、Lon、Height、Pressure、GeoX、GeoY、GeoZ、RedialAzimuth、RedialElevation、RedialDistance、RunTimely、Ensemble。

在测试样例中,用到的坐标轴变量为Time、Lat、Lon、GeoX、GeoY,在此对这五个变量进行一个说明:

Time:时间,在NetCDF中为time。

Lat:纬度,在NetCDF中为lat。

Lon:经度,在NetCDF中为lon。

GeoX:网格横轴,在NetCDF中为grid_xt。

GeoY:网格横轴,在NetCDF中为grid_yt。

其中GeoX与GeoY都是用来描述网格大小的变量,而Lat和Lon才是真实的物理地址,即经纬度是实际数据所对应的地理坐标,而GeoX和GeoY是数据在图纸上的坐标,同理可以猜测海拔(Height)是与GeoZ相对应的三维坐标系下的z轴。

至于时间纬度,则是一个固定的值或某个范围,可参考Introduction to netCDF – Data Carpentry for Oceanographers中的这张图来理解:

此图所展示的是随时间变化的区域内的温度,故可以这样理解:在这种情况下,温度是唯一含有我们所关心的数据的变量,而经纬度、时间则是坐标变量,每一张表的行列分别为经度纬度,它们各自有固定的时间,随时间变化的区域内温度则表示为一组二维表格。

如果这时,加入了海拔这一坐标变量(如上图所示),则随时间和海拔变化的区域内温度表示为一组三维表格。

以上例子,都只是关于温度这一变量的情况,但实际上的NetCDF文件,通常是由一组涵盖各方面参数的变量组成的,因此,NetCDF文件根据时间、经纬度、海拔,表示为关于多个变量的多个表格。

正是因为其维度的复杂性,当将NetCDF文件解析为CSV文件后,要对所得到的关于各个变量的一组表格进行解读,则需要同时参照时间CSV文件、经度CSV文件、纬度CSV文件及该变量的自身的表格,对格点数据一一对应来解释该变量在所处条件下的值。

举个例子:

time.csv

 lon.csv

lat.csv

dlwrf.csv

以上数据均来源于测试样例一,在此文件中,时间为固定的单值变量,故下面将不再提及time变量。

当我们要解读dlwrf变量的D5格点中的值时,需参照坐标变量进行读取,在上述例子中,即是dlwrf在经度为0.703125,纬度为88.88682时,值为210.4392。

同理,dlwrf变量的F10格点可解释为,在经度为1.171875,纬度为87.71603时,dlwrf值为208.5743。

对于该文件中的其余变量,也以如上方式进行解读即可。

NetCDF文件解析

解析语言及涉及依赖

因为项目要求,因此,在此用Java语言对NetCDF进行解析,根据NetCDF文件的自身特性考虑,将其解析为CSV文件是有助于提高可读性的,下面为将NetCDF文件解析为CSV文件时所涉及到的依赖:

<!-- nc格式解析包 -->
<dependency>
    <groupId>edu.ucar</groupId>
    <artifactId>netcdf4</artifactId>
    <version>4.5.5</version>
</dependency>

<!-- csv操作包 -->
<dependency>
    <groupId>com.opencsv</groupId>
    <artifactId>opencsv</artifactId>
    <version>4.6</version>
</dependency>

解析过程说明

首先,要明确解析过程需要进行哪几个步骤,在此处,由于要将nc文件解析为CSV文件,故应该分为如下几步:

  • 读取nc文件
  • 将nc文件解析为可读的结构化形式(此处为了后续便于写入CSV,将其解析为String数组)
  • 将读取到的数据写入CSV

其中具体要做的各步骤可归纳为如下流程图:

下面将对各步骤进行说明。

读取nc文件

在读取文件时,同时获取其中的所有变量

// NC文件路径
String ncPath = "这里填nc文件的路径";

// 建立nc文件对象
NetcdfFile ncFile = NetcdfFile.open(ncPath);
// 获取所有的变量
List<Variable> variables = ncFile.getVariables();

解析各类变量

根据NetCDF文件中变量的特性,可以分为三类:time、grid_xt、grid_yt等一维变量,经、纬度等不受其他变量影响的以二维表格形式展示的地理位置变量,以及测量所得变量。

因此,对于上述变量,要采用不同的方式进行解析:

  • time、grid_xt、grid_yt变量

这类变量本身就是一维的,因此可以直接读取,不需要进行额外的操作。

    // 读取变量,并储存到String数组中
    Array value = variable.read();
    String[] strings = new String[(int) value.getSize()];
    for (int i = 0; i < value.getSize(); i++) {
        Double temp = value.getDouble(i);
        strings[i] = temp.toString();
    }
  • 经、纬度变量

这类变量虽然表示为二维表格的形式,但本质上还是一维的,但会根据绘制图形的坐标变量grid_xt和grid_yt而进行行列上的重复,故而要对其样式进行整合。

// 读取经度,用grid_xt的大小来控制每行数据量,并将读取到的数据储存到String数组中写入CSV
Array value = variable.read();

long num = ncFile.findVariable("grid_xt").getSize();
String[] strings = new String[(int) num];
for (int i = 0; i < variable.getSize(); i++) {
     Double temp = value.getDouble(i);
    int index = (int) (i % num);
    strings[index] = temp.toString();

    // 整合后写入CSV
    if((i + 1) % num == 0) 
        ; // 此处填写写入csv文件的指令
}

// 读取纬度,用grid_xt的大小来控制每行数据量,并将读取到的数据储存到String数组中写入CSV
Array value = variable.read();

long num = ncFile.findVariable("grid_xt").getSize();
String[] strings = new String[(int) num];
for (int i = 0; i < variable.getSize(); i++) {
    Double temp = value.getDouble(i);
    int index = (int) (i % num);
    strings[index] = temp.toString();

    // 整合后写入CSV
    if((i + 1) % num == 0)
        ; // 此处填写写入csv文件的指令
}
  • 测量所得变量

这类型的变量是nc文件中,我们最为关心的数据,但由于其是根据时间、经度、纬度构建的,因此需要对数组进行降维。

// 读取nc数据到数组
Array data = v.read();

// 获取参数和索引,其中shape的前三个参数分别是时间、纬度、经度
int[] shape = data.getShape();
Index index = data.getIndex();

// 将三维数组降维,并用String数组提取数据
// 按时间
for (int i = 0; i < shape[0]; i++) {
    // 按维度
    for (int j = 0; j < shape[1]; j++) {
        String[] strings = new String[shape[2]];
        // 按经度
        for (int k = 0; k < shape[2]; k++) {
            // 按照对应索引获取数据并转换为string类型添加到数组中
            Float dval = data.getFloat(index.set(i, j, k));
            strings[k] = dval.toString();
        }
        ; // 此处填写写入csv文件的指令
    }
}

写入CSV

在写入CSV时,需要构建CSV,并根据其输出路径和相关参数进行设置,从而按行将数据输入到CSV中。

注:之所以不按列,是因为openCSV中没有按列写入的操作,如果要按列输入,需要自行对二维数组进行转置,随后再按行输入,但不建议这样做,因为nc文件很大,这样的操作相当消耗内存,容易出现内存溢出的错误。

// 创建CSV
FileOutputStream fileOutputStream = new FileOutputStream(outPath);
OutputStreamWriter outputStreamWriter = new OutputStreamWriter(fileOutputStream);
CSVWriter csvWriter = new CSVWriter(outputStreamWriter
        , CSVWriter.DEFAULT_SEPARATOR, CSVWriter.NO_QUOTE_CHARACTER, CSVWriter.DEFAULT_ESCAPE_CHARACTER, CSVWriter.DEFAULT_LINE_END);
        
// 将string数组写入CSV
csvWriter.writeNext(strings);

// 刷新、关闭CSV文件
csvWriter.flush();
csvWriter.close();
outputStreamWriter.close();
fileOutputStream.close();

涉及到的函数体

此处将解析变量以及写入CSV文件的操作写成了相应函数,其代码如下:

// 读取时间、grid_xt、grid_yt的值,并将其存放在一维数组中
public static void writeDimensionCSV(String outPath, Variable variable) throws IOException, InvalidRangeException {

    // 创建CSV
    FileOutputStream fileOutputStream = new FileOutputStream(outPath);
    OutputStreamWriter outputStreamWriter = new OutputStreamWriter(fileOutputStream);
    CSVWriter csvWriter = new CSVWriter(outputStreamWriter
            , CSVWriter.DEFAULT_SEPARATOR, CSVWriter.NO_QUOTE_CHARACTER, CSVWriter.DEFAULT_ESCAPE_CHARACTER, CSVWriter.DEFAULT_LINE_END);

    // 读取维度变量,并储存到String数组中
    Array value = variable.read();
    String[] strings = new String[(int) value.getSize()];
    for (int i = 0; i < value.getSize(); i++) {
        Double temp = value.getDouble(i);
        strings[i] = temp.toString();
    }
    csvWriter.writeNext(strings);

    // 刷新、关闭
    csvWriter.flush();
    csvWriter.close();
    outputStreamWriter.close();
    fileOutputStream.close();

}



// 读取经度的值,并将其存放在一维数组中
public static void writeLonCSV(NetcdfFile ncFile, String outPath, Variable variable) throws IOException, InvalidRangeException {

    // 创建CSV
    FileOutputStream fileOutputStream = new FileOutputStream(outPath);
    OutputStreamWriter outputStreamWriter = new OutputStreamWriter(fileOutputStream);
    CSVWriter csvWriter = new CSVWriter(outputStreamWriter
            , CSVWriter.DEFAULT_SEPARATOR, CSVWriter.NO_QUOTE_CHARACTER, CSVWriter.DEFAULT_ESCAPE_CHARACTER, CSVWriter.DEFAULT_LINE_END);

    // 读取经度,用grid_xt的大小来控制每行数据量,并将读取到的数据储存到String数组中写入CSV
    Array value = variable.read();

    long num = ncFile.findVariable("grid_xt").getSize();
    String[] strings = new String[(int) num];
    for (int i = 0; i < variable.getSize(); i++) {
         Double temp = value.getDouble(i);
        int index = (int) (i % num);
        strings[index] = temp.toString();

        // 整合后写入CSV
        if((i + 1) % num == 0)
            csvWriter.writeNext(strings);
    }


    // 刷新、关闭
    csvWriter.flush();
    csvWriter.close();
    outputStreamWriter.close();
    fileOutputStream.close();

}



// 读取纬度的值,并将其存放在一维数组中
public static void writeLatCSV(NetcdfFile ncFile, String outPath, Variable variable) throws IOException, InvalidRangeException {

    // 创建CSV
    FileOutputStream fileOutputStream = new FileOutputStream(outPath);
    OutputStreamWriter outputStreamWriter = new OutputStreamWriter(fileOutputStream);
    CSVWriter csvWriter = new CSVWriter(outputStreamWriter
            , CSVWriter.DEFAULT_SEPARATOR, CSVWriter.NO_QUOTE_CHARACTER, CSVWriter.DEFAULT_ESCAPE_CHARACTER, CSVWriter.DEFAULT_LINE_END);


    // 读取纬度,用grid_xt的大小来控制每行数据量,并将读取到的数据储存到String数组中写入CSV
    Array value = variable.read();

    long num = ncFile.findVariable("grid_xt").getSize();
    String[] strings = new String[(int) num];
    for (int i = 0; i < variable.getSize(); i++) {
        Double temp = value.getDouble(i);
        int index = (int) (i % num);
        strings[index] = temp.toString();

        // 整合后写入CSV
        if((i + 1) % num == 0)
            csvWriter.writeNext(strings);
    }

    // 刷新、关闭
    csvWriter.flush();
    csvWriter.close();
    outputStreamWriter.close();
    fileOutputStream.close();

}



// 读取变量在任一时间、经度、纬度下的值,并将其存放在二维数组中
public static void writeVariableCSV(String outPath, Variable v) throws IOException, InvalidRangeException {
    // 读取nc数据到数组
    Array data = v.read();

    // 获取参数和索引,其中shape的前三个参数分别是时间、纬度、经度
    int[] shape = data.getShape();
    Index index = data.getIndex();

    // 创建CSV
    FileOutputStream fileOutputStream = new FileOutputStream(outPath);
    OutputStreamWriter outputStreamWriter = new OutputStreamWriter(fileOutputStream);
    CSVWriter csvWriter = new CSVWriter(outputStreamWriter
            , CSVWriter.DEFAULT_SEPARATOR, CSVWriter.NO_QUOTE_CHARACTER, CSVWriter.DEFAULT_ESCAPE_CHARACTER, CSVWriter.DEFAULT_LINE_END);

    // 将三维数组降维,并用String数组提取数据,将其按行导出到CSV文件
    // 按时间
    for (int i = 0; i < shape[0]; i++) {
        // 按维度
        for (int j = 0; j < shape[1]; j++) {
            String[] strings = new String[shape[2]];
            // 按经度
            for (int k = 0; k < shape[2]; k++) {
                // 按照对应索引获取数据并转换为string类型添加到数组中
                Float dval = data.getFloat(index.set(i, j, k));
                strings[k] = dval.toString();
            }
            csvWriter.writeNext(strings);
        }
    }

    // 刷新、关闭
    csvWriter.flush();
    csvWriter.close();
    outputStreamWriter.close();
    fileOutputStream.close();

}

其他需注意事项

由于在NetCDF文件中,变量的前五项是时间、经度、纬度、grid_xt、grid_yt,因而在对变量进行解析导出时,需要根据其索引判断调用能正确解析相应变量的函数。

// 由于变量的前五项是时间、经度、纬度、grid_xt、grid_yt,都不是二维数组,故要用一维数组的方式进行导出
for (int i = 0; i < 5; i++) {
    // 得到该索性对应的变量名以形成导出的CSV文件名称
    String variableName = variables.get(i).getName();
    String outPath = "D:\\test0\\" + variableName + ".csv";

    // 根据时间、经纬度、grid_xt/yt的性质,将其按照相应方式写入CSV
    // 维度变量在nc文件中的顺序为grid_xt、lon、grid_yt、lat、time
    switch (i){
        case 1:
            writeLonCSV(ncFile, outPath, variables.get(i));
            break;
        case 3:
            writeLatCSV(ncFile, outPath, variables.get(i));
            break;
        default:
            writeDimensionCSV(outPath, variables.get(i));
            break;
    }

}

// 由于变量的前五项是时间、经度、维度、grid_xt、grid_yt,都不是二维数组,故此处从第六个变量开始写入CSV
for (int i = 5; i < variables.size(); i++) {
    // 得到该索性对应的变量名以形成导出的CSV文件名称
    String variableName = variables.get(i).getName();
    String outPath = "D:\\test0\\" + variableName + ".csv";

    // 将该变量写入CSV
    writeVariableCSV(outPath, variables.get(i));
}

当然,最后需要记得关闭nc文件。

// 关闭nc文件
ncFile.close();

相关测试结果记录

以下测试数据均来源自美国National Centers for Environmental Information (NCEI)的开源数据,为了简化,文件名进行了删减,读者可自行到网站上去下载所需的nc文件。

测试样例一:

文件名:sfcf003.nc

文件大小:262M

导出用时:19.08s

解析后大小:1.26G

测试样例二:

文件名:sfcf000.nc

文件大小:0.98G

导出用时:77.10s

解析后大小:5.08G

参考资料

Introduction to netCDF – Data Carpentry for Oceanographers

NetCDF-Java Online Tutorial | netCDF-Java Documentatid

National Centers for Environmental Information (NCEI)

  • 5
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 8
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值