如何在不分割多行记录的情况下有效地分割大型文本文件?


9

我有一个大的文本文件(gz'ed时为〜50Gb)。该文件包含4*N行或N记录;即每条记录由4行组成。我想将此文件拆分为4个较小的文件,每个文件的大小约为输入文件的25%。如何在记录边界分割文件?

天真的方法是zcat file | wc -l获取行数,将其除以4,然后使用split -l <number> file。但是,这会翻遍文件两次,并且行计数非常慢(36分钟)。有没有更好的办法?

这很接近,但不是我想要的。接受的答案也会进行行计数。

编辑:

该文件包含fastq格式的测序数据。两条记录如下所示(匿名):

@NxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxGCGA+ATAGAGAG
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxTTTATGTTTTTAATTAATTCTGTTTCCTCAGATTGATGATGAAGTTxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
+
AAAAA#FFFFFFFFFFFFAFFFFF#FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF<AFFFFFFFFFFAFFFFFFFFFFFFFFFFFFF<FFFFFFFFFAFFFAFFAFFAFFFFFFFFAFFFFFFAAFFF<FAFAFFFFA
@NxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxGCGA+ATAGAGAG
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxCCCTCTGCTGGAACTGACACGCAGACATTCAGCGGCTCCGCCGCCxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
+
AAAAA#FFFFF7FFFFFFAFFFFA#F7FFFFFFFFF7FFFFFAF<FFFFFFFFFFFFFFAFFF.F.FFFFF.FAFFF.FFFFFFFFFFFFFF.)F.FFA))FFF7)F7F<.FFFF.FFF7FF<.FFA<7FA.<.7FF.FFFAFF

每条记录的第一行以开头@

编辑2:

zcat file > /dev/null 需要31分钟。

EDIT3: Onlye第一行以开头@。其他任何人都不会。看这里。记录需要保持秩序。向结果文件中添加任何内容都是不正确的。


单程zcat file > /dev/null需要多长时间?
choroba

您能否提供所涉及文件的小样本?
FloHimself

您说每条记录都始于,@并且每条记录有4行。这些都是绝对的吗?-并且第2、3、4行可以以@吗?文件中是否存在页脚行的非记录头?
Peter.O 2015年

1
您是否正在寻找可处理压缩输入和/或产生压缩输出的解决方案?您是否正在寻找四个大小相等的压缩文件?
Stephen Kitt 2015年

Answers:


4

我认为您无法做到这一点-不可靠,也不是您要求的方式。事实是,压缩文件的压缩率可能不会从头到尾均匀分布-压缩算法将在某些部分比其他部分更好地应用。这就是它的工作原理。因此,您不能将压缩文件的大小作为分割因素。

而且,gzip不支持存储大于4GB的压缩文件的原始大小-它无法处理。因此,您无法查询存档以获取可靠的大小-因为它会使您蒙昧。

4行内容-确实很容易。4文件的东西-我只是不知道如何才能可靠地,均匀地分布它,而无需先提取档案来获得其未压缩的大小。我认为您无法做到,因为我尝试过。

但是,您可以做的是为拆分的输出文件设置最大大小,并确保这些文件总是在记录障碍处被破坏。您可以轻松做到。这是一个小脚本,它可以通过提取gzip档案文件,然后通过一些dd带有特定count=$rpt参数的显式管道缓冲区将内容传递给管道,然后动态地lz4对每个文件进行解压缩。我还提出了一些小tee技巧,以便将每个段的最后四行也打印到stderr。

(       IFS= n= c=$(((m=(k=1024)*k)/354))
        b=bs=354xk bs=bs=64k
        pigz -d </tmp/gz | dd i$bs o$b |
        while   read -r line _$((n+=1))
        do      printf \\n/tmp/lz4.$n\\n
        { {     printf %s\\n "$line"
                dd count=$c i$b o$bs
        }|      tee /dev/fd/3|lz4 -BD -9 >/tmp/lz4.$n
        } 3>&1| tail -n4 |tee /dev/fd/2 |
                wc -c;ls -lh /tmp/[gl]z*
        done
)

这将一直继续下去,直到处理完所有输入。它不会尝试按一定百分比进行拆分(无法获得),而是按每次拆分的最大原始字节数进行拆分。而且无论如何,问题的很大一部分是您无法在归档文件中获得可靠的大小,因为它太大了-无论您做什么,都不要再这样做-使拆分的碎片小于4GB , 也许。至少,这个小脚本使您无需写未压缩的字节到磁盘即可执行此操作。

这是精简要点的简短版本-并非所有报告内容都包括在内:

(       IFS= n= c=$((1024*1024/354))
        pigz -d | dd ibs=64k obs=354xk |
        while   read -r line _$((n+=1))
        do {    printf %s\\n "$line"
                dd count=$c obs=64k ibs=354xk
        }  |    lz4 -BD -9  >/tmp/lz4.$n
        done
)  </tmp/gz

它的功能与第一个相同,主要是,它没有太多要说的。另外,杂波也更少了,因此也许更容易看到正在发生的事情。

IFS=东西只是处理一个read每次迭代线。我们read之所以这样,是因为我们需要循环在输入结束时结束。这取决于您的记录大小 - 在您的示例中,每个记录大小为354个字节。gzip为了测试它,我创建了一个4GB的存档,其中包含一些随机数据。

随机数据是通过以下方式获得的:

(       mkfifo /tmp/q; q="$(echo '[1+dPd126!<c]sc33lcx'|dc)"
        (tr '\0-\33\177-\377' "$q$q"|fold -b144 >/tmp/q)&
        tr '\0-\377' '[A*60][C*60][G*60][N*16][T*]' | fold -b144 |
        sed 'h;s/^\(.\{50\}\)\(.\{8\}\)/@N\1+\2\n/;P;s/.*/+/;H;x'|
        paste "-d\n" - - - /tmp/q| dd bs=4k count=kx2k  | gzip
)       </dev/urandom >/tmp/gz 2>/dev/null

...但是也许您不必为此担心太多,因为您已经拥有了所有数据。返回解决方案...

基本上pigz-似乎比解压缩要快一些zcat-通过管道传输未压缩的流,并将dd缓冲区输出到写块,该写块的大小专门为354字节的倍数。该循环将read$line每次迭代中测试一次输入是否仍然到达,printf然后printf在调用lz4另一个循环之前dd读取该块,以读取特定大小为354字节倍数的块,以与缓冲dd过程保持同步。每次迭代都会有一个简短的读取,这是因为初始的read $line-没关系,因为lz4无论如何,我们都在收集器过程中打印它。

我将其设置为每次迭代将读取大约1gb的未压缩数据,并将其流内压缩到大约650Mb左右。lz4比任何其他有用的压缩方法都快得多-这就是我在这里选择它的原因,因为我不想等待。xz不过,实际压缩可能会做得更好。lz4不过,有一点是,它通常可以接近RAM的速度解压缩-这意味着很多时候,您可以lz4快速压缩档案,而无论如何您都可以将其写入内存。

大公司每次迭代都会做一些报告。两个循环都将打印dd有关传输的原始字节数和速度等的报告。大循环还将在每个循环中输出最后4行输入,并输出相同的字节数,然后是ls我将lz4档案写入其中的目录的。这是几轮输出:

/tmp/lz4.1
2961+1 records in
16383+1 records out
1073713090 bytes (1.1 GB) copied, 169.838 s, 6.3 MB/s
@NTACGTANTTCATTGGNATGACGCGCGTTTATGNGAGGGCGTCCGGAANGC+TCTCTNCC
TACGTANTTCATTGGNATGACGCGCGTTTATGNGAGGGCGTCCGGAANGCTCTCTNCCGAGCTCAGTATGTTNNAAGTCCTGANGNGTNGCGCCTACCCGACCACAACCTCTACTCGGTTCCGCATGCATGCAACACATCGTCA
+
I`AgZgW*,`Gw=KKOU:W5dE1m=-"9W@[AG8;<P7P6,qxE!7P4##,Q@c7<nLmK_u+IL4Kz.Rl*+w^A5xHK?m_JBBhqaLK_,o;p,;QeEjb|">Spg`MO6M'wod?z9m.yLgj4kvR~+0:.X#(Bf
354

-rw-r--r-- 1 mikeserv mikeserv 4.7G Jun 16 08:58 /tmp/gz
-rw-r--r-- 1 mikeserv mikeserv 652M Jun 16 12:32 /tmp/lz4.1

/tmp/lz4.2
2961+1 records in
16383+1 records out
1073713090 bytes (1.1 GB) copied, 169.38 s, 6.3 MB/s
@NTTGTTGCCCTAACCANTCCTTGGGAACGCAATGGTGTGANCTGCCGGGAC+CTTTTGCT
TTGTTGCCCTAACCANTCCTTGGGAACGCAATGGTGTGANCTGCCGGGACCTTTTGCTGCCCTGGTACTTTTGTCTGACTGGGGGTGCCACTTGCAGNAGTAAAAGCNAGCTGGTTCAACNAATAAGGACNANTTNCACTGAAC
+
>G-{N~Q5Z5QwV??I^~?rT+S0$7Pw2y9MV^BBTBK%HK87(fz)HU/0^%JGk<<1--7+r3e%X6{c#w@aA6Q^DrdVI0^8+m92vc>RKgnUnMDcU:j!x6u^g<Go?p(HKG@$4"T8BWZ<z.Xi
354

-rw-r--r-- 1 mikeserv mikeserv 4.7G Jun 16 08:58 /tmp/gz
-rw-r--r-- 1 mikeserv mikeserv 652M Jun 16 12:32 /tmp/lz4.1
-rw-r--r-- 1 mikeserv mikeserv 652M Jun 16 12:35 /tmp/lz4.2

gzip -l仅适用于小于2GiB的未压缩文件IIRC(无论如何,其大小都小于OP的文件)。
斯特凡Chazelas

@StéphaneChazelas-该死。这是我想得到未压缩大小的唯一方法。没有它,这根本不起作用。
mikeserv

4

实际上,无需任何代码即可在记录边界上拆分文件:

zcat your_file.gz | split -l 10000 - output_name_

这将创建每个10000行的输出文件,名称分别为output_name_aa,output_name_ab,output_name_ac,...。输入与您的输入一样大,这将为您提供很多输出文件。用10000四个的任意倍数替换,您可以根据需要将输出文件放大或缩小。不幸的是,与其他答案一样,没有一种好的方法来保证您获得所需数量的(大约)相等大小的输出文件,而无需对输入进行任何猜测。(或者实际上是通过管道传递整个内容wc。)如果记录的大小大约相等(或至少大致均匀分布),则可以尝试得出如下估算:

zcat your_file.gz | head -n4000 | gzip | wc -c

这将告诉您文件的前1000条记录的压缩大小。基于此,您可能可以估算出每个文件中要包含四个文件的行数。(如果您不希望剩下退化的第五个文件,请确保将您的估算值增加一点,或者准备将第五个文件添加到第四个文件的尾部。)

编辑:这是另一个技巧,假设您需要压缩输出文件:

#!/bin/sh

base=$(basename $1 .gz)
unpigz -c $1 | split -l 100000 --filter='pigz -c > _$FILE.gz' - ${base}_

batch=$((`ls _*.gz | wc -l` / 4 + 1))
for i in `seq 1 4`; do
  files=`ls _*.gz | head -$batch`
  cat $files > ${base}_$i.gz && rm $files
done

这将创建许多较小的文件,然后迅速将它们组合在一起。(您可能需要根据文件中的行数来调整-l参数。)假定您使用的是相对较新版本的GNU coreutils(用于split --filter),并且输入文件的大小约为130%。可用磁盘空间。如果没有,请用gzip / zcat代替pigz / unpigz。我听说有些软件库(Java?)无法处理以这种方式连接的gzip文件,但是到目前为止,我还没有遇到任何问题。(pigz使用相同的技巧来并行化压缩。)


如果安装了Pigz,则可以用“ pigz -cd”代替“ zcat”来加快速度。
Drew

2
嗯,我现在才注意到您已经在问题中提到了分裂。但是实际上,几乎所有解决方案都将在后台进行相同的操作。困难的部分是弄清楚每个文件中需要放入多少行。
提请

3

从我所收集检查谷歌的球,并进一步测试7.8吉布后.gz的文件,好像是原始未压缩文件的大小的元数据是不准确的(即错误)大型.gz文件(大于4GiB(也许2GiB一些的版本gzip。请
重新测试gzip的元数据:

* The compressed.gz file is  7.8 GiB ( 8353115038 bytes) 
* The uncompressed  file is 18.1 GiB (19436487168 bytes)
* The metadata says file is  2.1 GiB ( 2256623616 bytes) uncompressed

因此,似乎不可能在不实际解压缩的情况下确定未压缩的大小(至少可以说这有点粗糙!)

无论如何,这是一种在记录边界分割未压缩文件的方法,其中每条记录包含4行

它使用文件大小(以字节为单位)(通过stat)以及awk计数字节(不是字符)。行尾是否为LF| CR| CRLF,此脚本通过内置变量RT)处理行的结束长度。

LC_ALL=C gawk 'BEGIN{"stat -c %s "ARGV[1] | getline inSize
                      segSiz=int(inSize/4)+((inSize%4)==0?0:1)
                      ouSplit=segSiz; segNb=0 }
               { lnb++; bytCt+=(length+length(RT))
                 print $0 > ARGV[1]"."segNb
                 if( lnb!=4 ) next
                 lnb=0
                 if( bytCt>=ouSplit ){ segNb++; ouSplit+=segSiz }
               }' myfile

以下是我用来检查每个文件的行数是否为 mod 4 == 0

for i in myfile  myfile.{0..3}; do
    lc=$(<"$i" wc -l)
    printf '%s\t%s\t' "$i" $lc; 
    (( $(echo $lc"%4" | bc) )) && echo "Error: mod 4 remainder !" || echo 'mod 4 ok'  
done | column -ts$'\t' ;echo

测试输出:

myfile    1827904  mod 4 ok
myfile.0  456976   mod 4 ok
myfile.1  456976   mod 4 ok
myfile.2  456976   mod 4 ok
myfile.3  456976   mod 4 ok

myfile 由以下人员生成:

printf %s\\n {A..Z}{A..Z}{A..Z}{A..Z}—{1..4} > myfile

2

这不是认真的答案!我一直在flex玩,这在大约50Gb的输入文件上(如果有的话,在比我的测试文件大的输入数据上)很可能不起作用:

这对我来说适用于〜1Gb文件input.txt

给定flex输入文件splitter.l

%{
#include <stdio.h>
extern FILE* yyin;
extern FILE* yyout;

int input_size = 0;

int part_num;
int part_num_max;
char **part_names;
%}

%%
@.+ {
        if (ftell(yyout) >= input_size / part_num_max) {
            fclose(yyout);
            if ((yyout = fopen(part_names[++part_num], "w")) == 0) {
                exit(1);
            }
        }
        fprintf(yyout, "%s", yytext);
    }
%%

int main(int argc, char *argv[]) {

    if (argc < 2) {
        return 1;
    } else if ((yyin = fopen(argv[1], "r")) == 0) {
        return 1;
    } else if ((yyout = fopen(argv[2], "w")) == 0) {
        fclose(yyin);
        return 1;
    } else {

        fseek(yyin, 0L, SEEK_END);
        input_size = ftell(yyin);
        rewind(yyin);

        part_num = 0;
        part_num_max = argc - 2;
        part_names = argv + 2;

        yylex();

        fclose(yyin);
        fclose(yyout);
        return 0;
    }
}

生成lex.yy.c并使用以下命令将其编译为splitter二进制文件:

$ flex splitter.l && gcc lex.yy.c -ll -o splitter

用法:

$ ./splitter input.txt output.part1 output.part2 output.part3 output.part4

1Gb input.txt的运行时间:

$ time ./splitter input.txt output.part1 output.part2 output.part3 output.part4

real    2m43.640s
user    0m48.100s
sys     0m1.084s

这里的实际词法处理是如此简单,您真的不能从lex中受益。只需调用getc(stream)并应用一些简单的逻辑即可。另外,您知道吗。(f)lex中的(dot)regex字符匹配除newline之外的任何字符,对吗?这些记录是多行的。
卡兹2015年

@Kaz当你的陈述通常corrent,这实际上在问:提供的数据工作
FloHimself

只是偶然,因为在没有匹配项时有一个默认规则:消耗一个字符并将其打印到输出中!换句话说,您可以仅使用识别@字符的规则来进行文件切换,然后让默认规则复制数据。现在,您可以将规则的一部分数据复制为一个大令牌,然后默认规则将第二行一次获取一个字符。
卡兹2015年

感谢您的澄清。我想知道,您如何用解决该任务txr
FloHimself

我不确定是否会这样做,因为任务是尽可能快地对大量数据执行非常简单的操作。
卡兹2015年

1

这是Python中的一种解决方案,它可以使输入文件一遍遍地遍历输出文件。

有关使用的功能wc -l是,您假设此处的每个记录的大小都相同。在这里可能是正确的,但是即使不是这种情况,下面的解决方案也可以使用。它基本上是使用wc -c或文件中的字节数。在Python中,这是通过os.stat()完成的

所以这是程序的工作方式。我们首先将理想的分割点计算为字节偏移量。然后,您读取输入文件的各行,并将其写入相应的输出文件。当您看到已超出最佳下一个分割点并且处于记录边界时,请关闭最后一个输出文件,然后打开下一个输出文件。

从这个意义上说,该程序是最佳的,它只读取一次输入文件的字节。获取文件大小不需要读取文件数据。所需的存储量与行的大小成正比。但是Python或系统可能具有合理的文件缓冲区来加速I / O。

我添加了参数,用于拆分多少文件以及记录大小,以防将来您要调整此文件。

显然,这也可以翻译成其他编程语言。

另一件事,我不确定带有crlf的Windows是否能像在Unix-y系统上一样正确处理行的长度。如果len()在此处偏离一个,我希望如何调整程序很明显。

#!/usr/bin/env python
import os

# Adjust these
filename = 'file.txt'
rec_size = 4
file_splits = 4

size = os.stat(filename).st_size
splits = [(i+1)*size/file_splits for i in range(file_splits)]
with open(filename, 'r') as fd:
    linecount = 0
    i = 0 # File split number
    out = open('file%d.txt' % i, 'w')
    offset = 0  # byte offset of where we are in the file: 0..size
    r = 0 # where we are in the record: 0..rec_size-1
    for line in fd:
        linecount += 1
        r = (r+1) % rec_size
        if offset + len(line) > splits[i] and r == 1 :
            out.close()
            i += 1
            out = open('file%d.txt' % i, 'w')
        out.write(line)
        offset += len(line)
    out.close()
    print("file %s has %d lines" % (filename, linecount))

它不会在记录边界处分裂。例如。第一个子文件分割发生在具有此输入的第三行之后printf %s\\n {A..Z}{A..Z}{A..Z}{A..Z}—{1..4}
Peter.O 2015年

1

用户FloHimself似乎对TXR解决方案感到好奇。这是使用嵌入式TXR Lisp的一种

(defvar splits 4)
(defvar name "data")

(let* ((fi (open-file name "r"))                 ;; input stream
       (rc (tuples 4 (get-lines fi)))            ;; lazy list of 4-tuples
       (sz (/ (prop (stat name) :size) splits))  ;; split size
       (i 1)                                     ;; split enumerator
       (n 0)                                     ;; tuplecounter within split
       (no `@name.@i`)                           ;; output split file name
       (fo (open-file no "w")))                  ;; output stream
  (whilet ((r (pop rc)))  ;; pop each 4-tuple
    (put-lines r fo) ;; send 4-tuple into output file
    ;; if not on the last split, every 1000 tuples, check the output file
    ;; size with stat and switch to next split if necessary.
    (when (and (< i splits)
               (> (inc n) 1000)
               (>= (seek-stream fo 0 :from-current) sz))
      (close-stream fo)
      (set fo (open-file (set no `@name.@(inc i)`) "w")
           n 0)))
  (close-stream fo))

笔记:

  1. 出于相同的原因,pop从元组的惰性列表中对每个元组执行ping操作很重要,这样就消耗了惰性列表。我们一定不能保留对该列表开头的引用,因为当我们浏览文件时,内存将增加。

  2. (seek-stream fo 0 :from-current)是的无操作情况seek-stream,它通过返回当前位置而非常有用。

  3. 表现:别提了。可用,但不会带来任何奖杯。

  4. 由于我们仅每1000个元组进行一次大小检查,因此我们可以使元组大小为4000行。


0

如果您不需要将新文件作为原始文件的连续块,则可以sed通过以下方式完全完成此操作:

sed -n -e '1~16,+3w1.txt' -e '5~16,+3w2.txt' -e '9~16,+3w3.txt' -e '13~16,+3w4.txt'

-n从印刷每一行停止它,每一个的-e剧本基本上是做同样的事情。1~16匹配第一行,之后每16行。,+3均值匹配每行后面的三行。w1.txt表示将所有这些行都写入文件1.txt。这是从第4组第4行开始,每4行第4组记录并将其写入文件。其他三个命令执行相同的操作,但是它们每个都向前移动了4行,并写入了不同的文件。

如果文件与您布置的规范不完全匹配,这将导致严重破坏,但否则它将按预期工作。我没有分析它,所以我不知道它的效率如何,但是sed在流编辑方面是相当有效的。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.