转换大量制表符分隔文件(2.1TB)@ 8.5Kx39M的有效方法

时间:2013-04-03 19:45:15

标签: bash transpose

我有几个巨大的制表符分隔文件@~2.1 TB每个,~8.5 K行,~39.3M列。第一列是可变长度的所有字符串(ID),其余是超过零的三个位置的非负固定精度小数(即,ID后面的每个列长度为5个字符0.000)。

在具有256GB RAM的Linux机器中转置文件的最有效方法是什么?最终,在转置后,我想将文件分块为500K行,以便我可以开始处理它。硬盘没问题,有70TB的可用空间。

这是我能想到的(每个文件大约需要2.5天)。问题是每个文件的行数和列数都略有不同。我不想在每次运行时修改脚本。或者,我可以制作一个专门的C程序,但我不愿意这样做。

#!/bin/bash
i=$1
mkdir temp-$i
cd temp-$i
echo "Splitting $i"
split -dl 1 -a 4 ../$i
echo "Transposing all lines"
for a in x???? ; do
    cat $a | sed 's/\t/\n/g' > $a.txt
    mv $a.txt $a
done
echo "Joining all columns"
# Here's where it gets really ugly:
paste x0000 x0001 ... x0999 > a.txt
paste x1000 x1001 ... x1999 > b.txt
paste x2000 x2001 ... x2999 > c.txt
paste x3000 x3001 ... x3999 > d.txt
paste x4000 x4001 ... x4999 > e.txt
paste x5000 x5001 ... x5999 > f.txt
paste x6000 x6001 ... x6999 > g.txt
paste x7000 x7001 ... x7999 > h.txt
paste x8000 x8001 ... x8499 > i.txt
paste ../colinfo-$i.txt a.txt b.txt c.txt d.txt e.txt f.txt g.txt h.txt i.txt > ../tr-$i.txt
cd ../
rm -Rf temp-$i

6 个答案:

答案 0 :(得分:2)

粘贴操作会影响您的性能。如何创建尽可能多的文件,然后使用单个传递输入文件,将数据写入列文件。 I. e。:

输入文件:

ID     data1 data2 data3 .... data5000
94239  0.001 0.002 0.003 .... 5.000
43244  0.011 0.012 0.013 .... 5.010

输出文件:

COL0:

ID    94239   43244

COL1:

data1 0.001   0.011

......这里有另外4999个文件......

col5000:

data5000 5.000 5.010

可以使用此perl程序完成拆分列:

#!perl -n

use strict;
use warnings;
use File::Path 'make_path';

$INPUT_RECORD_SEPARATOR = "\t";

my $colno = 0;
my $maxcol = 0;
while(my $col = <STDIN>) {
    $colno = 0 if $col =~ s/\n//;
    $colno++;
    my $path = join '/', $colno =~ /(\d{3})/g;
    if($colno > $maxcol) {
        make_path $path;
        $maxcol = $colno;
    }
    open my $OUT, '>>', "$path/col.tsv";
    print $OUT "$col\t";
    close $OUT;
}

(未测试!)

然后在最后将这些文件连接在一起:

cat col0 col1 ... col5000 > newfile.tsv

(可能需要xargs。)

答案 1 :(得分:1)

https://gitlab.com/ole.tange/tangetools/raw/master/transpose/transpose

cat table.csv | transpose -d , > transposed.csv
transpose -d '\t' table.tsv > transposed.tsv

这是一个通用工具,您可以在其中提供分隔符和要使用的缓冲区大小。

它的工作原理是将table.csv切成10M块(可以用-b调整,并且应该是FreeMem / NumberOfCores / 10的顺序)。然后将它们并行转换(每个CPU内核一个)。最后,将转置的块粘贴在一起。

它假设每一行都是一个完整的CSV记录(即引用\ n将不起作用)。

速度:3 GB CSV文件1000000x300表格的2.5分钟。

速度:3 GB CSV文件300x1000000表5分钟。

答案 2 :(得分:1)

您的文件大小非常规则,因此可以寻找数据的准确位置,并在一次性IO读/写操作中获得转置结果。

它只需要计算第一行的长度,其他行都是5字节的数字。

Input File example:
ID  data1   data2   data3   data4   data5   data6   data7 ...
78028   0.185   0.146   0.910   0.458   0.223   0.853   0.215 ...
76877   0.049   0.486   0.313   0.777   0.599   0.197   0.676 ...
81636   0.055   0.640   0.081   0.477   0.713   0.866   0.308 ...

数据'0.049'(目标行= 2,col = 3)的文件偏移量为:length(标题行)+ length(eachdataline)*(col-2)+ row *(5 + 1)

以下python代码适用于我的测试数据

#encoding=utf-8
import sys
from sys import argv
import os
import time

start = time.time()
def elapsed():
    return time.time() - start

