Unix - Teilen Sie eine riesige .gz-Datei zeilenweise

16

Ich bin sicher, dass jemand die folgenden Anforderungen hatte. Was ist ein schneller Weg, um eine riesige .gz-Datei zeilenweise aufzuteilen? Die zugrunde liegende Textdatei enthält 120 Millionen Zeilen. Ich habe nicht genügend Speicherplatz, um die gesamte Datei auf einmal zu komprimieren. Ich habe mich gefragt, ob jemand ein Bash / Perl-Skript oder -Tool kennt, mit dem die Datei (entweder die .gz-Datei oder die innere .txt-Datei) in 3x 40mn-Zeilendateien aufgeteilt werden kann . zB wie es heißt:

    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

Ist es vielleicht eine Lösung, eine Reihe dieser Probleme zu lösen, oder würde das gunzip -c genügend Speicherplatz benötigen, um die gesamte Datei zu entpacken (dh das ursprüngliche Problem): gunzip -c hugefile.txt.gz | Kopf 4000000

Hinweis: Ich kann keine zusätzliche Festplatte beschaffen.

Vielen Dank!

toop
quelle
1
Möchten Sie, dass die resultierenden Dateien erneut gziped werden?
Sie können Gunzip in einer Ipe verwenden. Der Rest kann mit Kopf und Schwanz erledigt werden
Ingo
@ Tichodroma - nein, ich brauche sie nicht wieder gziped. Ich konnte aber nicht alle geteilten Textdateien auf einmal speichern. Also habe ich die erste Spaltung erhalten möchten, damit Dinge zu tun, löschen Sie die erste Spalte, und dann wird die zweite split.etc schließlich die ursprüngliche gz entfernen bekommen
toop
1
@toop: Danke für die Klarstellung. Beachten Sie, dass es im Allgemeinen besser ist, Ihre Frage zu bearbeiten, wenn Sie sie klären möchten, als sie in einen Kommentar einzufügen. so wird es jeder sehen.
sleske
Die akzeptierte Antwort ist gut, wenn Sie nur einen Bruchteil der Stücke wollen und sie nicht im Voraus kennen. Wenn Sie alle Blöcke auf einmal generieren möchten, sind die auf der Aufteilung basierenden Lösungen viel schneller: O (N) anstelle von O (N²).
Donnerstag,

Antworten:

11

Wie dies am besten funktioniert, hängt davon ab, was Sie möchten:

  • Möchten Sie einen einzelnen Teil der großen Datei extrahieren?
  • Oder möchten Sie alle Teile auf einmal erstellen?

Wenn Sie einen einzelnen Teil der Datei möchten , ist Ihre Idee zu verwenden gunzipund headrichtig. Sie können verwenden:

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

Das würde die ersten 4000000 Zeilen bei Standardausgabe ausgeben - Sie möchten wahrscheinlich eine weitere Pipe anhängen, um tatsächlich etwas mit den Daten zu tun.

Um die anderen Teile zu erhalten, würden Sie eine Kombination aus headund verwenden tail, wie:

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

um den zweiten Block zu bekommen.

Ist es vielleicht eine Lösung, eine Reihe davon zu tun, oder würde das gunzip -c genügend Speicherplatz benötigen, damit die gesamte Datei entpackt werden kann

Nein, der gunzip -cbenötigt keinen Speicherplatz - er erledigt alles im Speicher und überträgt es dann auf stdout.


Wenn Sie alle Teile auf einmal erstellen möchten , ist es effizienter , alle Teile mit einem einzigen Befehl zu erstellen, da die Eingabedatei dann nur einmal gelesen wird. Eine gute Lösung ist zu verwenden split; Weitere Informationen finden Sie in der Antwort von Jim Mcnamara.

sleske
quelle
1
Aus Sicht der Leistung: Entzippt gzip tatsächlich die gesamte Datei? Oder kann es "magisch" wissen, dass nur 4mn Leitungen benötigt werden?
Alois Mahdal
3
@AloisMahdal: Eigentlich wäre das eine gute eigene Frage :-). Kurzversion: gzipkennt das Limit nicht (was aus einem anderen Prozess stammt). Wenn headverwendet, headwird beendet, wenn es genug empfangen hat, und dies wird sich ausbreiten gzip(über SIGPIPE, siehe Wikipedia). Da taildies nicht möglich ist, wird ja gzipalles dekomprimiert.
sleske
Aber wenn Sie interessiert sind, sollten Sie dies wirklich als separate Frage stellen.
sleske
20

Pipe zum Teilen verwenden Sie entweder gunzip -c oder zcat, um die Datei zu öffnen

gunzip -c bigfile.gz | split -l 400000

Fügen Sie dem Befehl split Ausgabespezifikationen hinzu.

Jim Mcnamara
quelle
3
Dies ist erheblich effizienter als die akzeptierte Antwort, es sei denn, Sie benötigen nur einen Bruchteil der aufgeteilten Teile. Bitte stimme zu.
Donnerstag,
1
@ b0fh: Ja, du hast recht. Upvoted und in meiner Antwort verwiesen :-).
sleske
Beste Antwort sicher.
Stephen Blum
Wie lauten die Ausgabespezifikationen, sodass die Ausgaben selbst GZ-Dateien sind?
Quetzalcoatl
7

Wenn Sie an einem Stream (der nicht zurückgespult werden kann) arbeiten, möchten Sie die Endform '+ N' verwenden, um Zeilen ab Zeile N zu erhalten.

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
quelle
4

Ich würde Split in Betracht ziehen .

Teilen Sie eine Datei in Stücke

Michael Krelin - Hacker
quelle
3

GZ-Datei direkt in GZ-Dateien aufteilen:

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

Ich denke, das ist es, was OP wollte, weil er nicht viel Platz hat.

siulkilulki
quelle
2

Hier ist ein Python-Skript, mit dem Sie eine globale Gruppe von Dateien aus einem Verzeichnis öffnen, diese bei Bedarf komprimieren und zeilenweise durchlesen können. Es wird nur der Speicherplatz verwendet, der zum Speichern der Dateinamen und der aktuellen Zeile erforderlich ist, plus ein wenig Overhead.

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

Der Befehl print line sendet jede Zeile an std out, sodass Sie in eine Datei umleiten können. Wenn Sie uns aber auch mitteilen, was Sie mit den Zeilen tun möchten, kann ich es dem Python-Skript hinzufügen, und Sie müssen keine Teile der Datei herumliegen lassen.

Spencer Rathbun
quelle
2

Hier ist ein Perl-Programm, mit dem Sie stdin lesen und die Zeilen teilen können. Dabei wird jeder Clump an einen separaten Befehl weitergeleitet, der mithilfe der Shell-Variablen $ SPLIT an ein anderes Ziel weitergeleitet werden kann. Für Ihren Fall würde es mit aufgerufen werden

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

Tut mir leid, die Befehlszeilenverarbeitung ist ein wenig kompliziert, aber Sie haben die Idee.

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