高效处理NetCDF文件经纬度转换:一个纯CDO驱动的Bash脚本详解
前言
在气候科学、海洋学和地球系统建模领域,NetCDF(Network Common Data Form)文件是存储多维科学数据的标准格式。然而,这些文件常常面临经纬度范围不一致的问题——有些数据使用[-180, 180]的地理标准范围,而另一些则采用[0, 360]的气象标准,甚至偶尔出现跨越日期变更线或特殊范围(如20.5-379.5)的异常。这不仅会干扰数据可视化和分析,还可能导致模型输入错误。作为一名从事地理信息系统和数据处理的从业者,我经常遇到这类挑战,因此开发了一个灵活的Bash脚本,利用强大的CDO(Climate Data Operators)工具来自动化处理这些问题。
这个脚本支持单文件和批量模式,能够轻松实现经度范围转换、纬度顺序反转,并自动检测特殊情况,如跨越日期变更线的数据。它还集成了并行处理、日志记录和进度显示等实用功能,帮助用户高效管理大规模数据集。在这篇博客中,我将详细介绍脚本的配置、使用方法、核心代码逻辑,以及一些实际应用案例和优化建议。无论你是科研工作者、数据分析师,还是初学者,这款脚本都能显著简化你的工作流程。让我们一起来探索如何让NetCDF数据处理变得更智能、更可靠!
脚本
#!/bin/bash# ================================================================
# NC文件经纬度转换脚本
# 纯CDO版本 v3.0
# ================================================================
# 功能说明:
# 1. 支持单文件和批量处理模式
# 2. 支持经度范围转换, -180-180和0-360之间转换(包括20.5-379.5等特殊范围)
# 3. 支持纬度反转
# 4. 自动检测并处理跨越日期变更线的数据
# ================================================================# ┌──────────────────────────────────────────────────────────────┐
# │ 参数配置区 │
# │ 请根据需求修改以下参数,然后运行脚本 │
# └──────────────────────────────────────────────────────────────┘# ══════════════════════════════════════════════════════════════════════
# 1. 基本路径和模式配置
# ══════════════════════════════════════════════════════════════════════# INPUT_PATH - 输入路径(必填)| 单文件路径 | 目录路径
# 说明:要处理的NC文件路径或包含NC文件的目录路径
INPUT_PATH="test" # 替换成自己的路径# OUTPUT_PATH - 输出路径(选填)| 留空自动生成 | 指定文件/目录路径
# 说明:留空时单文件模式生成"原名_xxx.nc",批量模式创建"输入目录_processed"
OUTPUT_PATH="test_processed" #替换成自己的路径# PROCESS_MODE - 处理模式 | auto(自动检测) | single(单文件) | batch(批量)
# 说明:auto根据INPUT_PATH自动判断,推荐使用
PROCESS_MODE="auto"# ══════════════════════════════════════════════════════════════════════
# 2. 经纬度处理配置
# ══════════════════════════════════════════════════════════════════════# PROCESS_LONGITUDE - 经度处理 | yes(启用) | no(禁用)
# 说明:是否对经度进行范围转换,处理跨日期变更线等问题
PROCESS_LONGITUDE="yes"# LONGITUDE_TARGET - 目标范围 | [-180,180](地理标准) | [0,360](气象标准)
# 说明:[-180,180]西经为负值,[0,360]所有经度为正值,会自动处理>360度的特殊范围
LONGITUDE_TARGET="[0,360]" #"[-180,180]"# LONGITUDE_METHOD - 处理方法 | auto(自动) | sellonlatbox | shiftlon | expr
# 说明:auto自动选择最佳方法,expr适合特殊范围(如20.5-379.5)
LONGITUDE_METHOD="auto"# SPECIAL_LON_HANDLING - 特殊经度处理(>360度) | yes(启用) | no(禁用)
# 说明:自动检测并处理超过360度的经度范围
SPECIAL_LON_HANDLING="yes"# PROCESS_LATITUDE - 纬度处理(反转顺序) | yes(启用) | no(禁用)
# 说明:反转纬度顺序(北→南 ↔ 南→北),用于某些模型输出格式转换
PROCESS_LATITUDE="yes"# ══════════════════════════════════════════════════════════════════════
# 3. 文件处理选项
# ══════════════════════════════════════════════════════════════════════# FILE_SUFFIX - 输出文件后缀 | 任意字符串
# 说明:自定义后缀,会自动添加处理类型标识(如_lon-180-180)
FILE_SUFFIX="processed"# OVERWRITE_EXISTING - 覆盖已存在文件 | yes(覆盖) | no(跳过) | backup(备份后覆盖)
# 说明:backup会创建.bak备份文件
OVERWRITE_EXISTING="no"# SKIP_PATTERN - 跳过文件模式 | 留空不跳过 | *test* | *_processed* 等通配符
# 说明:批量处理时跳过匹配此模式的文件,支持*?[]通配符
SKIP_PATTERN="*_processed*"# CHECK_OUTPUT - 检查输出文件完整性 | yes(检查) | no(不检查)
# 说明:处理后验证输出文件是否正确生成且可读
CHECK_OUTPUT="yes"# KEEP_TEMP_FILES - 保留临时文件(调试用) | yes(保留) | no(删除)
# 说明:调试时可保留中间文件,正常使用建议删除
KEEP_TEMP_FILES="no"# ══════════════════════════════════════════════════════════════════════
# 4. 性能和日志配置
# ══════════════════════════════════════════════════════════════════════# PARALLEL_PROCESSING - 并行处理(需GNU parallel) | yes(启用) | no(串行)
# 说明:批量模式下同时处理多个文件,需要先安装parallel工具
PARALLEL_PROCESSING="yes"# MAX_PARALLEL_JOBS - 最大并行数 | auto(自动) | 4 | 8 | 16 等正整数
# 说明:建议设为CPU核心数的50-75%,auto根据CPU自动决定, 但是使用auto的时候会报错。
MAX_PARALLEL_JOBS="4"# CDO_OPTIONS - CDO选项 | -s(静默) | -v(详细) | -O(覆盖) | -z zip_9(压缩)
# 说明:可组合使用,如"-s -z zip_6"表示静默+压缩
CDO_OPTIONS="-v"# LOG_LEVEL - 日志级别 | ERROR | WARN | INFO(推荐) | DEBUG
# 说明:INFO显示主要处理信息,DEBUG显示详细调试信息
LOG_LEVEL="INFO"# LOG_FILE - 日志文件 | auto(自动生成) | 指定路径 | 留空禁用
# 说明:auto生成带时间戳的日志文件,留空则不保存日志
LOG_FILE="auto"# SHOW_PROGRESS - 显示进度 | yes(显示) | no(不显示)
# 说明:批量处理时显示进度条和百分比
SHOW_PROGRESS="yes"# DRY_RUN - 演练模式(仅显示操作) | yes(演练) | no(正常执行)
# 说明:演练模式只显示将要执行的操作,不实际处理文件
DRY_RUN="no"# ┌──────────────────────────────────────────────────────────────┐
# │ 参数配置结束 │
# │ 以下为脚本执行代码 │
# │ 请勿修改! │
# └──────────────────────────────────────────────────────────────┘# ================================================================
# 脚本执行部分
# ================================================================# 颜色定义
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
CYAN='\033[0;36m'
MAGENTA='\033[0;35m'
NC='\033[0m'# 全局变量
SCRIPT_NAME=$(basename "$0")
SCRIPT_DIR=$(dirname "$(readlink -f "$0")")
START_TIME=$(date +%s)
PROCESSED_COUNT=0
FAILED_COUNT=0
SKIPPED_COUNT=0# 生成日志文件名
if [ "$LOG_FILE" = "auto" ]; then# 尝试在当前目录创建日志文件,如果失败则使用临时目录LOG_FILE="nc_process_$(date +%Y%m%d_%H%M%S).log"if ! touch "$LOG_FILE" 2>/dev/null; thenLOG_FILE="/tmp/nc_process_$(date +%Y%m%d_%H%M%S).log"if ! touch "$LOG_FILE" 2>/dev/null; thenLOG_FILE="/dev/null"echo "警告: 无法创建日志文件,日志将被禁用"fifi
elif [ -z "$LOG_FILE" ]; thenLOG_FILE="/dev/null"
fi# ========================
# 日志函数
# ========================
log() {local level=$1shiftlocal message="$*"local timestamp=$(date '+%Y-%m-%d %H:%M:%S')case $level inERROR)[ "$LOG_LEVEL" != "NONE" ] && echo -e "${RED}[ERROR]${NC} $message" >&2[ "$LOG_FILE" != "/dev/null" ] && echo "[$timestamp] [ERROR] $message" >> "$LOG_FILE" 2>/dev/null;;WARN)if [[ "$LOG_LEVEL" =~ ^(WARN|INFO|DEBUG)$ ]]; thenecho -e "${YELLOW}[WARN]${NC} $message"fi[ "$LOG_FILE" != "/dev/null" ] && echo "[$timestamp] [WARN] $message" >> "$LOG_FILE" 2>/dev/null;;INFO)if [[ "$LOG_LEVEL" =~ ^(INFO|DEBUG)$ ]]; thenecho -e "${GREEN}[INFO]${NC} $message"fi[ "$LOG_FILE" != "/dev/null" ] && echo "[$timestamp] [INFO] $message" >> "$LOG_FILE" 2>/dev/null;;DEBUG)if [ "$LOG_LEVEL" = "DEBUG" ]; thenecho -e "${CYAN}[DEBUG]${NC} $message"fi[ "$LOG_FILE" != "/dev/null" ] && echo "[$timestamp] [DEBUG] $message" >> "$LOG_FILE" 2>/dev/null;;*)echo "$message"[ "$LOG_FILE" != "/dev/null" ] && echo "[$timestamp] $message" >>