unix - divide um enorme arquivo .gz por linha


16

Tenho certeza que alguém teve a necessidade abaixo, qual é uma maneira rápida de dividir um arquivo .gz enorme por linha? O arquivo de texto subjacente possui 120 milhões de linhas. Eu não tenho espaço em disco suficiente para compactar o arquivo inteiro de uma só vez, então eu queria saber se alguém conhece um script ou ferramenta bash / perl que possa dividir o arquivo (o .gz ou o .txt interno) em arquivos de linha de 3x 40mn . ou seja, chamando assim:

    bash splitter.sh hugefile.txt.gz 4000000 1
 would get lines 1 to 40 mn    
    bash splitter.sh hugefile.txt.gz 4000000 2
would get lines 40mn to 80 mn
    bash splitter.sh hugefile.txt.gz 4000000 3
would get lines 80mn to 120 mn

Talvez esteja fazendo uma série dessas soluções ou o gunzip -c exigiria espaço suficiente para que o arquivo inteiro fosse descompactado (ou seja, o problema original): gunzip -c hugefile.txt.gz | cabeça 4000000

Nota: Não consigo obter disco extra.

Obrigado!


1
Deseja que os arquivos resultantes sejam compactados novamente em gzip?

Você pode usar o gunzip em um ipê. O resto pode ser feito com cabeça e cauda
Ingo

@ Tichodroma - não, eu não preciso deles gziped novamente. Mas não consegui armazenar todos os arquivos de texto divididos de uma só vez. Então eu gostaria de obter a primeira divisão, fazer coisas com ele, em seguida, elimine a primeira divisão, e em seguida, obter a 2ª split.etc finalmente remover a GZ originais
toop

1
@top: Obrigado pelo esclarecimento. Observe que geralmente é melhor editar sua pergunta se você quiser esclarecê-la, em vez de colocá-la em um comentário; Dessa forma, todos verão.
sleske

A resposta aceita é boa se você deseja apenas uma fração dos pedaços e não os conhece com antecedência. Se você deseja gerar todos os blocos de uma só vez, as soluções baseadas na divisão serão muito mais rápidas, O (N) em vez de O (N²).
precisa saber é

Respostas:


11

Como fazer isso melhor depende do que você deseja:

  • Deseja extrair uma única parte do arquivo grande?
  • Ou você deseja criar todas as partes de uma só vez?

Se você deseja uma única parte do arquivo , sua ideia é usar gunzipe headestá certa. Você pode usar:

gunzip -c hugefile.txt.gz | head -n 4000000

Isso produziria as primeiras 4000000 linhas na saída padrão - você provavelmente deseja acrescentar outro canal para realmente fazer alguma coisa com os dados.

Para obter as outras partes, você usaria uma combinação de heade tail, como:

gunzip -c hugefile.txt.gz | head -n 8000000 |tail -n 4000000

para pegar o segundo bloco.

Talvez esteja fazendo uma série dessas soluções ou o gunzip -c exigiria espaço suficiente para que o arquivo inteiro fosse descompactado

Não, gunzip -cele não requer espaço em disco - ele faz tudo na memória e o envia para o stdout.


Se você deseja criar todas as partes de uma só vez , é mais eficiente criá-las todas com um único comando, porque o arquivo de entrada é lido apenas uma vez. Uma boa solução é usar split; veja a resposta de jim mcnamara para obter detalhes.


1
Do ponto de vista de desempenho: o gzip realmente descompacta o arquivo inteiro? Ou é capaz de "magicamente" saber que são necessárias apenas linhas de 4 milhões?
Alois Mahdal

3
@AloisMahdal: Na verdade, essa seria uma boa pergunta separada :-). Versão curta: gzipnão sabe sobre o limite (que vem de um processo diferente). Se headfor usado, headsairá quando tiver recebido o suficiente, e isso será propagado para gzip(via SIGPIPE, consulte Wikipedia). Como tailisso não é possível, então sim, gzipdescomprimirá tudo.
22412 sleske

Mas se você estiver interessado, você deve realmente fazer isso como uma pergunta separada.
sleske

20

pipe para dividir use gunzip -c ou zcat para abrir o arquivo

gunzip -c bigfile.gz | split -l 400000

Adicione especificações de saída ao comando de divisão.


