unix: divide un gran archivo .gz por línea

16

Estoy seguro de que alguien ha tenido la siguiente necesidad, ¿cuál es una forma rápida de dividir un gran archivo .gz por línea? El archivo de texto subyacente tiene 120 millones de filas. No tengo suficiente espacio en el disco para comprimir todo el archivo a la vez, así que me preguntaba si alguien conoce un script o herramienta bash / perl que pueda dividir el archivo (ya sea .gz o .txt interno) en archivos de 3x 40 millones de líneas . es decir, llamarlo así:

    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

Tal vez hacer una serie de estos sea una solución o el gunzip -c requeriría suficiente espacio para descomprimir todo el archivo (es decir, el problema original): gunzip -c hugefile.txt.gz | cabeza 4000000

Nota: No puedo obtener un disco extra.

¡Gracias!

toop
fuente
1
¿Desea que los archivos resultantes se vuelvan a grabar?
Puedes usar gunzip en un ipe. El resto se puede hacer con la cabeza y la cola
Ingo
@Tichodroma: no, no los necesito gziped de nuevo. Pero no pude almacenar todos los archivos de texto dividido a la vez. Así que me gustaría obtener la primera división, hacer cosas con ella, luego eliminar la primera división y luego obtener la segunda división.etc finalmente eliminar el gz
toop
1
@toop: Gracias por la aclaración. Tenga en cuenta que generalmente es mejor editar su pregunta si desea aclararla, en lugar de ponerla en un comentario; de esa manera todos lo verán.
sleske
La respuesta aceptada es buena si solo desea una fracción de los fragmentos y no los conoce de antemano. Si desea generar todos los fragmentos a la vez, las soluciones basadas en la división serán mucho más rápidas, O (N) en lugar de O (N²).
b0fh

Respuestas:

11

Cómo hacer esto mejor depende de lo que quieras:

  • ¿Desea extraer una sola parte del archivo grande?
  • ¿O quieres crear todas las partes de una vez?

Si desea una sola parte del archivo , su idea es usar gunzipy heades correcta. Puedes usar:

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

Eso generaría las primeras 4000000 líneas en la salida estándar: probablemente desee agregar otra tubería para hacer algo con los datos.

Para obtener las otras partes, usaría una combinación de heady tail, como:

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

para obtener el segundo bloque.

Tal vez hacer una serie de estos sea una solución o el gunzip -c requeriría suficiente espacio para descomprimir todo el archivo

No, gunzip -cno requiere espacio en disco: hace todo en la memoria y luego lo transfiere a stdout.


Si desea crear todas las partes de una vez , es más eficiente crearlas todas con un solo comando, porque el archivo de entrada solo se lee una vez. Una buena solución es usar split; ver la respuesta de jim mcnamara para más detalles.

sleske
fuente
1
Desde la vista de rendimiento: ¿gzip realmente descomprime todo el archivo? ¿O es capaz de "mágicamente" saber que solo se necesitan 4 millones de líneas?
Alois Mahdal
3
@AloisMahdal: En realidad, esa sería una buena pregunta por separado :-). Versión corta: gzipno conoce el límite (que proviene de un proceso diferente). Si headse usa, headsaldrá cuando haya recibido suficiente, y esto se propagará a gzip(a través de SIGPIPE, consulte Wikipedia). Para tailesto no es posible, entonces sí, gzipdescomprimirá todo.
sleske
Pero si está interesado, realmente debería hacer esto como una pregunta separada.
sleske
20

tubería para dividir use gunzip -c o zcat para abrir el archivo

gunzip -c bigfile.gz | split -l 400000

Agregue especificaciones de salida al comando de división.

jim mcnamara
fuente
3
Esto es masivamente más eficiente que la respuesta aceptada, a menos que solo requiera una fracción de los fragmentos divididos. Por favor vota.
b0fh
1
@ b0fh: Sí, tienes razón. Upvoted y referenciado en mi respuesta :-).
sleske
La mejor respuesta es segura.
Stephen Blum
¿Cuáles son las especificaciones de salida para que las salidas sean archivos .gz?
Quetzalcóatl
7

Como está trabajando en una secuencia (no rebobinable), querrá usar la forma de cola '+ N' para obtener líneas que comiencen desde la línea N en adelante.

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
zgpmax
fuente
4

Consideraría usar split .

dividir un archivo en pedazos

Michael Krelin - hacker
fuente
3

Divide directamente el archivo .gz en archivos .gz:

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

Creo que esto es lo que OP quería, porque no tiene mucho espacio.

siulkilulki
fuente
2

Aquí hay un script de Python para abrir un conjunto global de archivos de un directorio, comprimirlos si es necesario y leerlos línea por línea. Solo usa el espacio necesario en la memoria para guardar los nombres de archivo y la línea actual, más un poco 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)

El comando de línea de impresión enviará cada línea a la salida estándar, para que pueda redirigir a un archivo. Alternativamente, si nos hace saber lo que quiere hacer con las líneas, puedo agregarlo al script de Python y no necesitará dejar trozos del archivo.

Spencer Rathbun
fuente
2

Aquí hay un programa perl que se puede usar para leer stdin y dividir las líneas, canalizando cada grupo a un comando separado que puede usar una variable de shell $ SPLIT para enrutarlo a un destino diferente. Para su caso, se invocaría con

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

Lo sentimos, el procesamiento de la línea de comandos es un poco torpe, pero se entiende la idea.

#!/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;
Liudvikas Bukys
fuente