如何将多个.h5文件合并?

4

网上提供的所有内容都太复杂了。我有一个很大的数据库,所以我把它分成几个部分导出。现在我有三个.h5文件,我想将它们合并成一个.h5文件以进行进一步的工作。我该怎么做呢?


我的建议是使用 h5py 库编写一个简单的 Python 代码。 - Tom de Geus
4个回答

8
这些示例展示了如何使用h5py在两个HDF5文件之间复制数据集。查看我的其他答案,了解有关PyTables的示例。我创建了一些简单的HDF5文件来模仿CSV类型数据(全部为浮点数,但如果您有混合数据类型,则过程是相同的)。根据您的描述,每个文件只有一个数据集。当您有多个数据集时,您可以在h5py中使用visititems()扩展此过程。
注意:用于创建示例中使用的HDF5文件的代码位于末尾。
所有方法都使用glob()来查找下面操作中使用的HDF5文件。
方法1:创建外部链接 这将在新的HDF5文件中产生3个组,每个组都有一个指向原始数据的外部链接。这不会复制数据,但通过1个文件中的链接提供对所有文件中数据的访问。
with h5py.File('table_links.h5',mode='w') as h5fw:
    link_cnt = 0 
    for h5name in glob.glob('file*.h5'):
        link_cnt += 1
        h5fw['link'+str(link_cnt)] = h5py.ExternalLink(h5name,'/')   

方法2a:直接复制数据
(2020年5月26日更新:对于所有数据集,使用.copy()方法。)
此方法将原始文件中每个数据集的数据复制到新文件中,并使用原始数据集名称。它循环复制所有根级别数据集。这需要每个文件中的数据集具有不同的名称。数据不会合并到一个数据集中。

with h5py.File('table_copy.h5',mode='w') as h5fw:
    for h5name in glob.glob('file*.h5'):
        h5fr = h5py.File(h5name,'r') 
        for obj in h5fr.keys():        
            h5fr.copy(obj, h5fw)       

方法二b:直接复制数据
(这是我最初的答案,当时我还不知道.copy()方法。)
这个方法会将原始文件中每个数据集的数据以原始数据集名称的形式复制到新文件中。需要注意的是,在每个文件中的数据集名称都必须不同。数据不会合并到一个数据集中。

with h5py.File('table_copy.h5',mode='w') as h5fw:
    for h5name in glob.glob('file*.h5'):
        h5fr = h5py.File(h5name,'r') 
        dset1 = list(h5fr.keys())[0]
        arr_data = h5fr[dset1][:]
        h5fw.create_dataset(dset1,data=arr_data)   

方法三a:将所有数据合并为一个固定大小的数据集
这将把原始文件中每个数据集的数据复制和合并到新文件的单个数据集中。 在本例中,对数据集名称没有任何限制。另外,我最初创建了一个大型数据集,并且不调整大小。这假设有足够的行来容纳所有合并的数据。生产工作中应添加测试。

with h5py.File('table_merge.h5',mode='w') as h5fw:
    row1 = 0
    for h5name in glob.glob('file*.h5'):
        h5fr = h5py.File(h5name,'r') 
        dset1 = list(h5fr.keys())[0]
        arr_data = h5fr[dset1][:]
        h5fw.require_dataset('alldata', dtype="f",  shape=(50,5), maxshape=(100, 5) )
        h5fw['alldata'][row1:row1+arr_data.shape[0],:] = arr_data[:]
        row1 += arr_data.shape[0]

第三种方法b:将所有数据合并到一个可调整大小的数据集中
这与上述方法类似。不过,我创建了一个可调整大小的数据集,并根据读取和添加的数据量进行扩大。

with h5py.File('table_merge.h5',mode='w') as h5fw:
    row1 = 0
    for h5name in glob.glob('file*.h5'):
        h5fr = h5py.File(h5name,'r') 
        dset1 = list(h5fr.keys())[0]
        arr_data = h5fr[dset1][:]
        dslen = arr_data.shape[0]
        cols = arr_data.shape[1]
        if row1 == 0: 
            h5fw.create_dataset('alldata', dtype="f",  shape=(dslen,cols), maxshape=(None, cols) )
        if row1+dslen <= len(h5fw['alldata']) :
            h5fw['alldata'][row1:row1+dslen,:] = arr_data[:]
        else :
            h5fw['alldata'].resize( (row1+dslen, cols) )
            h5fw['alldata'][row1:row1+dslen,:] = arr_data[:]
        row1 += dslen

