unix - podziel ogromny plik .gz według linii

16

Jestem pewien, że ktoś miał poniższą potrzebę, jaki jest szybki sposób dzielenia ogromnego pliku .gz po linii? Podstawowy plik tekstowy ma 120 milionów wierszy. Nie mam wystarczającej ilości miejsca na dysku, aby rozpalić cały plik naraz, więc zastanawiałem się, czy ktoś wie o skrypcie bash / perl lub narzędziu, które mogłoby podzielić plik (.gz lub wewnętrzny .txt) na pliki linii 3x 40mn . tzn. nazywając to tak:

    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

Być może robi serię tych rozwiązań, czy też gunzip -c wymagałby wystarczającej ilości miejsca, aby rozpakować cały plik (tj. Oryginalny problem): gunzip -c hugefile.txt.gz | głowica 4000000

Uwaga: nie mogę dostać dodatkowego dysku.

Dzięki!

toop
źródło
1
Czy chcesz, aby pliki wynikowe zostały ponownie skompresowane?
Możesz użyć gunzip w ipe. Resztę można zrobić głową i ogonem
Ingo
@Tichodroma - nie, nie potrzebuję ich ponownie. Ale nie mogłem zapisać wszystkich podzielonych plików tekstowych jednocześnie. Więc chciałbym uzyskać pierwszy podział, zrobić z nim rzeczy, a następnie usunąć pierwszy podział, a następnie uzyskać drugi podział. Etc w końcu usuwam oryginalny gz
toop
1
@toop: Dziękuję za wyjaśnienie. Zauważ, że ogólnie lepiej jest edytować pytanie, jeśli chcesz je wyjaśnić, niż wstawić je w komentarzu; w ten sposób wszyscy to zobaczą.
sleske
Przyjęta odpowiedź jest dobra, jeśli chcesz tylko ułamek kawałków i nie znasz ich z góry. Jeśli chcesz wygenerować wszystkie porcje naraz, rozwiązania oparte na podziale będą znacznie szybsze, O (N) zamiast O (N²).
b0fh

Odpowiedzi:

11

Jak to zrobić najlepiej, zależy od tego, czego chcesz:

  • Czy chcesz wyodrębnić jedną część dużego pliku?
  • A może chcesz stworzyć wszystkie części za jednym razem?

Jeśli chcesz pojedynczą część pliku , twój pomysł użyć gunzipi headma rację. Możesz użyć:

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

Spowodowałoby to wyświetlenie pierwszych 4000000 wierszy przy standardowym wyjściu - prawdopodobnie chcesz dołączyć kolejny potok, aby faktycznie zrobić coś z danymi.

Aby uzyskać inne części, użyj kombinacji headi tail, na przykład:

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

dostać drugi blok.

Być może robi serię tych rozwiązań, czy też gunzip -c wymaga wystarczającej ilości miejsca, aby rozpakować cały plik

Nie, gunzip -cnie wymaga miejsca na dysku - robi wszystko w pamięci, a następnie przesyła strumieniowo na standardowe wyjście.


Jeśli chcesz utworzyć wszystkie części za jednym razem , wydajniej jest utworzyć je wszystkie za pomocą jednego polecenia, ponieważ wtedy plik wejściowy jest odczytywany tylko raz. Jednym dobrym rozwiązaniem jest użycie split; szczegóły znajdziesz w odpowiedzi jima mcnamary.

Śleske
źródło
1
Z widoku wydajności: czy gzip rozpakowuje cały plik? Czy może „magicznie” wiedzieć, że potrzebne są tylko 4 miliony linii?
Alois Mahdal
3
@AloisMahdal: Właściwie to byłoby dobre osobne pytanie :-). Krótka wersja: gzipnie wie o limicie (który pochodzi z innego procesu). Jeśli headzostanie użyty, headzakończy działanie, gdy otrzyma wystarczającą ilość, i nastąpi jego propagacja gzip(za pośrednictwem SIGPIPE, patrz Wikipedia). Do tailtego nie jest możliwe, więc tak, gzipbędzie rozpakować wszystko.
śleske
Ale jeśli jesteś zainteresowany, powinieneś naprawdę zadać to jako osobne pytanie.
śleske
20

do dzielenia potoku użyj polecenia gunzip -c lub zcat, aby otworzyć plik

gunzip -c bigfile.gz | split -l 400000

Dodaj specyfikacje wyjściowe do polecenia split.

Jim Mcnamara
źródło
3
Jest to znacznie bardziej wydajne niż zaakceptowana odpowiedź, chyba że potrzebujesz tylko ułamka podzielonych fragmentów. Proszę głosować.
b0fh
1
@ b0fh: Tak, masz rację. Głosowałem i do którego odwołuje się moja odpowiedź :-).
sleske
Najlepsza odpowiedź na pewno.
Stephen Blum,
jakie są specyfikacje wyjściowe, aby dane wyjściowe były samymi plikami .gz?
Quetzalcoatl
7

Podczas pracy nad (nieodwracalnym) strumieniem, będziesz chciał użyć formy „+ N” ogona, aby uzyskać linie zaczynające się od linii 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
źródło
4

Rozważałbym użycie podziału .

podziel plik na części

Michael Krelin - haker
źródło
3

Bezpośrednio podziel plik .gz na pliki .gz:

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

Myślę, że tego właśnie chciał OP, ponieważ nie ma dużo miejsca.

siulkilulki
źródło
2

Oto skrypt Pythona do otwierania globalnego zestawu plików z katalogu, pistoletowania ich w razie potrzeby i odczytywania ich wiersz po wierszu. Wykorzystuje tylko przestrzeń potrzebną w pamięci do przechowywania nazw plików i bieżącej linii, a także niewielki narzut.

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

Polecenie print line wyśle ​​każdą linię do standardowego wyjścia, abyś mógł przekierować do pliku. Ewentualnie, jeśli dasz nam znać, co chcesz zrobić z liniami, mogę dodać to do skryptu Pythona i nie będziesz musiał zostawiać fragmentów pliku leżących wokół.

Spencer Rathbun
źródło
2

Oto program perlowy, którego można użyć do odczytu standardowego wejścia i podziału linii, przesyłając każdą grupę do osobnego polecenia, które może użyć zmiennej powłoki $ SPLIT, aby skierować ją do innego miejsca docelowego. W twoim przypadku zostanie wywołane za pomocą

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

Niestety przetwarzanie w wierszu polecenia jest trochę nieprzyzwoite, ale masz pomysł.

#!/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
źródło