def transpose(filepath):
    # calculate offset for header line
    header_item_offsets = []
    org_rowcnt, org_colcnt = 0, 0
    with open(filepath) as infile:
        for line in infile:
            if org_rowcnt==0:
                headeritems = line[:-1].split('\t')     # remove \n, then split by \t
                org_colcnt = len(headeritems)
                offset = 0
                offset_headerline = len(line)      # + 1 #windows end with \r\n
                offset_dataline = org_colcnt*6    # +1 # windows end with \r\n
                for item in headeritems:
                    header_item_offsets.append(offset)
                    offset += len(item)+1
                # append one more offset for the last item
                header_item_offsets.append(offset)
            org_rowcnt += 1

    with open(filepath,'rb') as infile, open( filepath+'_transed.txt','w') as outfile:
        # transpose
        for row in xrange(org_colcnt):
            line = []
            for col in xrange(org_rowcnt):
                if col==0:
                    # data from header line
                    offset = header_item_offsets[row]
                    readsize = header_item_offsets[row+1] - header_item_offsets[row] - 1
                else:
                    # data from data line offset = header_offset + eachline_offset*(col-1) + row*6
                    offset = offset_headerline + offset_dataline*(col-1) + row*6
                    readsize = 5
                infile.seek(offset)
                line.append( infile.read(readsize) )
            outfile.write( '\t'.join(line) + '\n' )
            if row%10000==0:
                print "Generating row %d" % row

if __name__=="__main__":
    script, filepath = argv
    transpose(filepath)
    print "Running Time:\t", elapsed()

答案 3 :(得分:0)

你正在写每个字节4次:split,sed,paste和final。

如果我们可以避免多次写入字节,我们将节省时间。

这个脚本一次完成拆分和sed。

粘贴和决赛一气呵成。

因此,您只需将数据写入临时空间一次。

最后它使用&lt;()bash结构:

paste <(paste t{1..999}) <(paste t{1000..2000})

这将寻求很多,所以如果你可以改变磁盘的预读,那么这将是一个非常好的主意。 (blockdev --setra和--setfra)

脚本:

#!/usr/bin/perl                                                                                                                         

use English;

while(<>) {
    s/,/\n/g;
    open(OUT,">","t$NR") or die;
    print OUT $_;
    close OUT;
}

my $nfiles = $NR;

@cmd = ("paste ");
my $last = 0;
for my $n (1..$nfiles) {
    if(not $n % 1000) {
    push @cmd, "<(paste t{".($last+1)."..".$n."})";
    $last = $n;
    }
}
if(not $last == $nfiles) {
    push @cmd, "<(paste t{".($last+1)."..".$nfiles."})";
}
system "bash -c '@cmd'";

致电:

cat table | mysplit.pl | split -C 500000; rm t*

答案 4 :(得分:0)

如果到目前为止没有一个解决方案足够快,我相信这将是。

我们知道每一行都是39.3M *(5 + 1)+一点。因此,找到每一行的确切起始位置相对容易。这将花费大约8.5k寻求+读取。

鉴于8.5k起始位置我们做:

for s (startpositions) {
    push r, record.init(s)
}
nrows = len(startpositions)

while (not finished) {
    for row (1..nrows-1) {
        print r[row].get(),"\t"
    }
    print r[nrows].get(),"\n"
}

class record:
init(position) {
    pos = position
}

get() {
    if values.empty and not end_of_line {
        seek pos
        buffer += read(1MB)
        pos += 1MB
        if buffer =~ s/\n.*// {
            end_of_line = true;
        }
        values = split /\t/, buffer
        buffer = pop values
    }
    return pop values
}

这样我们每1 MB只进行1次搜索,我们只读取一次文件,没有临时文件。

1 MB /记录应该总共缓存8.5 GB,因此在256 GB计算机上增加1 MB到20 MB应该是安全的,每20 MB数据提供1次搜索。

答案 5 :(得分:0)

如果您允许像mzedeler建议的临时文件,并希望保持这对我有用:

inFile="bigTable.csv"
sep=","
outFile="bigTable.transposed.csv"

TMPWD=`mktemp -d --tmpdir=$TMPDIR transpose.XXXXXXXX`

prefix=$TMPWD/col

while read line; do
  cat \
    | mawk -vFS="$sep" -f  <(
      #generate AWK-code and print first entries to the temporary files
      echo $line \
        | mawk -vFS="$sep" -vprefix=$prefix '
            {
              printf("{")
              endCmd="END {"
              for(i=1;i<=NF;i+=1){
                file=sprintf(prefix"%05d",i)
                printf("%s",$i) > file
                printf("printf(\"%s%%s\",$%s) >> \"%s\"; ",FS,i,file)
                endCmd= endCmd""sprintf("printf(\"\\n\") >> \"%s\"; ",file)
              }
              endCmd=endCmd"}"
              printf("} ")
              print endCmd
            }
          '
    )
done < <( cat $inFile )

cat ${prefix}* > $outFile

rm -rf $TMPWD

不可否认,我只有12x25M,但有了这个,它在大约2分钟内完成。我试图通过包含代码生成步骤来避免AWK中的for循环。内部代码位于以下表单:

{printf(",%s",$1) >> "tmp/col00001"; ... printf(",%s",$N) >> "tmp/col0000N"; }
END {printf("\n") >> "tmp/col00001"; ... printf("\n") >> "tmp/col0000N" }