创建源文件:请参阅上文。

for fcnt in range(1,4,1):
    fname = 'file' + str(fcnt) + '.h5'
    arr = np.random.random(50).reshape(10,5)
    with h5py.File(fname,'w') as h5fw :
        h5fw.create_dataset('data_'+str(fcnt),data=arr)

1
你是正确的。方法2创建一个新数据集,然后从第一个数据集复制数据。因此,在创建新数据集时,您需要获取属性,然后使用它们。在我写下那个回答的时候,我不知道h5py中有.copy()方法可以复制组和数据集。我猜测使用.copy()创建的新数据集将继承这些属性--但您应该进行测试以确认。(这类似于下面的PyTables copy_children()方法。)我需要更新我的答案以添加该方法。 - kcw78
@kcw78 假设我有一些HDF5文件,它们都具有相同的结构,即它们具有相同数量的键(所有键都具有相同的名称)。对于每个键,有几个数据集,但是在HDF5文件中,它们的名称也相同。理想情况下,我现在想获得一个单独的HDF5文件,它具有相同数量的键(具有相同的名称),并且每个键具有数据集的数量。唯一需要更改的是数据集的形状,因为最终的HDF5文件是单个文件的组合。您能否展示一下如何更新Method 3a的代码? - Hermi
你已经写过 visititems() 函数用于多个数据集,但我还需要考虑 HDF5 文件的多个键。 - Hermi
@Hermi,我根据你的描述改编了现有的解决方案。请查看我今天添加的新答案。注意,它不使用.visititems()。我找到了一个更好的解决方案,使用了生成器。 - kcw78
h5r.copy(obj, h5fw) 中有一个小错别字吗?应该是 h5fr.copy(obj, h5fw),变量名应该有一个 f 字符,对吧? - Hakan Baba
显示剩余3条评论

2

对于那些更喜欢使用PyTables的人,我重新制作了我的h5py示例,展示了在两个HDF5文件之间复制数据的不同方法。这些示例使用与以前相同的示例HDF5文件。每个文件只有一个数据集。当您有多个数据集时,可以使用Pytables中的walk_nodes()扩展此过程。

所有方法都使用glob()来查找下面操作中使用的HDF5文件。

方法1:创建外部链接
类似于h5py,它在新的HDF5文件中创建3个组,每个组都有一个指向原始数据的外部链接。数据不会被复制。

import tables as tb
with tb.File('table_links_2.h5',mode='w') as h5fw:
    link_cnt = 0 
    for h5name in glob.glob('file*.h5'):
        link_cnt += 1
        h5fw.create_external_link('/', 'link'+str(link_cnt), h5name+':/')

方法二:直接复制数据
此方法将从原始文件中每个数据集中复制数据到新文件中,使用原始数据集名称。数据集对象与源HDF5文件相同类型。在这种情况下,它们是PyTable数组(因为所有列都是相同类型的)。数据集使用源HDF5中的名称进行复制,因此每个数据集必须具有不同的名称。数据不会合并成单个数据集。

with tb.File('table_copy_2.h5',mode='w') as h5fw:
    for h5name in glob.glob('file*.h5'):
        h5fr = tb.File(h5name,mode='r') 
        print (h5fr.root._v_children)
        h5fr.root._f_copy_children(h5fw.root)     

方法3a:将所有数据合并为1个数组
此方法将原始文件中每个数据集中的数据复制并合并到新文件的单个数据集中。数据保存为PyTables Array格式。数据集名称没有限制。首先读取数据并追加到Numpy数组中。一旦处理完所有文件,Numpy数组就会被复制到PyTables数组中。这个过程会将Numpy数组保存在内存中,所以对于大型数据集可能不适用。您可以通过使用PyTables EArray(可扩展的数组)来避免此限制。请参见方法3b。

with tb.File('table_merge_2a.h5',mode='w') as h5fw:
    row1 = 0
    for h5name in glob.glob('file*.h5'):
        h5fr = tb.File(h5name,mode='r') 
        dset1 = h5fr.root._f_list_nodes()[0]
        arr_data = dset1[:]
        if row1 == 0 :
           all_data = arr_data.copy()
           row1 += arr_data.shape[0]
        else :
           all_data = np.append(all_data,arr_data,axis=0)
           row1 += arr_data.shape[0]
    tb.Array(h5fw.root,'alldata', obj=all_data )

