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

rho*_*ron 8 bash awk r dataframe

我有 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
Run Code Online (Sandbox Code Playgroud)

-可被任何东西所代替,例如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)
Run Code Online (Sandbox Code Playgroud)

但是当数据太大时,这会因内存错误而失败。

实现这一目标的最佳方法是什么?

我认为最好的方法是首先遍历所有文件以收集列名,然后遍历文件以将它们放入正确的格式,并在遇到它们时将它们写入磁盘。但是,是否可能已经有一些代码可以执行此操作?

kva*_*our 6

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

  1. 处理标题:

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

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

      1. 循环合并文件的所有字段
      2. 获取列名 h
      3. 检查列名是否在a,如果没有打印-,如果有则打印对应的字段编号a

使用 GNU awk 使用扩展名nextfileasorti. 该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) }
Run Code Online (Sandbox Code Playgroud)

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

awk -f merge.awk f1 f2 f3 f4 ... fx
Run Code Online (Sandbox Code Playgroud)

类似的方式,但不那么匆忙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) }
Run Code Online (Sandbox Code Playgroud)

此方法略有不同,但允许将具有不同字段分隔符的文件处理为

awk -f merge.awk f1 FS="," f2 f3 FS="|" f4 ... fx
Run Code Online (Sandbox Code Playgroud)

如果您的参数列表变得太长,您可以使用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) }
Run Code Online (Sandbox Code Playgroud)

可以运行为:

$ awk -f merge.awk filelist
$ find . | awk -f merge.awk "-"
$ find . | awk -f merge.awk
Run Code Online (Sandbox Code Playgroud)

或任何类似的命令。

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


Ini*_*ian 6

Miller ( 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
Run Code Online (Sandbox Code Playgroud)

它一次性产生输出为

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
Run Code Online (Sandbox Code Playgroud)

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

所用字段的简要说明

  • 要将输入流分隔为制表符分隔的内容, --tsvlite
  • 漂亮地打印表格数据 --opprint
  • unsparsify像上面所解释的完成了所有的字段名的工会在所有输入流
  • reorder需要重新排序动词,因为列标题在文件之间以随机顺序出现。因此,要明确定义顺序,请将该-f选项与您希望输出显示的列标题一起使用。

包的安装非常简单。Miller 是用可移植的现代 C 语言编写的,运行时依赖性为零。在通过包管理器安装是那么容易,它支持所有主要的软件包管理器自制,MacPorts的,apt-getaptyum