2009-09-11 9 views
37

Podano: Jeden duży plik danych tekstowych (np. Format CSV) ze specjalną pierwszą linią (np. Nazwy pól).Jak podzielić plik i zachować pierwszy wiersz w każdym kawałku?

Poszukiwane: odpowiednik coreutils komenda split -l, ale z dodatkowym wymogiem, że nagłówek od oryginalnego pliku pojawia się na początku każdego z otrzymanych kawałków.

Zgaduję, że niektóre mikstury z split i head zrobi lewy?

+8

Wydaje się logiczne, że ktoś powinien dodać, że jako wbudowaną cechą 'split' , prawda? –

+1

Prawdopodobnie największym czynnikiem * na * stanie się wbudowanym jest to, że generalnie zrekonstruujesz podzielony plik, wykonując 'cat a b c> rekonstruowany'. Zewnętrzne linie w pliku oznaczają, że normalna metoda rekonstrukcji nie odtwarza oryginalnego pliku. –

+2

To jest właśnie to, do czego służy nadchodzące narzędzie (* not *) "unsplit --remove-header"! Ale poważnie, "split", jeśli miałby opcję "powtórzyć nagłówek", powinien nadal domyślnie działać. Używałbyś tylko nagłówków, jeśli naprawdę tego chciałeś. –

Odpowiedz

32

to robhruska za skrypt oczyścić kawałek:

tail -n +2 file.txt | split -l 4 - split_ 
for file in split_* 
do 
    head -n 1 file.txt > tmp_file 
    cat $file >> tmp_file 
    mv -f tmp_file $file 
done 

usunąłem wc, cutls i echo, w miejscach, gdzie przebywa niepotrzebne. Zmieniłem niektóre nazwy plików, aby były trochę bardziej znaczące. Zerwałem go na wiele linii tylko po to, aby ułatwić czytanie.

Jeśli chcesz mieć ochotę, możesz użyć mktemp lub tempfile, aby utworzyć tymczasową nazwę pliku zamiast używać twardego.

Edit

Korzystanie GNU split to możliwe, aby to zrobić:

split_filter() { { head -n 1 file.txt; cat; } > "$FILE"; }; export -f split_filter; tail -n +2 file.txt | split --lines=4 --filter=split_filter - split_ 

wybuchł dla czytelności:

split_filter() { { head -n 1 file.txt; cat; } > "$FILE"; } 
export -f split_filter 
tail -n +2 file.txt | split --lines=4 --filter=split_filter - split_ 

Kiedy --filter jest określony, split uruchamia komendę (a funkcja w tym przypadku, która musi zostać wyeksportowana) dla każdego pliku wyjściowego i ustawia v ariable FILE, w środowisku polecenia, do pliku.

Skrypt lub funkcja filtrująca może wykonać dowolną manipulację, którą chce, na zawartość wynikową, a nawet na nazwę pliku. Przykładem tego ostatniego może być na przykład wyprowadzenie do stałej nazwy pliku w katalogu zmiennych: > "$FILE/data.dat".

+0

To na pewno zadziała. Miałem tylko nadzieję, że uda mi się stworzyć zręczny jednolinijkowy program typu 'for $ part in (split -l 1000 myfile); cat <(head -n1 myfile) $ part> myfile. $ part; done' – Arkady

+0

To nie działa, ponieważ 'split', z konieczności, nie wypisuje na' stdout'. –

+0

'split' * mogłoby * wypisać * nazwy * plików na standardowe wyjście, (o ile dyskutujemy o tym, co' split' * powinno zrobić * :-) – Arkady

4

Jestem nowicjuszem, jeśli chodzi o Bash-fu, ale udało mi się wymyślić to dwupołaciowe monstrum. Jestem pewien, że istnieją bardziej eleganckie rozwiązania.

$> tail -n +2 file.txt | split -l 4 
$> for file in `ls xa*`; do echo "`head -1 file.txt`" > tmp; cat $file >> tmp; mv -f tmp $file; done 

to zakładając, że plik wejściowy jest file.txt, nie używasz prefix argument split, a pracujesz w katalogu, który nie ma żadnych innych plików, które zaczynają się domyślnie split „s xa* format wyjściowy. Zamień również "4" na żądany rozmiar linii podziału.

1

Nigdy nie jestem pewien co do zasad kopiowania skryptów bezpośrednio z witryn innych osób, ale Geekology ma ładny skrypt do robienia tego, co chcesz, z kilkoma komentarzami potwierdzającymi, że działa. Pamiętaj, aby wykonać tail-n+2, jak podano w komentarzu u dołu.

2

