我有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
。
Caveat:文件太大,以至于我无法将所有文件同时加载到内存中。
我在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)
但是当数据太大时,这会因内存错误而失败。
实现此目标的最佳方法是什么?
我认为最好的方法是首先循环浏览所有文件以收集列名,然后循环浏览文件以将它们设置为正确的格式,然后在遇到它们时将它们写入光盘。但是,也许已经有一些执行此操作的代码了吗?
这里是我(OP)到目前为止提出的解决方案。不幸的是,它和时间一样慢。我希望有人能够提出更好的建议。
R代码:
library(parallel)
library(parallelMap)
library(filelock)
# specify the directory containing the files we want to merge
# as well as the output file
args <- commandArgs(TRUE)
directory <- if (length(args)>0) args[1] else 'filenames'
output_fname <- paste0(directory, '.dat')
# list the .dat files we want to merge
filenames <- file.path(directory, list.files(directory))
filenames <- filenames[grep('.dat', filenames)]
# a function to read the column names
get_col_names <- function(filename)
colnames(read.table(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
# read in the first data file and merge with the blank data frame
# it will have NAs in any columns it didn't have before
DF <- read.table(filenames[1], header=1, check.names=0)
DF <- plyr:::rbind.fill(name_DF, DF)
# write that data frame to the disk
write.table(DF, output_fname, quote=F, col.names=TRUE,
row.names=F, sep='\t')
# now repeat for every file we have, locking the file when we want to save
parallelStartMulticore(max(1, min(detectCores(), 62)))
success <- parallelMap(function(ii) {
print(filenames[ii])
DF <- read.table(filenames[ii], header=1, check.names=0)
DF <- plyr:::rbind.fill(name_DF, DF)
lck <- lock(output_fname, timeout=Inf)
write.table(DF, output_fname, quote=F, append=TRUE, col.names=F,
row.names=F, sep='\t')
unlock(lck)
}, ii=2:length(filenames))
# and we're done
print(all(unlist(success)))
这可以通过GNU awk脚本轻松完成。这个想法是先读取每个文件的标题,然后再次处理文件。为此,我们需要GNU awk,因为它具有nextfile
函数,该函数使我们无法完全读取GB的数据。我们还将使用awk将文件重新添加到参数列表:
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) n=asorti(h) # sort header into h[idx] = key
for(i=1;i<=n;++i) printf "%s" (i==n?ORS:OFS), h[i] # print header
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" (i==n?ORS:OFS) , (h[i] in a ? $(a[h[i]]) : s) }
如果将以上内容存储在文件merge.awk
中,则可以使用命令:
awk -f merge.awk f1 f2 f3 f4 ... fx