conda中使用ecCodes不支持JPEG解码的问题查错
本文是对笔者近期遇到的一次 Python + ecCodes 解码出错问题的总结。
背景
最近在 CMADaaS 容器中测试处理 GRIB2 数据的 Python 脚本,使用 CMA 镜像安装 Anaconda 环境和软件包,运行后报错,提示 ecCodes 不支持 JPEG 解码。 报错信息如下:
>>> import eccodes
>>> f = open("Z_NAFP_C_BABJ_20230409180000_P_NWPC-GRAPES-GFS-HNEHE-12000.grib2", "rb")
>>> m = eccodes.codes_grib_new_from_file(f)
>>> eccodes.codes_get_double_array(m, "values")
ECCODES ERROR : JPEG support not enabled.
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/space/cmadaas/dpl/CEMC_OPER_PROD/anaconda3/envs/py311-plot/lib/python3.11/site-packages/gribapi/gribapi.py", line 1196, in grib_get_double_array
GRIB_CHECK(err)
File "/space/cmadaas/dpl/CEMC_OPER_PROD/anaconda3/envs/py311-plot/lib/python3.11/site-packages/gribapi/gribapi.py", line 230, in GRIB_CHECK
errors.raise_grib_error(errid)
File "/space/cmadaas/dpl/CEMC_OPER_PROD/anaconda3/envs/py311-plot/lib/python3.11/site-packages/gribapi/errors.py", line 382, in raise_grib_error
raise ERROR_MAP[errid](errid)
gribapi.errors.FunctionalityNotEnabledError: Functionality not enabled
原因
报错原因是在该容器在 /usr/local
目录下安装了没有启用JPEG支持的 eccodes 库。
检查 /usr/local/bin/grib_ls
的依赖库
$ ldd ./grib_ls
linux-vdso.so.1 => (0x00007ffd999ef000)
libeccodes.so => /usr/local/bin/./../lib64/libeccodes.so (0x00007f9a074c3000)
libm.so.6 => /lib64/libm.so.6 (0x00007f9a071c1000)
libc.so.6 => /lib64/libc.so.6 (0x00007f9a06df4000)
libpthread.so.0 => /lib64/libpthread.so.0 (0x00007f9a06bd8000)
/lib64/ld-linux-x86-64.so.2 (0x00007f9a07989000)
没有 JPEG 的依赖库。
检查 conda 环境中安装的 /anaconda3/envs/py311-plot/bin/grib_ls
的依赖库
$ ldd ./grib_ls
linux-vdso.so.1 => (0x00007fff48cd6000)
libeccodes.so => /anaconda3/envs/py311-plot/bin/./../lib/libeccodes.so (0x00007f4caf931000)
libc.so.6 => /lib64/libc.so.6 (0x00007f4caf564000)
libm.so.6 => /lib64/libm.so.6 (0x00007f4caf262000)
libjasper.so.4 => /anaconda3/envs/py311-plot/bin/./../lib/./libjasper.so.4 (0x00007f4cafca6000)
libpthread.so.0 => /lib64/libpthread.so.0 (0x00007f4caf046000)
libaec.so.0 => /anaconda3/envs/py311-plot/bin/./../lib/./libaec.so.0 (0x00007f4cafc9c000)
libpng16.so.16 => /anaconda3/envs/py311-plot/bin/./../lib/./libpng16.so.16 (0x00007f4cafc5e000)
/lib64/ld-linux-x86-64.so.2 (0x00007f4cafc08000)
libjpeg.so.9 => /anaconda3/envs/py311-plot/bin/./../lib/././libjpeg.so.9 (0x00007f4caf007000)
libz.so.1 => /anaconda3/envs/py311-plot/bin/./../lib/././libz.so.1 (0x00007f4cafc43000)
可以看到 conda 安装的 eccodes 使用了 libjasper 库支持 JPEG 解码。
以上信息可以推测出 eccodes python API 首先定位到 /usr/local 下面的 eccodes,而没有使用 conda 环境中的库。
下面就更进一步分析为什么 eccodes python API 没有直接使用 conda 安装的自带库。
深入分析
浏览 eccodes python 的源代码,在 gribapi/bindings.py
中发现如下代码:
try:
import ecmwflibs as findlibs
except ImportError:
import findlibs
library_path = findlibs.find("eccodes")
if library_path is None:
raise RuntimeError("Cannot find the ecCodes library")
由此可知 eccodes 接口使用 findlibs 查找 eccodes 的库文件。 findlibs 也是 ecmwf 开发的开源库。
直接执行上述代码:
>>> import findlibs
>>> findlibs.find("eccodes")
'/usr/local/lib64/libeccodes.so'
发现 eccodes python 库确实首先找到了 /usr/local 下面的 eccodes 库。
进一步查看 findlibs/__init__.py
的源代码:
__version__ = "0.0.2"
EXTENSIONS = {
"darwin": ".dylib",
"win32": ".dll",
}
def find(name):
"""Returns the path to the selected library, or None if not found."""
extension = EXTENSIONS.get(sys.platform, ".so")
for what in ("HOME", "DIR"):
LIB_HOME = "{}_{}".format(name.upper(), what)
if LIB_HOME in os.environ:
home = os.environ[LIB_HOME]
fullname = os.path.join(home, "lib", "lib{}{}".format(name, extension))
if os.path.exists(fullname):
return fullname
for path in (
"LD_LIBRARY_PATH",
"DYLD_LIBRARY_PATH",
):
for home in os.environ.get(path, "").split(":"):
fullname = os.path.join(home, "lib{}{}".format(name, extension))
if os.path.exists(fullname):
return fullname
for root in ("/", "/usr/", "/usr/local/", "/opt/"):
for lib in ("lib", "lib64"):
fullname = os.path.join(home, "{}{}/lib{}{}".format(root, lib, name, extension))
if os.path.exists(fullname):
return fullname
return ctypes.util.find_library(name)
在该版本中 (0.0.2),查找分为 4 个步骤
- 在环境变量
ECCODES_HOME
、ECCODES_DIR
中的lib
、lib64
目录中查找 - 在环境变量
LD_LIBRARY_PATH
、DYLD_LIBRARY_PATH
中的lib
、lib64
目录中查找 - 在目录
/
、/usr
、/usr/local
、/opt
的lib
、lib64
目录中查找 - 调用 Python 内置的
ctypes.util.find_library
函数
因为容器环境存在 /usr/local/lib64/libeccodes.so
文件,所以在第3步中返回该路径。
手动执行第4步:
>>> import ctypes.util
>>> ctypes.util.find_library("eccodes")
'/anaconda3/envs/py311-plot/lib/libeccodes.so'
可以找到 conda 环境安装的 eccodes 库,由此可见问题就出在 findlibs 库的实现逻辑上。
解决方案
简单方案
笔者在开发中选用的方案是将 conda 库路径添加到 LD_LIBRARY_PATH
环境变量中。
但该方案仅为了能快速跑通脚本,存在各种问题,比如如果环境迁移了脚本就失效了。
也可以直接删掉 /usr/local 下的 eccodes 库,或者重新编译支持 JPEG 的版本。
更新 findlibs 库
容器 conda 环境安装的 findlibs 是 0.0.2 版本。
实际上,findlibs 在 0.0.3 及以后版本中就对本文的问题进行了修正。
在最新的 0.0.5 版本 (2023.04.21检索) 中,find
函数开头有了下面的代码:
# sys.prefix/lib, $CONDA_PREFIX/lib has highest priority;
# otherwise, system library may mess up anaconda's virtual environment.
roots = [sys.prefix]
if "CONDA_PREFIX" in os.environ:
roots.append(os.environ["CONDA_PREFIX"])
for root in roots:
for lib in ("lib", "lib64"):
fullname = os.path.join(root, lib, "lib{}{}".format(lib_name, extension))
if os.path.exists(fullname):
return fullname
从注释就可以看到新版本将 conda 安装的 eccodes 库作为最高优先级,而不是最后一步才检查。
更新 findlibs 到最新版就可以解决这个问题。 如果 conda forge 没有更新可以手动下载源码安装。
后记
笔者实际的查错过程比较费劲。
首先怀疑是 CMA 镜像的问题。在容器中使用清华镜像重新安装,运行报错。在超算上使用CMA镜像安装同样的环境,运行正常。
经过多次尝试后,才开始浏览 eccodes python 接口源码,直到手动运行 findlibs.find("eccodes")
函数才最终定位到问题所在。
快速定位问题是编程熟练工种的必备技能。 如果下次遇到类似的问题,可能就更有经验了。
参考
findlibs源码:https://github.com/ecmwf/findlibs/