将具有不同列的大数据文件合并为一个大文件

时间:2019-06-12 13:39:23

标签: bash dataframe bigdata multiple-columns cat

我有N个制表符分隔的文件。每个文件都有一个标题行,其中包含各列的名称。有些列对所有文件都是公用的,但有些列是唯一的。

我想将所有文件组合成一个包含所有相关标头的大文件。

示例:

> cat file1.dat
a b c
5 7 2
3 9 1

> cat file2.dat
a b e f
2 9 8 3
2 8 3 3
1 0 3 2

> cat file3.dat
a c d g
1 1 5 2

> merge file*.dat
a b c d e f g
5 7 2 - - - -
3 9 1 - - - -
2 9 - - 8 3 -
2 8 - - 3 3 -
1 0 - - 3 2 -
1 - 1 5 - - 2

-可以替换为任何内容,例如NA

注意事项:文件太大,以至于我无法将所有文件同时加载到内存中。

我使用

在R中有一个解决方案
write.table(do.call(plyr:::rbind.fill, 
            Map(function(filename) 
                    read.table(filename, header=1, check.names=0), 
                filename=list.files('.'))), 
    'merged.dat', quote=FALSE, sep='\t', row.names=FALSE)

,但是当数据太大时,这会失败并显示内存错误。

完成此操作的最佳方法是什么?

我认为最好的方法是首先循环浏览所有文件以收集列名,然后循环浏览文件以将它们设置为正确的格式,然后将其写入光盘。但是,也许已经有一些执行此操作的代码了吗?

4 个答案:

答案 0 :(得分:6)

