unix - divise un énorme fichier .gz par ligne

16

Je suis sûr que quelqu'un a eu le besoin ci-dessous, quel est un moyen rapide de diviser un énorme fichier .gz par ligne? Le fichier texte sous-jacent comporte 120 millions de lignes. Je n'ai pas assez d'espace disque pour compresser le fichier en entier à la fois, donc je me demandais si quelqu'un connaissait un script ou un outil bash / perl qui pourrait diviser le fichier (soit le .gz ou le .txt interne) en 3 fichiers de ligne de 40mn . c'est-à-dire l'appeler comme:

    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

Peut-être qu'une série de ces solutions est une solution ou le gunzip -c nécessiterait-il suffisamment d'espace pour que le fichier entier soit décompressé (c'est-à-dire le problème d'origine): gunzip -c énormefichier.txt.gz | tête 4000000

Remarque: je ne peux pas obtenir de disque supplémentaire.

Merci!

en haut
la source
1
Voulez-vous que les fichiers résultants soient à nouveau compressés au format gzip?
Vous pouvez utiliser gunzip dans un ipé. Le reste peut être fait avec la tête et la queue
Ingo
@Tichodroma - non, je n'en ai plus besoin. Mais je ne pouvais pas stocker tous les fichiers texte fractionnés à la fois. Je voudrais donc obtenir le premier split, faire des trucs avec lui, puis supprimer le premier split, puis obtenir le 2nd split.etc enfin supprimer le gz original
toop
1
@toop: Merci pour la clarification. Notez qu'il est généralement préférable de modifier votre question si vous souhaitez la clarifier plutôt que de la mettre dans un commentaire; de cette façon, tout le monde le verra.
sleske
La réponse acceptée est bonne si vous ne voulez qu'une fraction des morceaux et ne les connaissez pas à l'avance. Si vous souhaitez générer tous les morceaux en même temps, les solutions basées sur le fractionnement seront beaucoup plus rapides, O (N) au lieu de O (N²).
b0fh

Réponses:

11

La meilleure façon de procéder dépend de ce que vous voulez:

  • Voulez-vous extraire une seule partie du gros fichier?
  • Ou souhaitez-vous créer toutes les pièces en une seule fois?

Si vous voulez une seule partie du fichier , votre idée d'utiliser gunzipet headest juste. Vous pouvez utiliser:

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

Cela produirait les 400 000 premières lignes sur la sortie standard - vous voudrez probablement ajouter un autre canal pour réellement faire quelque chose avec les données.

Pour obtenir les autres parties, vous utiliseriez une combinaison de headet tail, comme:

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

pour obtenir le deuxième bloc.

Est-ce que peut-être une série de ces solutions ou le gunzip -c nécessiterait-il suffisamment d'espace pour que le fichier entier soit décompressé

Non, le gunzip -cne nécessite aucun espace disque - il fait tout en mémoire, puis le diffuse sur stdout.


Si vous souhaitez créer toutes les pièces en une seule fois , il est plus efficace de les créer toutes avec une seule commande, car le fichier d'entrée n'est lu qu'une seule fois. Une bonne solution consiste à utiliser split; voir la réponse de Jim Mcnamara pour plus de détails.

sleske
la source
1
De la vue des performances: gzip décompresse-t-il réellement le fichier entier? Ou est-il capable de savoir "par magie" que seules 4 millions de lignes sont nécessaires?
Alois Mahdal
3
@AloisMahdal: En fait, ce serait une bonne question distincte :-). Version courte: gzipne connaît pas la limite (qui provient d'un processus différent). Si headest utilisé, se headterminera lorsqu'il en aura reçu suffisamment, et cela se propagera vers gzip(via SIGPIPE, voir Wikipedia). Car tailce n'est pas possible, alors oui, gziptout décompressera.
sleske
Mais si vous êtes intéressé, vous devriez vraiment poser cette question séparément.
sleske
20

pipe à diviser utilisez gunzip -c ou zcat pour ouvrir le fichier

gunzip -c bigfile.gz | split -l 400000

Ajoutez des spécifications de sortie à la commande de division.

Jim Mcnamara
la source
3
C'est massivement plus efficace que la réponse acceptée, sauf si vous n'avez besoin que d'une fraction des morceaux fractionnés. Veuillez voter.
b0fh
1
@ b0fh: Oui, vous avez raison. Surévalué et référencé dans ma réponse :-).
sleske
La meilleure réponse, c'est sûr.
Stephen Blum
quelles sont les spécifications de sortie pour que les sorties soient elles-mêmes des fichiers .gz?
Quetzalcoatl
7

Comme vous travaillez sur un flux (non rembobinable), vous souhaiterez utiliser la forme de queue '+ N' pour obtenir des lignes à partir de la ligne N.

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
la source
4

J'envisagerais d'utiliser split .

diviser un fichier en morceaux

Michael Krelin - pirate
la source
3

Divisez directement le fichier .gz en fichiers .gz:

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

Je pense que c'est ce que voulait OP, car il n'a pas beaucoup d'espace.

siulkilulki
la source
2

Voici un script python pour ouvrir un ensemble de fichiers globalisés à partir d'un répertoire, les compresser si nécessaire et les parcourir ligne par ligne. Il utilise uniquement l'espace nécessaire en mémoire pour contenir les noms de fichiers et la ligne actuelle, plus un peu de surcharge.

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

La commande print line enverra chaque ligne vers std out, vous pouvez donc rediriger vers un fichier. Alternativement, si vous nous faites savoir ce que vous voulez faire avec les lignes, je peux l'ajouter au script python et vous n'aurez pas besoin de laisser des morceaux du fichier autour.

Spencer Rathbun
la source
2

Voici un programme perl qui peut être utilisé pour lire stdin et diviser les lignes, en redirigeant chaque groupe vers une commande distincte qui peut utiliser une variable shell $ SPLIT pour l'acheminer vers une destination différente. Pour votre cas, il serait invoqué avec

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

Désolé, le traitement en ligne de commande est un peu compliqué, mais vous avez l'idée.

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