3
Isso é massivamente mais eficiente do que a resposta aceita, a menos que você precise apenas de uma fração dos pedaços divididos. Voto por favor.
b0fh

1
@ b0fh: Sim, você está certo. Votado, e mencionado na minha resposta :-).
Sleske

Melhor resposta, com certeza.
Stephen Blum

quais são as especificações de saída para que as saídas sejam arquivos .gz?
Quetzalcoatl

7

Enquanto você está trabalhando em um fluxo (não rebobinável), convém usar a forma '+ N' de cauda para obter linhas começando na linha N em diante.

zcat hugefile.txt.gz | head -n 40000000
zcat hugefile.txt.gz | tail -n +40000001 | head -n 40000000
zcat hugefile.txt.gz | tail -n +80000001 | head -n 40000000


3

Divida diretamente o arquivo .gz em arquivos .gz:

zcat bigfile.gz | split -l 400000 --filter='gzip > $FILE.gz'

Eu acho que é isso que o OP queria, porque ele não tem muito espaço.


2

Aqui está um script python para abrir um conjunto global de arquivos de um diretório, compactá-los, se necessário, e lê-los linha por linha. Ele usa apenas o espaço necessário na memória para armazenar os nomes de arquivos e a linha atual, além de um pouco de sobrecarga.

#!/usr/bin/env python
import gzip, bz2
import os
import fnmatch

def gen_find(filepat,top):
    for path, dirlist, filelist in os.walk(top):
        for name in fnmatch.filter(filelist,filepat):
            yield os.path.join(path,name)

def gen_open(filenames):
    for name in filenames:
        if name.endswith(".gz"):
            yield gzip.open(name)
        elif name.endswith(".bz2"):
            yield bz2.BZ2File(name)
        else:
            yield open(name)

def gen_cat(sources):
    for s in sources:
        for item in s:
            yield item

def main(regex, searchDir):
    fileNames = gen_find(regex,searchDir)
    fileHandles = gen_open(fileNames)
    fileLines = gen_cat(fileHandles)
    for line in fileLines:
        print line

if __name__ == '__main__':
    parser = argparse.ArgumentParser(description='Search globbed files line by line', version='%(prog)s 1.0')
    parser.add_argument('regex', type=str, default='*', help='Regular expression')
    parser.add_argument('searchDir', , type=str, default='.', help='list of input files')
    args = parser.parse_args()
    main(args.regex, args.searchDir)

O comando print line envia todas as linhas para std, para que você possa redirecionar para um arquivo. Como alternativa, se você nos informar o que deseja fazer com as linhas, eu posso adicioná-lo ao script python e você não precisará deixar pedaços do arquivo por aí.


2

Aqui está um programa perl que pode ser usado para ler stdin e dividir as linhas, canalizando cada grupo para um comando separado que pode usar uma variável de shell $ SPLIT para rotear para um destino diferente. Para o seu caso, seria invocado com

zcat hugefile.txt.gz | perl xsplit.pl 40000000 'cat > tmp$SPLIT.txt; do_something tmp$SPLIT.txt; rm tmp$SPLIT.txt'

Desculpe, o processamento da linha de comando é um pouco complicado, mas você entendeu.

#!/usr/bin/perl -w
#####
# xsplit.pl: like xargs but instead of clumping input into each command's args, clumps it into each command's input.
# Usage: perl xsplit.pl LINES 'COMMAND'
# where: 'COMMAND' can include shell variable expansions and can use $SPLIT, e.g.
#   'cat > tmp$SPLIT.txt'
# or:
#   'gzip > tmp$SPLIT.gz'
#####
use strict;

sub pipeHandler {
    my $sig = shift @_;
    print " Caught SIGPIPE: $sig\n";
    exit(1);
}
$SIG{PIPE} = \&pipeHandler;

my $LINES = shift;
die "LINES must be a positive number\n" if ($LINES <= 0);
my $COMMAND = shift || die "second argument should be COMMAND\n";

my $line_number = 0;

while (<STDIN>) {
    if ($line_number%$LINES == 0) {
        close OUTFILE;
        my $split = $ENV{SPLIT} = sprintf("%05d", $line_number/$LINES+1);
        print "$split\n";
        my $command = $COMMAND;
        open (OUTFILE, "| $command") or die "failed to write to command '$command'\n";
    }
    print OUTFILE $_;
    $line_number++;
}

exit 0;
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.