从算法的角度来看,我将执行以下步骤:

  1. 处理标题:

    • 读取所有输入文件的所有标题并提取所有列名称
    • 按所需顺序对列名称进行排序
    • 创建一个查找表,当给出字段号(h[n] -> "name")时返回列名
  2. 处理文件:在标题之后,您可以重新处理文件

    • 读取文件的标题
    • 创建一个查找表,当给定列名时返回表号。关联数组在这里有用:(a["name"] -> field_number
    • 处理文件的其余部分

      1. 循环遍历合并文件的所有字段
      2. 使用h
      3. 获取列名
      4. 检查列名是否在a中,如果不打印-,则打印与a相对应的字段号。

使用扩展名nextfileasorti的GNU awk可以很容易地做到这一点。 nextfile函数允许我们仅读取标题并移至下一个文件,而无需处理整个文件。由于我们需要对文件进行两次处理(第1步读取标题,第2步读取文件),因此我们将要求awk动态操作其参数列表。每次处理文件头时,我们都会将其添加到参数列表ARGV的末尾,以便可以将其用于step 2

BEGIN { s="-" }                # define symbol
BEGIN { f=ARGC-1 }             # get total number of files
f { for (i=1;i<=NF;++i) h[$i]  # read headers in associative array h[key]
    ARGV[ARGC++] = FILENAME    # add file at end of argument list
    if (--f == 0) {            # did we process all headers?
       n=asorti(h)             # sort header into h[idx] = key
       for (i=1;i<=n;++i)      # print header
           printf "%s%s", h[i], (i==n?ORS:OFS)
    }
    nextfile                   # end of processing headers
}           
# Start of processing the files
(FNR==1) { delete a; for(i=1;i<=NF;++i) a[$i]=i; next } # read header
{ for(i=1;i<=n;++i) printf "%s%s", (h[i] in a ? $(a[h[i]]) : s), (i==n?ORS:OFS) }

如果将以上内容存储在文件merge.awk中,则可以使用以下命令:

awk -f merge.awk f1 f2 f3 f4 ... fx

一种类似的方法,但是使用f可以减少麻烦:

BEGIN { s="-" }                 # define symbol
BEGIN {                         # modify argument list from
        c=ARGC;                 #   from: arg1 arg2  ... argx
        ARGV[ARGC++]="f=1"      #   to:   arg1 arg2  ... argx f=1 arg1 arg2  ... argx
        for(i=1;i<c;++i) ARGV[ARGC++]=ARGV[i]
}
!f { for (i=1;i<=NF;++i) h[$i]  # read headers in associative array h[key]
     nextfile
}
(f==1) && (FNR==1) {            # process merged header
     n=asorti(h)                # sort header into h[idx] = key
     for (i=1;i<=n;++i)         # print header
        printf "%s%s", h[i], (i==n?ORS:OFS)
     f=2                         
}
# Start of processing the files
(FNR==1) { delete a; for(i=1;i<=NF;++i) a[$i]=i; next } # read header
{ for(i=1;i<=n;++i) printf "%s%s", (h[i] in a ? $(a[h[i]]) : s), (i==n?ORS:OFS) }

此方法稍有不同,但是允许使用不同的字段分隔符处理文件

awk -f merge.awk f1 FS="," f2 f3 FS="|" f4 ... fx

如果参数列表太长,可以使用awk为您创建参数:

BEGIN { s="-" }                 # define symbol
BEGIN {                         # read argument list from input file:
  fname=(ARGC==1 ? "-" : ARGV[1])
  ARGC=1                        # from: filelist or /dev/stdin
  while ((getline < fname) > 0) #   to:   arg1 arg2 ... argx
     ARGV[ARGC++]=$0
}
BEGIN {                         # modify argument list from
        c=ARGC;                 #   from: arg1 arg2  ... argx
        ARGV[ARGC++]="f=1"      #   to:   arg1 arg2  ... argx f=1 arg1 arg2  ... argx
        for(i=1;i<c;++i) ARGV[ARGC++]=ARGV[i]
}
!f { for (i=1;i<=NF;++i) h[$i]  # read headers in associative array h[key]
     nextfile
}
(f==1) && (FNR==1) {            # process merged header
     n=asorti(h)                # sort header into h[idx] = key
     for (i=1;i<=n;++i)         # print header
        printf "%s%s", h[i], (i==n?ORS:OFS)
     f=2                         
}
# Start of processing the files
(FNR==1) { delete a; for(i=1;i<=NF;++i) a[$i]=i; next } # read header
{ for(i=1;i<=n;++i) printf "%s%s", (h[i] in a ? $(a[h[i]]) : s), (i==n?ORS:OFS) }

可以运行为:

$ awk -f merge.awk filelist
$ find . | awk -f merge.awk "-"
$ find . | awk -f merge.awk

或任何类似的命令。

如您所见,通过仅添加一小段代码,我们就能够灵活地调整到awk代码来满足我们的需求。

答案 1 :(得分:6)

密勒(johnkerl/miller)在处理大型文件时使用率极低。它具有大量有用的文件处理工具所包含的功能。就像官方文件说的那样

密勒(Miller)类似于 awk, sed, cut, join sort ,用于名称索引数据,例如CSV,TSV和表格JSON。您可以使用命名字段来处理数据,而无需计算位置列索引。

在这种情况下,它支持动词unsparsify,该动词在文档中表示为

在所有输入记录上打印带有字段名并集的记录。 对于给定记录中不存在但在其他记录中存在的字段名称,请填写 一个值。此动词在产生任何输出之前会保留所有输入。

您只需要执行以下操作,然后根据需要将文件重新排列到列位置即可

mlr --tsvlite --opprint unsparsify then reorder -f a,b,c,d,e,f file{1..3}.dat

一次性生成输出

a   b   c   d   e   f   g
5   7   2   -   -   -   -
3   9   1   -   -   -   -
2   9   -   -   8   3   -
2   8   -   -   3   3   -
1   0   -   -   3   2   -
1   -   1   5   -   -   2

您甚至可以自定义用于填充空白字段的字符,默认字符为-。对于自定义字符,请使用unsparsify --fill-with '#'

所用字段的简要说明

  • 要将输入流定为制表符分隔的内容,--tsvlite
  • 要漂亮地打印表格数据--opprint
  • 如上所述,unsparsify对所有输入流进行所有字段名称的并集
  • 需要重新排序动词reorder,因为列标题以随机顺序出现在文件之间。因此,要明确定义顺序,请对要显示输出的列标题使用-f选项。

该软件包的安装非常简单。 Miller用可移植的现代C语言编写,具有零运行时间依赖性。 installation via package managers非常简单,它支持所有主要的软件包管理器Homebrew,MacPorts,apt-getaptyum

答案 2 :(得分:3)

在注释中提供更新的信息,其中包含大约10 ^ 5个输入文件(因此超出了shell的非内置命令的args的最大数量),并希望输出列按显示顺序而不是按字母顺序排序,下面的代码将可以在任何awk和任何查找条件下工作:

$ cat tst.sh
#!/bin/env bash
find . -maxdepth 1 -type f -name "$1" |
awk '
NR==FNR {
    fileName = $0
    ARGV[ARGC++] = fileName
    if ( (getline fldList < fileName) > 0 ) {
        if ( !seenList[fldList]++ ) {
            numFlds = split(fldList,fldArr)
            for (inFldNr=1; inFldNr<=numFlds; inFldNr++) {
                fldName = fldArr[inFldNr]
                if ( !seenName[fldName]++ ) {
                    hdr = (numOutFlds++ ? hdr OFS : "") fldName
                    outNr2name[numOutFlds] = fldName
                }
            }
        }
    }
    close(fileName)
    next
}
FNR == 1 {
    if ( !doneHdr++ ) {
        print hdr
    }
    delete name2inNr
    for (inFldNr=1; inFldNr<=NF; inFldNr++) {
        fldName = $inFldNr
        name2inNr[fldName] = inFldNr
    }
    next
}
{
    for (outFldNr=1; outFldNr<=numOutFlds; outFldNr++) {
        fldName = outNr2name[outFldNr]
        inFldNr = name2inNr[fldName]
        fldValue = (inFldNr ? $inFldNr : "-")
        printf "%s%s", fldValue, (outFldNr<numOutFlds ? OFS : ORS)
    }
}
' -

$ ./tst.sh 'file*.dat'
a b c e f d g
5 7 2 - - - -
3 9 1 - - - -
2 9 - 8 3 - -
2 8 - 3 3 - -
1 0 - 3 2 - -
1 - 1 - - 5 2

请注意,脚本的输入现在是您希望find用于查找文件而不是文件列表的glob模式。


原始答案:

如果您不介意shell + awk组合脚本,则可以使用任何awk进行工作:

$ cat tst.sh
#!/bin/env bash

awk -v hdrs="$(head -1 -q "$@" | tr ' ' '\n' | sort -u)" '
BEGIN {
    numOutFlds = split(hdrs,outNr2name)
    for (outFldNr=1; outFldNr<=numOutFlds; outFldNr++) {
        fldName = outNr2name[outFldNr]
        printf "%s%s", fldName, (outFldNr<numOutFlds ? OFS : ORS)
    }
}
FNR == 1 {
    delete name2inNr
    for (inFldNr=1; inFldNr<=NF; inFldNr++) {
        fldName = $inFldNr
        name2inNr[fldName] = inFldNr
    }
    next
}
{
    for (outFldNr=1; outFldNr<=numOutFlds; outFldNr++) {
        fldName = outNr2name[outFldNr]
        inFldNr = name2inNr[fldName]
        fldValue = (inFldNr ? $inFldNr : "-")
        printf "%s%s", fldValue, (outFldNr<numOutFlds ? OFS : ORS)
    }
}
' "$@"

$ ./tst.sh file{1..3}.dat
a b c d e f g
5 7 2 - - - -
3 9 1 - - - -
2 9 - - 8 3 -
2 8 - - 3 3 -
1 0 - - 3 2 -
1 - 1 5 - - 2

否则,使用GNU awk进行数组,sorted_in和ARGIND的所有操作都将是awk:

$ cat tst.awk
BEGIN {
    for (inFileNr=1; inFileNr<ARGC; inFileNr++) {
        inFileName = ARGV[inFileNr]
        if ( (getline < inFileName) > 0 ) {
            for (inFldNr=1; inFldNr<=NF; inFldNr++) {
                fldName = $inFldNr
                name2inNr[fldName][inFileNr] = inFldNr
            }
        }
        close(inFileName)
    }

    PROCINFO["sorted_in"] = "@ind_str_asc"
    for (fldName in name2inNr) {
        printf "%s%s", (numOutFlds++ ? OFS : ""), fldName
        for (inFileNr in name2inNr[fldName]) {
            outNr2inNr[numOutFlds][inFileNr] = name2inNr[fldName][inFileNr]
        }
    }
    print ""
}

FNR > 1 {
    for (outFldNr=1; outFldNr<=numOutFlds; outFldNr++) {
        inFldNr = outNr2inNr[outFldNr][ARGIND]
        fldValue = (inFldNr ? $inFldNr : "-")
        printf "%s%s", fldValue, (outFldNr<numOutFlds ? OFS : ORS)
    }
}

$ awk -f tst.awk file{1..3}.dat
a b c d e f g
5 7 2 - - - -
3 9 1 - - - -
2 9 - - 8 3 -
2 8 - - 3 3 -
1 0 - - 3 2 -
1 - 1 5 - - 2

为提高效率,上面的第二个脚本在BEGIN部分中进行了所有繁重的工作,因此在脚本主体中要做的工作很少,该主体在每个输入行中只进行一次评估。在BEGIN部分中,它创建一个关联数组(outNr2inNr[]),该数组将传出字段号(所有输入文件中所有字段名的按字母顺序排列的列表)映射到传入字段号,因此正文中要做的所有事情就是按该顺序打印字段。

答案 3 :(得分:2)

这是我(OP)到目前为止提出的解决方案。与其他方法相比,它可能具有一些优势,因为它可以并行处理文件。

R代码:

library(parallel)
library(parallelMap)

# specify the directory containing the files we want to merge
args <- commandArgs(TRUE)
directory <- if (length(args)>0) args[1] else 'sg_grid'
#output_fname <- paste0(directory, '.dat')

# make a tmp directory that will store all the files 
tmp_dir <- paste0(directory, '_tmp')
dir.create(tmp_dir)

# list the .dat files we want to merge 
filenames <- list.files(directory)
filenames <- filenames[grep('.dat', filenames)]

# a function to read the column names 
get_col_names <- function(filename) 
    colnames(read.table(file.path(directory, filename), 
        header=T, check.names=0, nrow=1))

# grab all the headers of all the files and merge them 
col_names <- get_col_names(filenames[1])
for (simulation in filenames) {
    col_names <- union(col_names, get_col_names(simulation))
}

# put those column names into a blank data frame 
name_DF <- data.frame(matrix(ncol = length(col_names), nrow = 0))
colnames(name_DF) <- col_names

# save that as the header file 
write.table(name_DF, file.path(tmp_dir, '0.dat'), 
    col.names=TRUE, row.names=F, quote=F, sep='\t')

# now read in every file and merge with the blank data frame 
# it will have NAs in any columns it didn't have before 
# save it to the tmp directory to be merged later 
parallelStartMulticore(max(1, 
    min(as.numeric(Sys.getenv('OMP_NUM_THREADS')), 62)))
success <- parallelMap(function(filename) {
    print(filename)
    DF <- read.table(file.path(directory, filename), 
        header=1, check.names=0)
    DF <- plyr:::rbind.fill(name_DF, DF)
    write.table(DF, file.path(tmp_dir, filename), 
        quote=F, col.names=F, row.names=F, sep='\t')
}, filename=filenames)

# and we're done 
print(all(unlist(success)))

这将创建所有文件的临时版本,每个文件现在都具有所有标题,然后我们可以cat一起添加到结果中:

ls -1 sg_grid_tmp/* | while read fn ; do cat "$fn" >> sg_grid.dat; done