Como dividir eficientemente um arquivo de texto grande sem dividir registros de múltiplas linhas?

9

Eu tenho um arquivo de texto grande (~ 50Gb quando gz'ed). O arquivo contém 4*N lines ou N records; isso é todo registro consiste em 4 linhas. Eu gostaria de dividir este arquivo em 4 arquivos menores, cada um com aproximadamente 25% do arquivo de entrada. Como posso dividir o arquivo no limite do registro?

Uma abordagem ingênua seria zcat file | wc -l para obter a contagem de linhas, dividir esse número por 4 e, em seguida, usar split -l <number> file . No entanto, isso passa pelo arquivo duas vezes e o contador de linhas é extremamente lento (36mins). Existe uma maneira melhor?

Este se aproxima, mas não é o que estou procurando. A resposta aceita também faz uma contagem de linha.

EDITAR:

O arquivo contém dados de seqüenciamento no formato fastq. Dois registros se parecem com isso (anônimos):

@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 primeira linha de cada registro começa com @ .

EDIT2:

zcat file > /dev/null demora 31 min.

EDIT3: Somente a primeira linha começa com @ . Nenhum dos outros nunca vai. Veja aqui . Registros precisam ficar em ordem. Não há problema em adicionar nada ao arquivo resultante.

    
por Rolf 16.06.2015 / 09:55

7 respostas

4

Eu não acho que você pode fazer isso - não de forma confiável, e não do jeito que você pergunta. O problema é que a taxa de compressão do arquivo provavelmente não será distribuída uniformemente da cabeça à cauda - o algoritmo de compressão se aplicará melhor a algumas partes do que a outras. É assim que funciona. E assim você não pode fatorar sua divisão no tamanho do arquivo compactado.

Além disso, gzip não suporta o armazenamento do tamanho original de arquivos compactados com tamanho superior a 4 gbs - ele não pode lidar com isso. E assim você não pode consultar o arquivo para obter um tamanho confiável - porque ele vai enganar você.

A coisa das 4 linhas - é bem fácil, na verdade. A coisa de 4 arquivos - eu simplesmente não sei como você pode fazê-lo de forma confiável e com uma distribuição uniforme sem primeiro extrair o arquivo para obter o tamanho descompactado. Eu não acho que você pode porque eu tentei.

No entanto, o que você pode fazer é definir um tamanho máximo para os arquivos de saída divididos e garantir que eles sejam sempre quebrados nas barreiras de registro. Isso você pode fazer facilmente. Aqui está um pequeno script que fará isso extraindo o gzip archive, e canalizando o conteúdo através de alguns dd pipe-buffers explícitos com argumentos count=$rpt específicos, antes de passar por lz4 para descompactar / recompactar cada arquivo no vôo. Eu também joguei alguns truques de pipe tee para imprimir as últimas quatro linhas para cada segmento para stderr também.

(       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
)

Isso só vai continuar até que tenha manipulado todas as entradas. Ele não tenta dividi-lo por alguma porcentagem - que não pode ser obtida -, mas ao invés disso, ele divide por uma contagem máxima de bytes brutos por divisão. E de qualquer forma, uma grande parte do seu problema é que você não pode obter um tamanho confiável no seu arquivo porque é muito grande - faça o que fizer, não faça isso de novo - faça as partições menores que 4gbs por peça , talvez. Este pequeno script, pelo menos, permite que você faça isso sem nunca escrever um byte descompactado em disco.

Aqui está uma versão mais curta, despojada para o essencial - não adiciona todas as informações do relatório:

(       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

Ele faz todas as mesmas coisas que o primeiro, principalmente, ele simplesmente não tem muito a dizer sobre isso. Além disso, há menos confusão, então é mais fácil ver o que está acontecendo, talvez.

A coisa IFS= é apenas para manipular a linha read por iteração. Nós read one porque precisamos que nosso loop termine quando a entrada terminar. Isso depende do seu registro tamanho - que, por exemplo, é de 354 bytes por. Eu criei um arquivo de 4 + gb gzip com alguns dados aleatórios para testá-lo.

Os dados aleatórios foram assim:

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

... mas talvez você não precise se preocupar tanto com isso, já que você já tem os dados e tudo. De volta à solução ...

Basicamente, pigz - que parece descompactar um pouco mais rápido que o zcat - canaliza o fluxo descompactado e dd armazena essa saída em blocos de gravação dimensionados especificamente em um múltiplo de 354 bytes. O loop irá read a $line uma vez a cada iteração para testar se a entrada ainda está chegando, o que será printf printf at lz4 antes que outro dd seja chamado para ler blocos dimensionados especificamente em um múltiplo de 354 bytes - para sincronizar com o buffer dd process - pela duração. Haverá uma pequena leitura por iteração por causa do read $line inicial - mas isso não importa, porque estamos imprimindo isso em lz4 - nosso processo de coletor - de qualquer forma.

Configurei-o para que cada iteração leia aproximadamente 1 GB de dados não compactados e comprima esse fluxo in-stream para cerca de 650 MB ou mais. lz4 é muito mais rápido do que qualquer outro método útil de compactação - que é a razão pela qual eu o escolhi aqui porque não gosto de esperar. O xz faria um trabalho muito melhor na compactação real, provavelmente. Uma coisa sobre o lz4 , no entanto, é que ele pode ser descompactado perto da velocidade da RAM - o que significa que muitas vezes você pode descompactar um arquivo lz4 tão rápido quanto você poderia gravar na memória.

O grande faz alguns relatórios por iteração. Ambos os loops imprimirão o relatório de dd sobre o número de bytes brutos transferidos e a velocidade e assim por diante. O loop grande também imprime as últimas 4 linhas de entrada por ciclo e uma contagem de bytes para o mesmo, seguido por um ls do diretório no qual eu escrevo os lz4 archives. Aqui estão algumas rodadas de saída:

(       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
    
por 16.06.2015 / 16:22
4

A divisão de arquivos nos limites de registro é realmente muito fácil, sem nenhum código:

zcat your_file.gz | split -l 10000 - output_name_

Isso criará arquivos de saída de 10000 linhas cada, com nomes output_name_aa, output_name_ab, output_name_ac, ... Com uma entrada tão grande quanto a sua, isso fornecerá muitos arquivos de saída. Substitua 10000 por qualquer múltiplo de quatro e você pode tornar os arquivos de saída tão grandes ou pequenos quanto desejar. Infelizmente, como acontece com as outras respostas, não há uma boa maneira de garantir que você obtenha o número desejado de tamanho (aproximadamente) igual dos arquivos de saída sem fazer algumas suposições sobre a entrada. (Ou, na verdade, canalizando tudo por wc .) Se seus registros tiverem aproximadamente o mesmo tamanho (ou, pelo menos, distribuídos uniformemente), você pode tentar chegar a uma estimativa como esta:

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

Isso informa o tamanho compactado dos primeiros 1000 registros do seu arquivo. Com base nisso, você provavelmente poderá obter uma estimativa de quantas linhas deseja em cada arquivo para obter quatro arquivos. (Se você não quiser um quinto arquivo degenerado, certifique-se de aumentar um pouco sua estimativa, ou esteja preparado para colocar o quinto arquivo na cauda do quarto.)

Edit: Aqui está mais um truque, supondo que você quer arquivos de saída compactados:

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

Isso criará muitos arquivos menores e, em seguida, os catará novamente. (Você pode ter que ajustar o parâmetro -l dependendo de quanto tempo as linhas em seus arquivos são.) Ele assume que você tem uma versão relativamente recente do GNU coreutils (para split -filter) e cerca de 130% do tamanho do arquivo de entrada Espaço livre em disco. Substitua gzip / zcat por pigz / unpigz se você não os tiver. Ouvi dizer que algumas bibliotecas de software (Java?) Não conseguem lidar com arquivos gzip concatenados dessa maneira, mas ainda não tive nenhum problema com isso. (o pigz usa o mesmo truque para paralelizar a compactação).

    
por 17.06.2015 / 00:56
3

Pelo que obtenho depois de verificar a esfera do google e testar mais um arquivo 7.8 GiB .gz , parece que os metadados do tamanho original do arquivo não compactado não são precisos (por exemplo, errado ) para grandes arquivos .gz (maiores que 4GiB (talvez 2GiB para algumas versões de gzip ).
Ré. meu teste dos metadados do 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

Portanto, parece que não é possível determinar o tamanho descompactado sem realmente descompactá-lo (o que é um pouco difícil, para dizer o mínimo!)

De qualquer forma, aqui está uma maneira de dividir um arquivo descompactado em limites de registro, onde cada registro contém 4 linhas .

Ele usa o tamanho do arquivo em bytes (via stat ) e com awk contando bytes (não caracteres). Se a terminação da linha é ou não LF | CR | CRLF , este script manipula o comprimento final da linha através da variável incorporada 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

Abaixo está o teste que usei para verificar se a contagem de linhas de cada arquivo é 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

Teste de saída:

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 foi gerado por:

printf %s\n {A..Z}{A..Z}{A..Z}{A..Z}—{1..4} > myfile
    
por 16.06.2015 / 11:48
2

Esta não é uma resposta séria! Estive apenas brincando com flex e isso provavelmente não funcionará em um arquivo de entrada com ~ 50Gb (se for o caso, em dados de entrada maiores do que o meu arquivo de teste):

Isso funciona para mim em um arquivo ~ 1Gb input.txt :

Dado o arquivo de entrada 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;
    }
}

gerando lex.yy.c e compilando-o para o binário splitter com:

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

Uso:

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

Tempo de execução para 1Gb input.txt :

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

real    2m43.640s
user    0m48.100s
sys     0m1.084s
    
por 16.06.2015 / 14:28
1

Aqui está uma solução em Python que faz com que uma pessoa passe pelo arquivo de entrada gravando os arquivos de saída à medida que avança.

Um recurso sobre o uso de wc -l é que você está assumindo que cada um dos registros aqui é do mesmo tamanho. Isso pode ser verdade aqui, mas a solução abaixo funciona mesmo quando não é esse o caso. Basicamente, é usado wc -c ou o número de bytes no arquivo. Em Python, isso é feito via os.stat ()

Então aqui está como o programa funciona. Primeiro calculamos os pontos de divisão ideais como compensações de byte. Então você lê as linhas do arquivo de entrada gravando no arquivo de saída apropriado. Quando você ver que excedeu o próximo ponto de divisão ideal, e você está em um limite de registro, fecha o último arquivo de saída e abre o próximo.

O programa é ótimo nesse sentido, ele lê os bytes do arquivo de entrada uma vez; Obter o tamanho do arquivo não requer a leitura dos dados do arquivo. O armazenamento necessário é proporcional ao tamanho de uma linha. Mas o Python ou o sistema presumivelmente tem buffers de arquivos razoáveis para acelerar a E / S.

Eu adicionei parâmetros para quantos arquivos dividir e qual é o tamanho do registro caso você queira ajustar isso no futuro.

E claramente isso pode ser traduzido para outras linguagens de programação também.

Uma outra coisa, não tenho certeza se o Windows com o seu crlf manipula o comprimento da linha corretamente, como acontece nos sistemas Unix-y. Se len () estiver desligado por um aqui, espero que seja óbvio como ajustar o programa.

#!/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))
    
por 16.06.2015 / 11:12
1

O usuário FloHimself pareceu curioso sobre uma solução de TXR . Aqui está um usando o TXR Lisp incorporado:

(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))

Notas:

  1. Pelo mesmo motivo, pop -ping cada tupla da lista lenta de tuplas é importante, para que a lista lenta seja consumida. Não devemos reter uma referência ao início dessa lista, porque a memória aumentará à medida que passamos pelo arquivo.

  2. (seek-stream fo 0 :from-current) não é o caso de seek-stream , o que se torna útil ao retornar a posição atual.

  3. Desempenho: não mencione isso. Útil, mas não trará troféus para casa.

  4. Como só fazemos o teste de tamanho a cada 1000 tuplas, poderíamos fazer o tamanho da tupla de 4000 linhas.

por 26.06.2015 / 06:17
0

Se você não precisa que os novos arquivos sejam partes contíguas do arquivo original, você pode fazer isso com sed da seguinte maneira:

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

O -n impede que ele imprima cada linha, e cada um dos scripts -e está essencialmente fazendo a mesma coisa. 1~16 corresponde à primeira linha e a cada 16 linhas depois. ,+3 significa coincidir com as próximas três linhas após cada uma delas. w1.txt diz para gravar todas essas linhas no arquivo 1.txt . Isto está tomando todo 4º grupo de 4 linhas e escrevendo-o em um arquivo, começando com o primeiro grupo de 4 linhas. Os outros três comandos fazem a mesma coisa, mas eles são trocados para frente por 4 linhas e escrevem para um arquivo diferente.

Isso irá quebrar horrivelmente se o arquivo não corresponder exatamente à especificação que você definiu, mas caso contrário, ele deve funcionar como você pretendia. Eu não o perfilei, então não sei quão eficiente será, mas sed é razoavelmente eficiente na edição de fluxo.

    
por 12.07.2015 / 13:05