To jest bardziej rozbudowana wersja skryptu Denis Williamson. Skrypt tworzy wiele plików tymczasowych i byłoby wstydem, gdyby zostały pozostawione w pobliżu, gdyby bieg był niekompletny. Dodajmy więc pułapkę sygnału (zobacz http://tldp.org/LDP/Bash-Beginners-Guide/html/sect_12_02.html, a następnie http://tldp.org/LDP/abs/html/debugging.html) i usuńmy nasze pliki tymczasowe; to i tak najlepsza praktyka.

trap 'rm split_* tmp_file ; exit 13' SIGINT SIGTERM SIGQUIT 
tail -n +2 file.txt | split -l 4 - split_ 
for file in split_* 
do 
    head -n 1 file.txt > tmp_file 
    cat $file >> tmp_file 
    mv -f tmp_file $file 
done 

Zamień "13" na dowolny kod powrotu, który chcesz. Aha, i prawdopodobnie powinieneś używać mktemp tak czy inaczej (jak niektórzy już zasugerowali), więc śmiało usuń "tmp_file" z rm w linii pułapki Zobacz stronę man dla sygnału więcej sygnałów do złapania

8

You można użyć funkcji [mg] awk:

awk 'NR==1{ 
     header=$0; 
     count=1; 
     print header > "x_" count; 
     next 
    } 

    !((NR-1) % 100){ 
     count++; 
     print header > "x_" count; 
    } 
    { 
     print $0 > "x_" count 
    }' file 

100 to liczba linii każdego wycinka nie wymaga pliki tymczasowe i można je umieścić w jednym wierszu

+0

Uaktualnianie do nauczania mnie czegoś nowego, ale jeśli mam zamiar napisać mały skrypt, równie dobrze mogę to zrobić w Perlu lub Pythonie :-) – Arkady

5

można użyć nowego.. - funkcjonalność filtra w GNU coreutils split> = 8.13 (2011):

tail -n +2 FILE.in | 
split -l 50 - --filter='sh -c "{ head -n1 FILE.in; cat; } > $FILE"' 
+1

Podoba mi się wersja z jednym linerem. Aby uczynić go bardziej ogólnym dla basha, zrobiłem: 'tail -n +2 FILE.in | split -d --lines 50 - --filter = 'bash -c "{head -n1 $ {FILE%. *}; cat;}> $ FILE"' FILE.in.x' – KullDox

1

lubiłem wersji awk Marco, przyjęty z tej uproszczonej jednej wkładki, gdzie można łatwo określić część podzielona w firn, jak chcesz:

awk 'NR==1{print $0 > FILENAME ".split1"; print $0 > FILENAME ".split2";} NR>1{if (NR % 10 > 5) print $0 >> FILENAME ".split1"; else print $0 >> FILENAME ".split2"}' file 
+0

Podoba mi się to rozwiązanie, jednak ogranicza się tylko do dwóch podzielonych plików. – Bas

+0

Jeśli Ci się spodoba, to jest do dyspozycji funkcja upvote;) Może być łatwo dopasowana do większej liczby plików, ale tak, nie jest tak elastyczna jak split -l – DreamFlasher

+0

"jedna linijka" ... psz – Pandem1c

1

bardzo lubiłem wersje Rob i Dennis', tak bardzo, że chciałem je poprawić.

Oto moja wersja:

in_file=$1 
awk '{if (NR!=1) {print}}' $in_file | split -d -a 5 -l 100000 - $in_file"_" # Get all lines except the first, split into 100,000 line chunks 
for file in $in_file"_"* 
do 
    tmp_file=$(mktemp $in_file.XXXXXX) # Create a safer temp file 
    head -n 1 $in_file | cat - $file > $tmp_file # Get header from main file, cat that header with split file contents to temp file 
    mv -f $tmp_file $file # Overwrite non-header containing file with header-containing file 
done 

Różnice:

  1. in_file jest argument plik, który chcesz podzielić zachowaniu nagłówki
  2. Korzystając awk zamiast tail powodu awk posiadające lepszą wydajność
  3. podzielone na 100 000 plików liniowych zamiast 4
  4. Podział nazwa pliku będzie nazwa pliku wejściowego dołączany znaku podkreślenia i liczb (do 99999 - od „-d -a 5” split argumentu)
  5. Zastosowanie mktemp bezpiecznie obsługi plików tymczasowych
  6. Używaj jednego head | cat linię zamiast dwóch linii
0

Wykorzystanie GNU równoległy:

parallel -a bigfile.csv --header : --pipepart 'cat > {#}' 

Jeśli trzeba uruchomić komendę w każdej części, a następnie GNU równoległy może pomóc zrobić, że zbyt:

parallel -a bigfile.csv --header : --pipepart my_program_reading_from_stdin 
parallel -a bigfile.csv --header : --pipepart --fifo my_program_reading_from_fifo {} 
parallel -a bigfile.csv --header : --pipepart --cat my_program_reading_from_a_file {} 

Jeśli chcesz podzielić na dwie części na rdzeń procesora (np.24 rdzenie = 48 równych części rozmiarów):

parallel --block -2 -a bigfile.csv --header : --pipepart my_program_reading_from_stdin 

Jeśli chcesz podzielić na 10 MB bloków:

parallel --block 10M -a bigfile.csv --header : --pipepart my_program_reading_from_stdin