2

我有几个巨大的制表符分隔文件 @ 每个 ~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
4

6 回答 6

2

粘贴操作正在扼杀你的表现。如何创建与列一样多的文件,然后使用单次通过输入文件,将数据写入列文件。IE。:

输入文件:

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可能需要。)

于 2013-04-03T21:41:16.587 回答
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 分钟。

于 2013-04-09T10:04:00.413 回答
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'(target row=2, col=3)的文件偏移量为:length(headerline)+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()
于 2016-03-10T02:56:56.820 回答
0

您将每个字节写入 4 次:split、sed、paste 和 final。

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

该脚本一次性完成拆分和 sed。

并且一次性完成粘贴和最终处理。

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

最后,它使用 <() 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*
于 2013-04-09T13:37:25.437 回答
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 次搜索。

于 2013-04-09T15:02:40.693 回答
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" }
于 2016-06-24T14:00:27.247 回答