Jak mogę podzielić plik tekstowy na wiele plików tekstowych?

16

Mam plik tekstowy o nazwie, entry.txtktóry zawiera następujące elementy:

[ entry1 ]
1239 1240 1242 1391 1392 1394 1486 1487 1489 1600
1601 1603 1657 1658 1660 2075 2076 2078 2322 2323
2325 2740 2741 2743 3082 3083 3085 3291 3292 3294
3481 3482 3484 3633 3634 3636 3690 3691 3693 3766
3767 3769 4526 4527 4529 4583 4584 4586 4773 4774
4776 5153 5154 5156 5628 5629 5631
[ entry2 ]
1239 1240 1242 1391 1392 1394 1486 1487 1489 1600
1601 1603 1657 1658 1660 2075 2076 2078 2322 2323
2325 2740 2741 2743 3082 3083 3085 3291 3292 3294
3481 3482 3484 3690 3691 3693 3766 3767 3769 4526
4527 4529 4583 4584 4586 4773 4774 4776 5153 5154
5156 5628 5629 5631
[ entry3 ]
1239 1240 1242 1391 1392 1394 1486 1487 1489 1600
1601 1603 1657 1658 1660 2075 2076 2078 2322 2323
2325 2740 2741 2743 3082 3083 3085 3291 3292 3294
3481 3482 3484 3690 3691 3693 3766 3767 3769 4241
4242 4244 4526 4527 4529 4583 4584 4586 4773 4774
4776 5153 5154 5156 5495 5496 5498 5628 5629 5631

Chciałbym podzielić ją na trzy pliki tekstowe: entry1.txt, entry2.txt, entry3.txt. Ich zawartość jest następująca.

entry1.txt :

[ entry1 ]
1239 1240 1242 1391 1392 1394 1486 1487 1489 1600
1601 1603 1657 1658 1660 2075 2076 2078 2322 2323
2325 2740 2741 2743 3082 3083 3085 3291 3292 3294
3481 3482 3484 3633 3634 3636 3690 3691 3693 3766
3767 3769 4526 4527 4529 4583 4584 4586 4773 4774
4776 5153 5154 5156 5628 5629 5631

entry2.txt :

[ entry2 ]
1239 1240 1242 1391 1392 1394 1486 1487 1489 1600
1601 1603 1657 1658 1660 2075 2076 2078 2322 2323
2325 2740 2741 2743 3082 3083 3085 3291 3292 3294
3481 3482 3484 3690 3691 3693 3766 3767 3769 4526
4527 4529 4583 4584 4586 4773 4774 4776 5153 5154
5156 5628 5629 5631

entry3.txt :

[ entry3 ]
1239 1240 1242 1391 1392 1394 1486 1487 1489 1600
1601 1603 1657 1658 1660 2075 2076 2078 2322 2323
2325 2740 2741 2743 3082 3083 3085 3291 3292 3294
3481 3482 3484 3690 3691 3693 3766 3767 3769 4241
4242 4244 4526 4527 4529 4583 4584 4586 4773 4774
4776 5153 5154 5156 5495 5496 5498 5628 5629 5631

