我有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)
,但是当数据太大时,这会失败并显示内存错误。
完成此操作的最佳方法是什么?
我认为最好的方法是首先循环浏览所有文件以收集列名,然后循环浏览文件以将它们设置为正确的格式,然后将其写入光盘。但是,也许已经有一些执行此操作的代码了吗?
答案 0 :(得分:6)
从算法的角度来看,我将执行以下步骤:
处理标题:
- 读取所有输入文件的所有标题并提取所有列名称
- 按所需顺序对列名称进行排序
- 创建一个查找表,当给出字段号(
h[n] -> "name"
)时返回列名处理文件:在标题之后,您可以重新处理文件
- 读取文件的标题
- 创建一个查找表,当给定列名时返回表号。关联数组在这里有用:(
a["name"] -> field_number
)处理文件的其余部分
- 循环遍历合并文件的所有字段
- 使用
获取列名h
- 检查列名是否在
a
中,如果不打印-
,则打印与a
相对应的字段号。
使用扩展名nextfile
和asorti
的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-get
,apt
和yum
。
答案 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