方法3b:将所有数据合并为1个可扩展的EArray
这与上面的方法类似,但是会逐步将数据保存在PyTables EArray中。使用EArray.append()方法添加数据。这个过程减少了方法3a中的内存问题。

with tb.File('table_merge_2b.h5',mode='w') as h5fw:
    row1 = 0
    for h5name in glob.glob('file*.h5'):
        h5fr = tb.File(h5name,mode='r') 
        dset1 = h5fr.root._f_list_nodes()[0]
        arr_data = dset1[:]
        if row1 == 0 :
           earr = h5fw.create_earray(h5fw.root,'alldata', 
                                     shape=(0,arr_data.shape[1]), obj=arr_data )
        else :
           earr.append(arr_data)
        row1 += arr_data.shape[0]   

方法 4:将所有数据合并到一个表中
此示例突出了h5pyPyTables之间的区别。在 h5py 中,数据集可以引用 np.arraysnp.recarrays -- h5py 处理不同的 dtypes。在 Pytables 中,数组(和 CArrays 和 EArrays)引用 nd.array 数据,而表引用 np.recarray 数据。此示例演示了如何将源文件中的 nd.array 数据转换为适合于 Table 对象的 np.recarray 数据。它还展示了如何使用 Table.append() 类似于方法 3b 中的 EArray.append()

with tb.File('table_append_2.h5',mode='w') as h5fw:
    row1 = 0
    for h5name in glob.glob('file*.h5'):
        h5fr = tb.File(h5name,mode='r') 
        dset1 = h5fr.root._f_list_nodes()[0]
        arr_data = dset1[:]
        ds_dt= ([ ('f1', float), ('f2', float), ('f3', float), ('f4', float), ('f5', float) ])
        recarr_data = np.rec.array(arr_data,dtype=ds_dt)
        if row1 == 0: 
            data_table = h5fw.create_table('/','alldata', obj=recarr_data)
        else :
            data_table.append(recarr_data)
        h5fw.flush()
        row1 += arr_data.shape[0]

1

本回答是为了解决@Hermi在2022年3月11日提出的合并来自多个文件的几个组和数据集的请求。一般情况下,这是一个复杂的问题--需要进行大量的错误检查,以确保一致的组名称、数据集名称和数据集属性(dtype和形状)。此外,数据集可以在多个方向上被“扩展”以容纳合并的数据(没有一个正确的答案)。

下面的代码实现了以下功能:

  • 如果在合并的文件中不存在源组/数据集,则会将数据复制到新文件中,并具有相同的组/数据集路径。数据集的形状会被扩展以增加另一个维度(轴),该维度可调整大小以在未来添加数据。
  • 如果在合并的文件中存在源组/数据集,则会测试现有数据集属性是否兼容。如果兼容,则会在N维(轴)上将数据集形状增加1,并将数据添加到该切片中。
  • 注意:它会添加带有源文件名字(供将来参考)的数据集属性。但它不会复制数据集或组属性。
  • 它使用了修改过的版本的生成器h5py_dataset_iterator(),该生成器来自如何区分HDF5数据集和组。由于.visititems()的返回和yield行为,这个生成器是一个更好的解决方案。
  • 注意:此过程会复制数据集,但不会复制空组(没有数据集的组)。如果需要,生成器将需要进行修改。

下面是代码:

import h5py
import glob

# Ref: https://dev59.com/fFsX5IYBdhLWcg3wB7n3#34401029
# with slight modifications
def h5py_dataset_iterator(g, prefix=''):
    for name, h5obj in g.items():
        path = '{}/{}'.format(prefix, name)
        if isinstance(h5obj, h5py.Dataset):  # test for dataset
            yield (h5obj, path)
        elif isinstance(h5obj, h5py.Group):  # test for group (go down)
            yield from h5py_dataset_iterator(h5obj, prefix=path)