Innymi słowy, [znak wskazuje, że nowy plik powinien się rozpocząć. Wpisy ( [ entry*]gdzie gdzie *jest liczbą całkowitą) są zawsze w kolejności numerycznej i są kolejnymi liczbami całkowitymi zaczynającymi się od 1 do N (w moim rzeczywistym pliku wejściowym, N = 200001).

Czy jest jakiś sposób na automatyczne dzielenie plików tekstowych w bash? Moje rzeczywiste wejście entry.txtfaktycznie zawiera 200 001 wpisów.

Andrzej
źródło

Odpowiedzi:

11

A oto ładny, prosty, gapiowski jednowarstwowy:

$ gawk '/^\[/{match($0, /^\[ (.+?) \]/, k)} {print >k[1]".txt" }' entry.txt

Będzie to działać dla każdego rozmiaru pliku, niezależnie od liczby linii w każdym wpisie, o ile wygląda nagłówek każdego wpisu [ blahblah blah blah ]. Zwróć uwagę na miejsce tuż po otwarciu [i tuż przed zamknięciem ].


WYJAŚNIENIE:

awki gawkczytać plik wejściowy linia po linii. Gdy każdy wiersz jest czytany, jego zawartość jest zapisywana w $0zmiennej. Mówimy tutaj, gawkaby dopasować wszystko w nawiasach kwadratowych i zapisać dopasowanie w tablicy k.

Tak więc za każdym razem, gdy wyrażenie regularne jest dopasowane, to znaczy dla każdego nagłówka w pliku, k [1] będzie miał dopasowany region linii. Mianowicie „entry1”, „entry2” lub „entry3” lub „entryN”.

Na koniec drukujemy każdą linię do pliku o nazwie <whatever value k currently has>.txt, tj. Entry1.txt, entry2.txt ... entryN.txt.

Ta metoda będzie znacznie szybsza niż Perl dla większych plików.

terdon
źródło
+1 fajnie. Nie musisz matchwpisywać: /^\[/ { name=$2 }powinno wystarczyć.
Thor
Dzięki @Thor. Twoja sugestia jest poprawna dla opisanego przypadku, ale zakłada, że ​​w nazwie wpisu nigdy nie ma spacji. Dlatego użyłem tego przykładu [ blahblah blah blah ]w mojej odpowiedzi.
terdon
Ach, tęskniłem trochę za wpisami oddzielonymi spacją. Możesz również pomieścić osoby FS, np -F '\\[ | \\]'.
Thor
@terdon Bardzo podoba mi się to krótkie rozwiązanie, niestety zazwyczaj nie generalizuję go do moich potrzeb. Czy możesz mi pomóc? Mój plik ma linie zaczynające się od #S x, gdzie x jest liczbą 1, 2 lub 3 cyfr. Wystarczy zapisać je w x.dat. Próbowałem: gawk '/^#S/{match($0, / [0-9]* /, k)} {print >k[1]".dat" }' myFile.txti kilka jego odmian.
mikuszefski
Mam gawk '/^#S/{match($0, /^#S (\s+?)([0-9]+)(\s+?)/, k)} {print >k[2]".txt" }' test.txtrację. 2Jednak nie rozumiem zbyt dobrze numeru tablicy .
mikuszefski
17

Z csplit z GNU coreutils ( niewbudowany Linux, Cygwin):

csplit -f entry -b '%d.txt' entry.txt '/^\[ .* \]$/' '{*}'

Otrzymasz dodatkowy pusty plik entry0.txt(zawierający część przed pierwszym nagłówkiem).

Standardowy csplit nie ma {*}nieokreślonego repeatera i -bopcji określania formatu sufiksu, więc w innych systemach musisz najpierw policzyć liczbę sekcji, a potem zmienić nazwy plików wyjściowych.

csplit -f entry -n 9 entry.txt '/^\[ .* \]$/' "{$(egrep -c '^'\[ .* \]$' <entry.txt)}"
for x in entry?????????; do
  y=$((1$x - 1000000000))
  mv "entry$x" "entry$y.txt"
done
Gilles „SO- przestań być zły”
źródło
Uważam, że csplit jest od czasu do czasu nieco dziwaczny, ale niezwykle przydatny, gdy chcę to zrobić.
ixtmixilix
10

W Perlu można to zrobić znacznie prościej:

perl -ne 'open(F, ">", ($1).".txt") if /\[ (entry\d+) \]/; print F;' file
wysypka
źródło
9

Oto krótki linijka awk:

awk '/^\[/ {ofn=$2 ".txt"} ofn {print > ofn}' input.txt

Jak to działa?

  • /^\[/ dopasowuje linie zaczynające się od lewego nawiasu kwadratowego, oraz
  • {ofn=$2 ".txt"}ustawia zmienną na drugie słowo rozdzielane białymi odstępami jako naszą nazwę pliku wyjściowego. Następnie,
  • ofn to warunek, który ma wartość true, jeśli zmienna jest ustawiona (powoduje to, że wiersze przed pierwszym nagłówkiem zostaną zignorowane)
  • {print > ofn} przekierowuje bieżącą linię do określonego pliku.

Zauważ, że wszystkie spacje w tym skrypcie awk można usunąć, jeśli zwartość cię uszczęśliwia.

Zauważ również, że powyższy skrypt naprawdę potrzebuje nagłówków sekcji, aby mieć spacje wokół siebie, a nie w nich. Jeśli chcesz mieć możliwość obsługi nagłówków sekcji takich jak [foo]i [ this that ], potrzebujesz jeszcze trochę więcej kodu:

awk '/^\[/ {sub(/^\[ */,""); sub(/ *\] *$/,""); ofn=$0 ".txt"} ofn {print > ofn}' input.txt

Używa sub()funkcji awk do usuwania wiodących i końcowych nawiasów kwadratowych plus białych znaków. Zauważ, że zgodnie ze standardowym zachowaniem awk spowoduje to zwinięcie białych znaków (separatora pól) w jedną spację (tzn. Zostanie [ this that ]zapisany "this that.txt"). Jeśli zachowanie oryginalnych białych znaków w wyjściowych nazwach plików jest ważne, możesz eksperymentować, ustawiając FS.

ghoti
źródło
2

Można to zrobić z wiersza poleceń w pythonie jako:

paddy$ python3 -c 'out=0
> with open("entry.txt") as f: 
>   for line in f:
>     if line[0] == "[":
>       if out: out.close()
>       out = open(line.split()[1] + ".txt", "w")
>     else: out.write(line)'
Paddy3118
źródło
2

Jest to dość prymitywny, ale łatwy do zrozumienia sposób: użyj, grep -l '[ entry ]' FILENAMEaby podzielić numery linii na [entry]. Użyj kombinacji z głową i ogonem, aby uzyskać odpowiednie kawałki.

Tak jak powiedziałem; nie jest ładny, ale łatwo go zrozumieć.

Sigurt Dinesen
źródło
2

Co powiesz na użycie awk z [jako separatora rekordów i spacji jako separatora pól. Daje nam to łatwo dane do umieszczenia w pliku jako miejsce, w $0którym musi on umieścić z powrotem usunięte wiodące [i nazwę pliku jako $1. Musimy wtedy poradzić sobie tylko ze specjalnym przypadkiem pierwszego rekordu, który jest pusty. To daje nam:

awk -v "RS=[" -F " " 'NF != 0 {print "[" $0 > $1}' entry.txt
jfg956
źródło
2

Odpowiedź terdona działa dla mnie, ale musiałem używać gawk, a nie awk. Podręcznik gawk (wyszukiwanie „match (”) wyjaśnia, że ​​argument tablicy w match () jest rozszerzeniem gawk. Może to zależy od instalacji Linuksa i wersji awk / nawk / gawk, ale na mojej maszynie Ubuntu tylko gawk działał znakomicie odpowiedź:

$ gawk '{if(match($0, /^\[ (.+?) \]/, k)){name=k[1]}} {print >name".txt" }' entry.txt
użytkownik31371
źródło
1

Oto rozwiązanie perla. Ten skrypt wykrywa [ entryN ]linie i odpowiednio zmienia plik wyjściowy, ale nie sprawdza, nie analizuje ani nie przetwarza danych w każdej sekcji, po prostu drukuje linię wejściową do pliku wyjściowego.

#! /usr/bin/perl 

# default output file is /dev/null - i.e. dump any input before
# the first [ entryN ] line.

$outfile='/dev/null';
open(OUTFILE,">",$outfile) || die "couldn't open $outfile: $!";

while(<>) {
  # uncomment next two lines to optionally remove comments (starting with
  # '#') and skip blank lines.  Also removes leading and trailing
  # whitespace from each line.
  # s/#.*|^\s*|\s*$//g;
  # next if (/^$/)

  # if line begins with '[', extract the filename
  if (m/^\[/) {
    (undef,$outfile,undef) = split ;
    close(OUTFILE);
    open(OUTFILE,">","$outfile.txt") || die "couldn't open $outfile.txt: $!";
  } else {
    print OUTFILE;
  }
}
close(OUTFILE);
cas
źródło
1

Cześć. Napisałem ten prosty skrypt, używając ruby, aby rozwiązać problem

#!ruby
# File Name: split.rb

fout = nil

while STDIN.gets
  line = $_
  if line.start_with? '['
    fout.close if fout
    fname = line.split(' ')[1] + '.txt'
    fout = File.new fname,'w'
  end
  fout.write line if fout
end

fout.close if fout

możesz użyć tego w ten sposób:

ruby split.rb < entry.txt

przetestowałem to i działa dobrze ..

Kokizzu
źródło
1

Wolę tę csplitopcję, ale alternatywą jest rozwiązanie awk GNU:

parse.awk

BEGIN { 
  RS="\\[ entry[0-9]+ \\]\n"  # Record separator
  ORS=""                      # Reduce whitespace on output
}
NR == 1 { f=RT }              # Entries are of-by-one relative to matched RS
NR  > 1 {
  split(f, a, " ")            # Assuming entries do not have spaces 
  print f  > a[2] ".txt"      # a[2] now holds the bare entry name
  print   >> a[2] ".txt"
  f = RT                      # Remember next entry name
}

Uruchom tak:

gawk -f parse.awk entry.txt
Thor
źródło
1
FWIW, RTzmienna wydaje się być specyficzna dla gawk. To rozwiązanie nie działa dla mnie przy użyciu awk FreeBSD.
ghoti
@ghoti: Racja, powinienem o tym wspomnieć. Uwzględniłem to teraz w odpowiedzi. Dzięki.
Thor