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

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

我有一个很大的文本文件(gz 后约为 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

每个记录的第一行都以 a 开头@(不幸的是,也可以@在每个记录的第三行;请参阅下面的 brendans 评论)

编辑2:

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

编辑3: 仅第一行以 开头@。其他人都不会。看这里。记录需要保持有序。向生成的文件添加任何内容都是不行的。

答案1

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

更重要的是,gzip只是不支持存储大小超过 4GB 的压缩文件的原始大小 - 它无法处理它。因此,您无法查询存档以获得可靠的大小 - 因为它会欺骗您。

4 行的事情 - 这真的很简单。 4 个文件的事情 - 我只是不知道如何在不首先提取存档以获得其未压缩大小的情况下可靠且均匀分布地做到这一点。我不认为你可以,因为我尝试过。

然而,你什么do,设置分割输出文件的最大大小,并确保这些文件始终在记录障碍处被破坏。您可以轻松做到。这是一个小脚本,它将通过提取存档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
)

这将继续下去,直到处理完所有输入。它不会尝试按一定百分比进行分割(这是它无法获得的),而是按照每次分割的最大原始字节数进行分割。不管怎样,你的问题的一个重要部分是你无法获得你的存档的可靠大小,因为它太大了 - 无论你做什么,都不要再这样做 - 使分割小于 4gbs 一块这一轮, 或许。这个小脚本至少使您能够执行此操作,而无需将未压缩的字节写入磁盘。

这是一个精简版,只保留了基本内容 - 它没有添加所有报告内容:

(       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 字节。我用一些随机数据创建了一个 4+GB 的gzip存档来测试它。

随机数据是这样获得的:

(       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迭代时测试一次输入是否仍然到达,然后printfprintf调用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

答案2

在记录边界上分割文件实际上非常简单,无需任何代码:

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 使用相同的技巧来并行压缩。)

答案3

根据我在检查 google-sphere 并进一步测试 7.8 GiB 文件后收集到的信息.gz,原始未压缩文件大小的元数据似乎不准确(即。错误的)对于大.gz文件(大于 4GiB(对于某些版本的 可能是 2GiB gzip)。Re
.我对 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

答案4

这是 Python 中的一个解决方案,它可以在输入文件进行时写入输出文件。

使用的一个特点wc -l是,您假设此处的每个记录的大小相同。此处可能是这样,但即使情况并非如此,下面的解决方案也有效。它基本上是使用wc -c或文件中的字节数。在 Python 中,这是通过以下方式完成的os.stat()

该程序的工作原理如下。我们首先将理想的分割点计算为字节偏移量。然后读取输入文件的行并将其写入相应的输出文件。当您发现已经超出了最佳下一个分割点时您处于记录边界,关闭最后一个输出文件并打开下一个。

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

我添加了要分割的文件数量以及记录大小的参数,以便您将来想要调整。

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

另一件事,我不确定带有 crlf 的 Windows 是否能像在 Unix-y 系统上那样正确处理行的长度。如果 len() 在这里减少了 1,我希望如何调整程序是显而易见的。

#!/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))

相关内容