对于序列化保存各种 array / data frame 等类型的数据,一直以来有各种各样的办法。例如我用过的,对于简单的一个 array,NumPy 有提供读写的方法;pandas 也有对应的 data frame 读写;而字符串/字典,可以变成 json 保存等。
但是,如果数量多了,例如有 100 个 array,上面的方法就不太方便了。我比较懒,会把这些 array 放到一个 dict 里面,然后用 pickle 把这个 dict pickle下来——保存和读取都非常方便,而且兼容所有数据类型。
后来,数据量多了之后,就发现 pickle 的方案也是有缺点的,就是性能不好(文末有初步的性能对比)。所以调研了一下后,选择了 HDF5。以前只是听过,没有用过,现在用了感觉不错,在下面稍微总结一下。
目标用户
无论是科学研究,还是各行各业,都有 HDF5 的身影。高效、跨平台、无上限,尤其适合数据量大的情景。见官网的 Who Uses HDF?
安装
HDF5 支持各种语言,Python 对应的库是 h5py。
$ pip install h5py or $ conda install h5py # Anaconda
核心概念
HDF5 里只有 2 种类型:dataset 和 group。
– dataset 就像数组,类似 Python 的 list (一维或多维),或 NumPy 的 ndarray。dataset 的语法和 ndarray 类似。
– group 就像 Python 的 dict,在我看来,它更像是带路径的文件夹。group 的语法和 dict 类似。
就像是在一层一层的文件夹中,存放着不同的 dataset。记住以上两点,就🆗
创建 HDF5 文件、dataset
通过创建一个属性为w
或a
(append) 的File
对象,来新建/修改一个 hdf5 文件,下面的例子,在 mytestfile.hdf5 的“根目录”下新建一个 dataset,数据类型是整数,shape 是一维的 (100,),初始化为 0。
>>> import h5py >>> with h5py.File('mytestfile.hdf5', 'w') as f: >>> dset = f.create_dataset("mydataset", (100,), dtype='i') # shape (100, )
读取 HDF5 文件
读取刚才创建的文件:
>>> import h5py >>> f = h5py.File('mytestfile.hdf5', 'r') >>> list(f.keys()) ['mydataset'] >>> dset = f['mydataset'] # 是一个dataset object 不是array >>> dset[:] # 这才是array的数据 array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]) >>> dset.shape (100,) >>> dset.dtype dtype('int32') >>> f.close()
层级
刚才创建的时候,是在 mytestfile.hdf5 的“根目录”下新建一个 dataset,我们看一下它是怎么表示的。
>>> f = h5py.File('mytestfile.hdf5', 'a') >>> f.name '/' >>> f['mydataset'].name '/mydataset'
可以看到,基本上和 Linux / Mac OS 路径结构是一样的。
接下来在“根目录”下创建一个 subgroup(“文件夹”):
>>> subgroup = f.create_group('subgroup') >>> subgroup.name # 就像创建了一个文件夹 '/subgroup'
然后就可以在 subgroup 中创建 dataset。
>>> dset2 = subgroup.create_dataset('another_dataset', (50,), dtype='f') >>> dset2.name '/subgroup/another_dataset'
一个比较方便的 feature 是可以不用创建 group 也能直接创建“子目录”下的 dataset:
>>> dset3 = f.create_dataset('subgroup2/dataset_three', (10,), dtype='i') >>> dset3.name '/subgroup2/dataset_three' >>> dataset_three = f['subgroup2/dataset_three'] # 不用一层一层进去
遍历一个 group:
>>> for name in f: ... print(name) mydataset subgroup subgroup2
和 Python dict 类似,也可以用 key in f
, keys()
, values()
, items()
等方法。
属性 (Attribute)
HDF5 是可以自我描述 (self-describing) 的,每个 group 和 dataset 都有一个 .attrs 属性,。它就像一个 dict,可以往里面塞东西:
>>> dset.attrs['date'] = '2020-01-01' >>> dset.attrs['date'] '2020-01-01' >>> 'date' in dset.attrs True
以上就是官方文档介绍的快速入门。接下来是一些入门之外的内容。
进阶
压缩
dataset 的压缩是 on the fly 的,即写入时即时压缩,读取时即时解压。相比不压缩,只是多花了点时间,读取的代码没有任何区别,写入时需要增加参数:
dset = f.create_dataset("zipped_dataset", (100, 100), compression="gzip", compression_opts=4) # 压缩等级0~9 默认为4
字符串
h5py 支持的字符串有 3 种:定长 ASCII、变长 ASCII、变长 UTF-8。我用到的一般只有变长 UTF-8,所以下面只介绍它。
# 用在属性 dset.attrs['name'] = 'Hello' # 用在dataset str_dtype = h5py.string_dtype() dset = f.create_dataset("a_string", data='aaaaaaaaaaaa', dtype=str_dtype)
与 pickle 的性能对比
我在实际的项目里,会用多个 Python 进程进行一些计算,然后把计算的结果存下来。对于 pickle,我用 4 个 Python 进程,保存 4 个 pickle 文件;对于 HDF5,4 个进程分别计算,保存时加锁,存到同一个 .hdf5 文件中。我在某一个实际项目中,粗略测试的性能数据如下:
保存格式 | 计算+保存运行时间 | 计算+保存峰值内存占用 | 序列化文件总大小 | 从序列化文件导出数据耗时 | 导出数据内存占用 |
pickle | 1m 26s | 2600 MB (pickle时内存翻倍) | 1400 MB | 2m 30s | 900 MB |
hdf5 | 1m 36s | 1800 MB | 700 MB gzip 压缩 | 2m 15s | 150 MB |
参考
1. 官方文档
发表评论