with h5py.File('merged_h5_data.h5', mode='w') as h5w:            
    for h5source in glob.iglob('file*.h5'):
        print(f'\nWorking on file: {h5source}')
        with h5py.File(h5source, mode='r') as h5r:   
            for (dset, path) in h5py_dataset_iterator(h5r):
                print(f'Copying dataset from: {path}')
                ds_obj = h5r[path]
                arr_dtype = ds_obj.dtype
                arr_shape = ds_obj.shape
    
                # If dataset doesn't exist, create new datset and copy data
                # Note: we can't use .copy() method b/c we are changing shape and maxshape
                if path not in h5w:
                    h5w.create_dataset(path, data=ds_obj,
                                       shape=arr_shape+(1,), maxshape=arr_shape+(None,))
                else:
                    # Check for compatiable dtype and shape
                    ds_dtype = h5w[path].dtype
                    ds_shape = h5w[path].shape
                    # If dataset exists and is compatibale, resize datset and copy data
                    if ds_dtype == arr_dtype and ds_shape[:-1] == arr_shape:
                        new_shape = ds_shape[0:-1] + (ds_shape[-1]+1,)
                        h5w[path].resize(new_shape)               
                        h5w[path][...,-1] = ds_obj
                # Add attribute to dataset with source file name:
                h5w[path].attrs[f'File for index {h5w[path].shape[-1]-1} '] = h5source

用于创建上述使用的源文件的代码:
示例1:简单模式(2个组,每个组有3个数据集)

import h5py
import numpy as np

for fcnt in range(1,4,1):
    fname = 'file' + str(fcnt) + '.h5'
    with h5py.File(fname,'w') as h5fw:
        for gcnt in range(1,3,1):  
            grp = h5fw.create_group(f'group_{gcnt}')
            for dcnt in range(1,4,1):
                arr = np.random.randint(0,high=255,size=100,dtype=np.uintc).reshape(10,10)
                grp.create_dataset(f'dataset_{dcnt}',data=arr)

示例2:高级模式(包含组和数据集的3个层级)

import h5py
import numpy as np

ds_list = ['/dataset_1', '/dataset_2', 
           '/group_1/group_11/dataset_1', '/group_1/group_11/dataset_2', 
           '/group_1/group_12/dataset_1', '/group_1/group_12/dataset_2', 
           '/group_2/dataset_1', '/group_2/dataset_2',
           '/group_3/group_31/group_311/dataset_1', 
           '/group_3/group_31/group_312/dataset_1']
for fcnt in range(1,4,1):
    fname = 'cfile' + str(fcnt) + '.h5'
    with h5py.File(fname,'w') as h5fw:
        for name in ds_list:
            arr = np.random.randint(0,high=255,size=100,dtype=np.uintc).reshape(10,10)
            h5fw.create_dataset(name, data=arr)

说实话,我对h5py不太熟悉,所以这个问题可能被认为是琐碎/愚蠢的,但我只是想知道:难道不能简单地枚举原始h5py文件的所有键,从而获取组,然后迭代组的键以获取数据集吗?那是否是您的函数h5py_dataset_iterator的有效替代方案? - Hermi
你的问题并不琐碎或愚蠢。你的方法适用于这个简单的模式 - 只有根级别的组,而且组只有数据集。你的方法无法处理更深/更复杂的模式的文件。例如:根级别有3个组和2个数据集。第1组有2个带有数据集的子组。第2组有2个组和2个数据集。第3组有1个带有2个子组的组,每个子组都有数据集。你需要递归来遍历该对象树。生成器非常适合这个任务。:-) PyTables有一个内置的方法来做这件事(Group.walk_nodes())。 - kcw78
我添加了第二个示例,模仿了我之前评论中描述的更复杂的模式。合并文件的代码完美运行 - 不需要更改。请注意,空组不会被复制的警告。 - kcw78

1
至少有三种方法可以将单个HDF5文件中的数据合并到一个文件中:
  1. 使用外部链接创建一个新文件,指向其他文件中的数据(需要pytables/tables模块)
  2. 使用HDF Group实用程序h5copy.exe复制数据
  3. 使用Python(使用h5py或pytables)复制数据
外部链接的示例在此处可用:
https://stackoverflow.com/a/55399562/10462884
它展示了如何创建链接,然后如何取消引用它们。
h5copy的文档在此处:
https://support.hdfgroup.org/HDF5/doc/RM/Tools.html#Tools-Copy 使用h5py或pytables进行复制更加复杂。

我想出了一种方法,请告诉我这是否是正确的方法:首先我使用Pandas读取我的.h5文件,然后使用pandas的“to_csv()”函数将其保存为csv格式。将多个csv文件合并比.h5文件容易得多,而且文件大小几乎保持不变。这是其中一种做法吗? - ktt_11
如果这个方法适用于您,那么这是另一个选项(特别是如果您只想做一次并且可以在您的流程中使用CSV文件)。然而,如果我需要频繁地执行此操作或需要HDF5下游,则会使用上述方法之一,以避免创建和合并CSV文件。 - kcw78

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接