Jak skutecznie podzielić duży plik tekstowy bez dzielenia rekordów wielowierszowych?

9

Mam duży plik tekstowy (~ 50 Gb, gdy gz'ed). Plik zawiera 4*Nwiersze lub Nrekordy; to znaczy każdy rekord składa się z 4 linii. Chciałbym podzielić ten plik na 4 mniejsze pliki o rozmiarze około 25% pliku wejściowego. Jak mogę podzielić plik na granicy rekordów?

Naiwnym podejściem byłoby zcat file | wc -luzyskanie liczby wierszy, podzielenie tej liczby przez 4, a następnie użycie split -l <number> file. Jest to jednak powtarzane dwukrotnie, a licznik wierszy jest wyjątkowo wolny (36 minut). Czy jest lepszy sposób?

To się zbliża, ale nie tego szukam. Zaakceptowana odpowiedź również liczy liczbę wierszy.

EDYTOWAĆ:

Plik zawiera dane sekwencjonowania w formacie fastq. Dwa rekordy wyglądają tak (anonimowe):

@NxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxGCGA+ATAGAGAG
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxTTTATGTTTTTAATTAATTCTGTTTCCTCAGATTGATGATGAAGTTxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
+
AAAAA#FFFFFFFFFFFFAFFFFF#FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF<AFFFFFFFFFFAFFFFFFFFFFFFFFFFFFF<FFFFFFFFFAFFFAFFAFFAFFFFFFFFAFFFFFFAAFFF<FAFAFFFFA
@NxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxGCGA+ATAGAGAG
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxCCCTCTGCTGGAACTGACACGCAGACATTCAGCGGCTCCGCCGCCxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
+
AAAAA#FFFFF7FFFFFFAFFFFA#F7FFFFFFFFF7FFFFFAF<FFFFFFFFFFFFFFAFFF.F.FFFFF.FAFFF.FFFFFFFFFFFFFF.)F.FFA))FFF7)F7F<.FFFF.FFF7FF<.FFA<7FA.<.7FF.FFFAFF

Pierwszy wiersz każdego rekordu zaczyna się od @.

EDYCJA 2:

zcat file > /dev/null zajmuje 31 minut.

EDYCJA 3: Tylko pierwsza linia zaczyna się od @. Żaden z pozostałych nigdy. Zobacz tutaj . Zapisy muszą pozostać w porządku. Nie można nic dodawać do wynikowego pliku.

Rolf
źródło
Jak długo trwa singiel zcat file > /dev/null?
choroba
Czy możesz podać małą próbkę danego pliku?
FloHimself
Mówisz, że każdy rekord zaczyna się od, @a także, że na rekord przypadają 4 linie. Czy oba są absolutne? - i czy linie 2,3,4 mogą zaczynać się od @? i czy w pliku jest jakiś niezapisany nagłówek linii stopki?
Peter.O,
1
Szukasz rozwiązania, które obsługuje skompresowane dane wejściowe i / lub generuje skompresowane dane wyjściowe? Czy szukasz czterech skompresowanych plików o jednakowej wielkości?
Stephen Kitt

Odpowiedzi:

4

Nie sądzę, żebyś mógł to zrobić - niezawodnie i nie tak, jak prosisz. Chodzi o to, że współczynnik kompresji archiwum prawdopodobnie nie będzie równomiernie rozłożony od głowy do ogona - algorytm kompresji będzie miał zastosowanie w niektórych częściach lepiej niż w innych. Tak to działa. I dlatego nie możesz brać pod uwagę podziału na rozmiar skompresowanego pliku.

Co więcej, gzippo prostu nie obsługuje przechowywania oryginalnego rozmiaru skompresowanych plików większych niż 4 GB - nie może tego obsłużyć. Nie możesz więc zapytać archiwum, aby uzyskać niezawodny rozmiar - bo to Cię oszuka.

Czteroliniowa rzecz - to całkiem proste. 4-plikowa rzecz - po prostu nie wiem, jak można to zrobić niezawodnie i z równomierną dystrybucją bez uprzedniego rozpakowania archiwum, aby uzyskać jego nieskompresowany rozmiar. Nie sądzę, żebyś mógł, bo próbowałem.

Jednak to, co można zrobić, to ustawić maksymalny rozmiar dla plików wyjściowych split, i upewnij się, że te zawsze są łamane na bariery płytowych. Możesz to łatwo zrobić. Oto mały skrypt, który zrobi to poprzez rozpakowanie gziparchiwum i przesłanie zawartości przez kilka jawnych ddbuforów potoku z konkretnymi count=$rptargumentami, przed przekazaniem tego lz4do dekompresji / ponownej kompresji każdego pliku w locie. Wrzuciłem także kilka małych teesztuczek na fajce, aby wydrukować ostatnie cztery wiersze dla każdego segmentu na stderr.

(       IFS= n= c=$(((m=(k=1024)*k)/354))
        b=bs=354xk bs=bs=64k
        pigz -d </tmp/gz | dd i$bs o$b |
        while   read -r line _$((n+=1))
        do      printf \\n/tmp/lz4.$n\\n
        { {     printf %s\\n "$line"
                dd count=$c i$b o$bs
        }|      tee /dev/fd/3|lz4 -BD -9 >/tmp/lz4.$n
        } 3>&1| tail -n4 |tee /dev/fd/2 |
                wc -c;ls -lh /tmp/[gl]z*
        done
)

To będzie trwać, dopóki nie obsłuży wszystkich danych wejściowych. Nie próbuje podzielić go na pewien procent - którego nie może uzyskać - ale dzieli go na maksymalną liczbę nieprzetworzonych bajtów na podział. Poza tym duża część twojego problemu polega na tym, że nie możesz uzyskać wiarygodnego rozmiaru w swoim archiwum, ponieważ jest ono zbyt duże - cokolwiek robisz, nie rób tego ponownie - spraw, aby podziały były mniejsze niż 4 gb , może. Przynajmniej ten mały skrypt pozwala to zrobić bez konieczności zapisywania nieskompresowanego bajtu na dysku.

Oto krótsza wersja pozbawiona zasadniczych elementów - nie dodaje się do wszystkich elementów raportu:

(       IFS= n= c=$((1024*1024/354))
        pigz -d | dd ibs=64k obs=354xk |
        while   read -r line _$((n+=1))
        do {    printf %s\\n "$line"
                dd count=$c obs=64k ibs=354xk
        }  |    lz4 -BD -9  >/tmp/lz4.$n
        done
)  </tmp/gz

Robi te same rzeczy, co pierwsze, w większości nie ma nic więcej do powiedzenia na ten temat. Ponadto jest mniej bałaganu, więc może łatwiej jest zobaczyć, co się dzieje.

Chodzi IFS=o to, aby obsłużyć jedną readlinię na iterację. My, readponieważ potrzebujemy, aby nasza pętla zakończyła się, gdy zakończy się wejście. Zależy to od wielkości twojego rekordu - który w twoim przykładzie wynosi 354 bajtów na. Stworzyłem gziparchiwum 4+ GB z losowymi danymi w celu przetestowania.

Losowe dane otrzymano w ten sposób:

(       mkfifo /tmp/q; q="$(echo '[1+dPd126!<c]sc33lcx'|dc)"
        (tr '\0-\33\177-\377' "$q$q"|fold -b144 >/tmp/q)&
        tr '\0-\377' '[A*60][C*60][G*60][N*16][T*]' | fold -b144 |
        sed 'h;s/^\(.\{50\}\)\(.\{8\}\)/@N\1+\2\n/;P;s/.*/+/;H;x'|
        paste "-d\n" - - - /tmp/q| dd bs=4k count=kx2k  | gzip
)       </dev/urandom >/tmp/gz 2>/dev/null

... ale może nie musisz się tym tak bardzo przejmować, ponieważ masz już wszystkie dane. Powrót do rozwiązania ...

Zasadniczo pigz- który wydaje się dekompresować nieco szybciej niż robi to zcat- odpompowuje nieskompresowany strumień i ddbuforuje dane wyjściowe do bloków zapisu o rozmiarze dokładnie wielokrotności 354 bajtów. Pętla będzie po każdej iteracji do testu, który wejście jest wciąż przybywających, co będzie potem co przed kolejnym nazywa się czytać bloki formowany specjalnie na wielokrotnością 354 bajtów - do synchronizacji z buforowania procesu - na czas. Będzie jeden krótki odczyt na każdą iterację z powodu początkowej - ale to nie ma znaczenia, ponieważ drukujemy to w naszym procesie kolekcjonowania.read$lineprintfprintflz4ddddread $linelz4

Skonfigurowałem go tak, aby każda iteracja odczytywała około 1 GB nieskompresowanych danych i kompresowała ten strumień do około 650 Mb lub mniej więcej. lz4jest znacznie szybszy niż jakakolwiek inna przydatna metoda kompresji - dlatego wybrałem ją tutaj, ponieważ nie lubię czekać. xzprawdopodobnie jednak wykonałby znacznie lepszą robotę przy kompresji. Jedną z rzeczy lz4jest to, że często może dekompresować przy prędkościach zbliżonych do pamięci RAM - co oznacza, że ​​wiele razy można dekompresować lz4archiwum tak szybko, jak i tak można by je zapisać do pamięci.

Ten duży wykonuje kilka raportów dla każdej iteracji. Obie pętle wydrukują ddraport o liczbie przesłanych nieprzetworzonych bajtów oraz prędkości i tak dalej. Wielka pętla wypisze również ostatnie 4 wiersze danych wejściowych na cykl i liczbę bajtów dla tego samego, a następnie jeden lsz katalogu, do którego piszę lz4archiwa. Oto kilka rund wyników:

/tmp/lz4.1
2961+1 records in
16383+1 records out
1073713090 bytes (1.1 GB) copied, 169.838 s, 6.3 MB/s
@NTACGTANTTCATTGGNATGACGCGCGTTTATGNGAGGGCGTCCGGAANGC+TCTCTNCC
TACGTANTTCATTGGNATGACGCGCGTTTATGNGAGGGCGTCCGGAANGCTCTCTNCCGAGCTCAGTATGTTNNAAGTCCTGANGNGTNGCGCCTACCCGACCACAACCTCTACTCGGTTCCGCATGCATGCAACACATCGTCA
+
I`AgZgW*,`Gw=KKOU:W5dE1m=-"9W@[AG8;<P7P6,qxE!7P4##,Q@c7<nLmK_u+IL4Kz.Rl*+w^A5xHK?m_JBBhqaLK_,o;p,;QeEjb|">Spg`MO6M'wod?z9m.yLgj4kvR~+0:.X#(Bf
354

-rw-r--r-- 1 mikeserv mikeserv 4.7G Jun 16 08:58 /tmp/gz
-rw-r--r-- 1 mikeserv mikeserv 652M Jun 16 12:32 /tmp/lz4.1

/tmp/lz4.2
2961+1 records in
16383+1 records out
1073713090 bytes (1.1 GB) copied, 169.38 s, 6.3 MB/s
@NTTGTTGCCCTAACCANTCCTTGGGAACGCAATGGTGTGANCTGCCGGGAC+CTTTTGCT
TTGTTGCCCTAACCANTCCTTGGGAACGCAATGGTGTGANCTGCCGGGACCTTTTGCTGCCCTGGTACTTTTGTCTGACTGGGGGTGCCACTTGCAGNAGTAAAAGCNAGCTGGTTCAACNAATAAGGACNANTTNCACTGAAC
+
>G-{N~Q5Z5QwV??I^~?rT+S0$7Pw2y9MV^BBTBK%HK87(fz)HU/0^%JGk<<1--7+r3e%X6{c#w@aA6Q^DrdVI0^8+m92vc>RKgnUnMDcU:j!x6u^g<Go?p(HKG@$4"T8BWZ<z.Xi
354

-rw-r--r-- 1 mikeserv mikeserv 4.7G Jun 16 08:58 /tmp/gz
-rw-r--r-- 1 mikeserv mikeserv 652M Jun 16 12:32 /tmp/lz4.1
-rw-r--r-- 1 mikeserv mikeserv 652M Jun 16 12:35 /tmp/lz4.2
mikeserv
źródło
gzip -ldziała tylko dla nieskompresowanych plików <2GiB IIRC (zresztą coś mniejszego niż plik OP).
Stéphane Chazelas,
@ StéphaneChazelas - cholera. To jedyny sposób, w jaki mogłem wymyślić uzyskanie nieskompresowanego rozmiaru. Bez tego to w ogóle nie działa.
mikeserv
4

Dzielenie plików na granicach rekordów jest w rzeczywistości bardzo łatwe, bez żadnego kodu:

zcat your_file.gz | split -l 10000 - output_name_

Spowoduje to utworzenie plików wyjściowych o długości 10000 linii, z nazwami nazwa_wyjściowa_aa, nazwa_wyjściowa_ab, nazwa_wyjściowa_ac, ... Przy wejściach tak dużych jak twoje, da ci to wiele plików wyjściowych. Zamień na 10000dowolną wielokrotność czterech, a pliki wyjściowe mogą być tak duże lub małe, jak chcesz. Niestety, podobnie jak w przypadku innych odpowiedzi, nie ma dobrego sposobu na zagwarantowanie, że uzyskasz pożądaną liczbę (w przybliżeniu) równych rozmiarów plików wyjściowych bez zgadywania na temat danych wejściowych. (Lub właściwie przepuszczając całość wc.) Jeśli twoje rekordy są w przybliżeniu jednakowej wielkości (lub przynajmniej mniej więcej równomiernie rozłożone), możesz spróbować oszacować tak:

zcat your_file.gz | head -n4000 | gzip | wc -c

To powie ci skompresowany rozmiar pierwszych 1000 rekordów twojego pliku. Na tej podstawie możesz prawdopodobnie oszacować, ile wierszy w każdym pliku ma kończyć się czterema plikami. (Jeśli nie chcesz pozostawić zdegenerowanego piątego pliku, pamiętaj, aby nieco zwiększyć swoje oszacowanie, lub przygotuj się na przyczepienie piątego pliku do końca czwartego.)

Edycja: Oto jeszcze jedna sztuczka, zakładając, że chcesz skompresowane pliki wyjściowe:

#!/bin/sh

base=$(basename $1 .gz)
unpigz -c $1 | split -l 100000 --filter='pigz -c > _$FILE.gz' - ${base}_

batch=$((`ls _*.gz | wc -l` / 4 + 1))
for i in `seq 1 4`; do
  files=`ls _*.gz | head -$batch`
  cat $files > ${base}_$i.gz && rm $files
done

Spowoduje to utworzenie wielu mniejszych plików, a następnie szybkie ich połączenie. (Może być konieczne dostosowanie parametru -l w zależności od długości linii w plikach.) Zakłada się, że masz stosunkowo najnowszą wersję jądra GNU (dla split --filter) i około 130% rozmiaru pliku wejściowego w wolne miejsce na dysku. Zamień gzip / zcat na pigz / unpigz, jeśli ich nie masz. Słyszałem, że niektóre biblioteki oprogramowania (Java?) Nie obsługują plików gzip połączonych w ten sposób, ale jak dotąd nie miałem z tym żadnych problemów. (pigz używa tej samej sztuczki do równoległego kompresji).

Rysował
źródło
Jeśli masz zainstalowany program Pigz, możesz nieco przyspieszyć, zastępując „Zcat” parametrem „pigz -cd”.
Drew
2
Ach, właśnie zauważyłem, że już wspomniałeś o podzieleniu w pytaniu. Ale tak naprawdę, prawie każde rozwiązanie będzie działało tak samo jak podział pod maską. Trudność polega na ustaleniu, ile wierszy należy umieścić w każdym pliku.
Drew
3

Z tego, co zbieram po sprawdzeniu sfery Google i dalszym testowaniu .gzpliku 7,8 GiB , wydaje się, że metadane rozmiaru oryginalnego nieskompresowanego pliku nie są dokładne (tj. Nieprawidłowe ) dla dużych .gzplików (większych niż 4GiB (może 2GiB dla niektórych wersje gzip).
Re. mój test metadanych gzip:

* The compressed.gz file is  7.8 GiB ( 8353115038 bytes) 
* The uncompressed  file is 18.1 GiB (19436487168 bytes)
* The metadata says file is  2.1 GiB ( 2256623616 bytes) uncompressed

Wygląda więc na to, że nie można określić rozmiaru nieskompresowanego bez jego faktycznego rozpakowania (co jest nieco szorstkie, delikatnie mówiąc!)

Tak czy inaczej, tutaj jest sposób na podzielenie nieskompresowanego pliku na granicy rekordu, gdzie każdy rekord zawiera 4 linie .

Wykorzystuje rozmiar pliku w bajtach (przez stat) i awkzliczając bajty (nie znaków). Określa, czy zakończeniem linii jest LF| CR| CRLF, ten skrypt obsługuje długość końca linii za pomocą wbudowanej zmiennej RT).

LC_ALL=C gawk 'BEGIN{"stat -c %s "ARGV[1] | getline inSize
                      segSiz=int(inSize/4)+((inSize%4)==0?0:1)
                      ouSplit=segSiz; segNb=0 }
               { lnb++; bytCt+=(length+length(RT))
                 print $0 > ARGV[1]"."segNb
                 if( lnb!=4 ) next
                 lnb=0
                 if( bytCt>=ouSplit ){ segNb++; ouSplit+=segSiz }
               }' myfile

Poniżej znajduje się test, którego użyłem do sprawdzenia, czy liczba wierszy każdego pliku to mod 4 == 0

for i in myfile  myfile.{0..3}; do
    lc=$(<"$i" wc -l)
    printf '%s\t%s\t' "$i" $lc; 
    (( $(echo $lc"%4" | bc) )) && echo "Error: mod 4 remainder !" || echo 'mod 4 ok'  
done | column -ts$'\t' ;echo

Wyjście testowe:

myfile    1827904  mod 4 ok
myfile.0  456976   mod 4 ok
myfile.1  456976   mod 4 ok
myfile.2  456976   mod 4 ok
myfile.3  456976   mod 4 ok

myfile został wygenerowany przez:

printf %s\\n {A..Z}{A..Z}{A..Z}{A..Z}—{1..4} > myfile
Peter.O
źródło
2

To nie jest poważna odpowiedź! Właśnie się bawiłem flexi to najprawdopodobniej nie zadziała na pliku wejściowym o ~ 50 Gb (jeśli w ogóle, na większych danych wejściowych niż mój plik testowy):

Działa to dla mnie na pliku ~ 1 Gb input.txt :

Biorąc pod uwagę flexplik wejściowy splitter.l :

%{
#include <stdio.h>
extern FILE* yyin;
extern FILE* yyout;

int input_size = 0;

int part_num;
int part_num_max;
char **part_names;
%}

%%
@.+ {
        if (ftell(yyout) >= input_size / part_num_max) {
            fclose(yyout);
            if ((yyout = fopen(part_names[++part_num], "w")) == 0) {
                exit(1);
            }
        }
        fprintf(yyout, "%s", yytext);
    }
%%

int main(int argc, char *argv[]) {

    if (argc < 2) {
        return 1;
    } else if ((yyin = fopen(argv[1], "r")) == 0) {
        return 1;
    } else if ((yyout = fopen(argv[2], "w")) == 0) {
        fclose(yyin);
        return 1;
    } else {

        fseek(yyin, 0L, SEEK_END);
        input_size = ftell(yyin);
        rewind(yyin);

        part_num = 0;
        part_num_max = argc - 2;
        part_names = argv + 2;

        yylex();

        fclose(yyin);
        fclose(yyout);
        return 0;
    }
}

generowanie lex.yy.c i kompilowanie go do plikusplitter binarnego za pomocą:

$ flex splitter.l && gcc lex.yy.c -ll -o splitter

Stosowanie:

$ ./splitter input.txt output.part1 output.part2 output.part3 output.part4

Czas działania dla wejścia 1 Gb. Txt :

$ time ./splitter input.txt output.part1 output.part2 output.part3 output.part4

real    2m43.640s
user    0m48.100s
sys     0m1.084s
FloHimself
źródło
Rzeczywiste leksykowanie tutaj jest tak proste, że tak naprawdę nie czerpiesz z niego korzyści. Wystarczy zadzwonić getc(stream)i zastosować prostą logikę. Czy wiesz również, że. (kropka) regex w (f) lex pasuje do dowolnego znaku oprócz nowej linii , prawda? Podczas gdy rekordy te są wieloliniowe.
Kaz
@Kaz Chociaż twoje oświadczenia są na ogół zgodne z prawdą, to tak naprawdę działa z danymi podanymi w Q.
FloHimself
Tylko przypadkowo, ponieważ istnieje domyślna reguła, gdy nic nie pasuje: zużyj znak i wydrukuj go na wyjściu! Innymi słowy, możesz przełączać swoje pliki za pomocą reguły, która rozpoznaje @znak, a następnie pozwala domyślnej regule skopiować dane. Teraz masz regułę kopiującą część danych jako jeden duży token, a następnie domyślna reguła otrzymuje drugi wiersz po jednym znaku na raz.
Kaz
Dzięki za wytłumaczenie. Zastanawiam się, jak rozwiązałbyś to zadanie txr.
FloHimself
Nie jestem pewien, czy tak zrobię, ponieważ zadaniem jest zrobienie bardzo prostej rzeczy z dużą ilością danych, tak szybko, jak to możliwe.
Kaz
1

Oto rozwiązanie w Pythonie, które polega na przejściu pliku wejściowego i zapisaniu plików wyjściowych.

Cechą związaną z używaniem wc -ljest to, że zakładasz, że każdy z rekordów ma ten sam rozmiar. To może być prawda tutaj, ale poniższe rozwiązanie działa, nawet jeśli tak nie jest. Zasadniczo używa wc -club liczbę bajtów w pliku. W Pythonie odbywa się to za pomocą os.stat ()

Oto jak działa program. Najpierw obliczamy idealne punkty podziału jako przesunięcia bajtów. Następnie odczytujesz wiersze pliku wejściowego zapisywane do odpowiedniego pliku wyjściowego. Gdy zobaczysz, że przekroczyłeś optymalny następny punkt podziału i jesteś na granicy rekordu, zamknij ostatni plik wyjściowy i otwórz następny.

Program jest optymalny pod tym względem, odczytuje raz bajty pliku wejściowego; Uzyskanie rozmiaru pliku nie wymaga odczytu danych pliku. Wymagane miejsce do przechowywania jest proporcjonalne do wielkości linii. Ale Python lub system prawdopodobnie mają rozsądne bufory plików, aby przyspieszyć operacje wejścia / wyjścia.

Dodałem parametry określające liczbę plików do podzielenia i wielkość rekordu na wypadek, gdybyś chciał to zmienić w przyszłości.

I oczywiście można to przetłumaczyć również na inne języki programowania.

Jeszcze jedno, nie jestem pewien, czy Windows z crlf odpowiednio obsługuje długość linii, tak jak to ma miejsce w systemach Unix-y. Jeśli len () jest wyłączony o jeden tutaj, mam nadzieję, że to oczywiste, jak dostosować program.

#!/usr/bin/env python
import os

# Adjust these
filename = 'file.txt'
rec_size = 4
file_splits = 4

size = os.stat(filename).st_size
splits = [(i+1)*size/file_splits for i in range(file_splits)]
with open(filename, 'r') as fd:
    linecount = 0
    i = 0 # File split number
    out = open('file%d.txt' % i, 'w')
    offset = 0  # byte offset of where we are in the file: 0..size
    r = 0 # where we are in the record: 0..rec_size-1
    for line in fd:
        linecount += 1
        r = (r+1) % rec_size
        if offset + len(line) > splits[i] and r == 1 :
            out.close()
            i += 1
            out = open('file%d.txt' % i, 'w')
        out.write(line)
        offset += len(line)
    out.close()
    print("file %s has %d lines" % (filename, linecount))
skalisty
źródło
Nie dzieli się na granicy rekordów. na przykład. Pierwszy podział pliku podrzędnego następuje po 3 linii z tym wejściemprintf %s\\n {A..Z}{A..Z}{A..Z}{A..Z}—{1..4}
Peter.O
1

Użytkownik FloHimself wydawał się ciekawy rozwiązania TXR . Oto jeden z wykorzystaniem wbudowanego TXR Lisp :

(defvar splits 4)
(defvar name "data")

(let* ((fi (open-file name "r"))                 ;; input stream
       (rc (tuples 4 (get-lines fi)))            ;; lazy list of 4-tuples
       (sz (/ (prop (stat name) :size) splits))  ;; split size
       (i 1)                                     ;; split enumerator
       (n 0)                                     ;; tuplecounter within split
       (no `@name.@i`)                           ;; output split file name
       (fo (open-file no "w")))                  ;; output stream
  (whilet ((r (pop rc)))  ;; pop each 4-tuple
    (put-lines r fo) ;; send 4-tuple into output file
    ;; if not on the last split, every 1000 tuples, check the output file
    ;; size with stat and switch to next split if necessary.
    (when (and (< i splits)
               (> (inc n) 1000)
               (>= (seek-stream fo 0 :from-current) sz))
      (close-stream fo)
      (set fo (open-file (set no `@name.@(inc i)`) "w")
           n 0)))
  (close-stream fo))

Uwagi:

  1. Z tego samego powodu pop- ważne jest użycie każdej krotki z leniwej listy krotek, aby leniwa lista została wykorzystana. Nie możemy zachować odniesienia do początku tej listy, ponieważ wtedy pamięć będzie rosła podczas marszu przez plik.

  2. (seek-stream fo 0 :from-current)jest przypadkiem braku operacji seek-stream, co czyni się przydatnym, zwracając bieżącą pozycję.

  3. Wydajność: nie wspominaj o tym. Użyteczne, ale nie przyniosą żadnych trofeów do domu.

  4. Ponieważ sprawdzamy rozmiar co 1000 krotek, możemy po prostu utworzyć krotkę o wielkości 4000 linii.

Kaz
źródło
0

Jeśli nie potrzebujesz, aby nowe pliki były ciągłymi częściami oryginalnego pliku, możesz to zrobić sedw następujący sposób:

sed -n -e '1~16,+3w1.txt' -e '5~16,+3w2.txt' -e '9~16,+3w3.txt' -e '13~16,+3w4.txt'

-nZatrzymuje go przed wydrukowaniem każdą linię, a każdy z tych -escenariuszy jest w zasadzie robi to samo. 1~16dopasowuje pierwszą linię, a następnie co 16 linię. ,+3oznacza dopasowanie kolejnych trzech wierszy po każdym z nich. w1.txtmówi napisz wszystkie te wiersze do pliku 1.txt. Zajmuje to co 4 grupę 4 linii i zapisuje ją do pliku, zaczynając od pierwszej grupy 4 linii. Pozostałe trzy polecenia robią to samo, ale każda z nich jest przesuwana do przodu o 4 linie i zapisuje do innego pliku.

Spowoduje to okropne uszkodzenie, jeśli plik nie będzie dokładnie zgodny z ustaloną specyfikacją, ale w przeciwnym razie powinien działać zgodnie z zamierzeniami. Nie wyprofilowałem go, więc nie wiem, jak skuteczny będzie, ale sedjest dość wydajny w edycji strumieniowej.

Erik
źródło