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)
但是当数据太大时,这会因内存错误而失败。
实现这一目标的最佳方法是什么?
我认为最好的方法是首先遍历所有文件以收集列名,然后遍历文件以将它们放入正确的格式,并在遇到它们时将它们写入磁盘。但是,是否可能已经有一些代码可以执行此操作?
从算法的角度来看,我将采取以下步骤:
处理标题:
- 读取所有输入文件的所有标题并提取所有列名
- 按照您想要的顺序对列名进行排序
- 创建一个查找表,在给定字段编号时返回列名 (
h[n] -> "name"
)处理文件:在标头之后,您可以重新处理文件
- 读取文件头
- 创建一个查找表,在给定列名时返回字段编号。关联数组在这里很有用:(
a["name"] -> field_number
)处理文件的其余部分
- 循环合并文件的所有字段
- 获取列名
h
- 检查列名是否在
a
,如果没有打印-
,如果有则打印对应的字段编号a
。
使用 GNU awk 使用扩展名nextfile
和asorti
. 该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 代码来支持我们的需求。
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-get
,apt
和yum
。
归档时间: |
|
查看次数: |
306 次 |
最近记录: |