Come posso dividere un file di testo in più file di testo?


16

Ho un file di testo chiamato entry.txtche contiene quanto segue:

[ 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

Vorrei dividerlo in tre file di testo: entry1.txt, entry2.txt, entry3.txt. I loro contenuti sono i seguenti.

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

In altre parole, il [carattere indica che dovrebbe iniziare un nuovo file. Le voci ( [ entry*], dove *è un numero intero) sono sempre in ordine numerico e sono numeri interi consecutivi a partire da 1 a N (nel mio file di input effettivo, N = 200001).

Esiste un modo per eseguire la suddivisione automatica dei file di testo in bash? Il mio vero input in entry.txtrealtà contiene 200.001 voci.

Risposte:


11

Ed ecco un bello, semplice, strabiliante one-liner:

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

Funzionerà per qualsiasi dimensione di file, indipendentemente dal numero di righe in ciascuna voce, purché appaia ogni intestazione della voce [ blahblah blah blah ]. Notare lo spazio subito dopo l'apertura [e poco prima della chiusura ].


SPIEGAZIONE:

awke gawkleggi un file di input riga per riga. Mentre ogni riga viene letta, il suo contenuto viene salvato nella $0variabile. Qui, stiamo dicendo gawkdi abbinare qualsiasi cosa tra parentesi quadre e salvarne la corrispondenza nella matrice k.

Pertanto, ogni volta che viene confrontata l'espressione regolare, ovvero per ogni intestazione del file, k [1] avrà la regione corrispondente della riga. Vale a dire "entry1", "entry2" o "entry3" o "entryN".

Infine, stampiamo ogni riga in un file chiamato <whatever value k currently has>.txt, cioè entry1.txt, entry2.txt ... entryN.txt.

Questo metodo sarà molto più veloce di perl per file più grandi.


+1 simpatico. Non è necessario per matchl'ingresso: /^\[/ { name=$2 }dovrebbe essere sufficiente.
Thor,

Grazie @Thor. Il tuo suggerimento è corretto per il caso descritto, ma presuppone che non ci sia mai uno spazio nel nome della voce. Ecco perché ho usato l'esempio [ blahblah blah blah ]nella mia risposta.
terdon

Ah, mi sono perso un po 'le voci separate da spazio. Potresti anche ospitare quelli con FS, ad es -F '\\[ | \\]'.
Thor,

@terdon Mi piacciono molto queste brevi soluzioni, purtroppo di solito non riesco a generalizzarle alle mie esigenze. Potresti darmi una mano? Il mio file ha delle righe che iniziano con #S x, dove x è un numero di 1, 2 o 3 cifre. Basta salvarli in x.dat sarebbe sufficiente. Ho provato: gawk '/^#S/{match($0, / [0-9]* /, k)} {print >k[1]".dat" }' myFile.txte alcune varianti di questo.
Mikuszefski,

Ho gawk '/^#S/{match($0, /^#S (\s+?)([0-9]+)(\s+?)/, k)} {print >k[2]".txt" }' test.txtfatto il trucco. 2Tuttavia, non capisco molto bene il numero di array .
Mikuszefski,

17

Con csplit da GNU coreutils (Linux non incorporato, Cygwin):

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

Ti ritroverai con un file vuoto aggiuntivo entry0.txt(contenente la parte prima della prima intestazione).

Lo standard csplit non ha il {*}ripetitore indefinito e l' -bopzione per specificare il formato del suffisso, quindi su altri sistemi dovrai prima contare il numero di sezioni e poi rinominare i file di output.

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

trovo che csplit sia un po 'bizzarro di tanto in tanto, ma incredibilmente utile quando voglio fare questo genere di cose.
ixtmixilix,

10

In perl può essere fatto molto più semplice:

perl -ne 'open(F, ">", ($1).".txt") if /\[ (entry\d+) \]/; print F;' file

9

Ecco una breve riga singola:

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

Come funziona?

  • /^\[/ corrisponde alle linee che iniziano con una parentesi quadra sinistra e
  • {ofn=$2 ".txt"}imposta una variabile sulla seconda parola delimitata da spazi bianchi come nome del nostro file di output. Poi,
  • ofn è una condizione che viene valutata vera se la variabile è impostata (facendo in modo che le righe prima della prima intestazione vengano ignorate)
  • {print > ofn} reindirizza la riga corrente al file specificato.

Nota che tutti gli spazi in questo script awk possono essere rimossi, se la compattezza ti rende felice.

Nota anche che lo script sopra ha davvero bisogno che le intestazioni di sezione abbiano spazi attorno e non al loro interno. Se vuoi essere in grado di gestire le intestazioni di sezione come [foo]e [ this that ], avresti bisogno di un codice leggermente maggiore:

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

Questo usa la sub()funzione di awk per eliminare gli spazi tra parentesi quadre iniziali e finali. Si noti che per comportamento awk standard, questo comprime gli spazi bianchi (il separatore di campo) in un singolo spazio (cioè [ this that ]viene salvato in "this that.txt"). Se è importante mantenere lo spazio bianco originale nei nomi dei file di output, puoi provare impostando FS.


2

Può essere fatto dalla riga di comando in Python come:

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)'

2

Questo è un modo un po 'grezzo, ma facilmente comprensibile per farlo: usare grep -l '[ entry ]' FILENAMEper far dividere i numeri di riga in [entry]. Usa una combinazione di testa e coda per ottenere i pezzi giusti.

Come ho detto; non è carino, ma è facile da capire.


2

Che dire dell'utilizzo di awk con [come separatore di record e dello spazio come separatore di campo. Questo ci dà facilmente i dati da inserire nel file come $0dove deve rimettere il lead rimosso [e il nome file come $1. Dobbiamo quindi solo gestire il caso speciale del 1 ° record che è vuoto. Questo ci dà:

awk -v "RS=[" -F " " 'NF != 0 {print "[" $0 > $1}' entry.txt

2

La risposta di Terdon funziona per me, ma avevo bisogno di usare Gawk, non Awk. Il manuale di gawk (cerca 'match (') spiega che l'argomento array in match () è un'estensione gawk. Forse dipende dalla tua installazione di Linux e dalle tue versioni di awk / nawk / gawk ma sulla mia macchina Ubuntu solo gawk ha funzionato con terdon eccellente risposta:

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

1

Ecco una soluzione perl. Questo script rileva le [ entryN ]righe e modifica di conseguenza il file di output, ma non convalida, analizza o elabora i dati in ciascuna sezione, stampa semplicemente la riga di input nel file di output.

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

1

Ciao, ho scritto questo semplice script usando ruby ​​per risolvere il tuo problema

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

puoi usarlo in questo modo:

ruby split.rb < entry.txt

l'ho provato e funziona benissimo ..


1

Preferisco l' csplitopzione ma in alternativa ecco una soluzione GNU awk:

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
}

Eseguilo in questo modo:

gawk -f parse.awk entry.txt

1
FWIW, la RTvariabile sembra essere specifica per gawk. Questa soluzione non funziona per me usando awk di FreeBSD.
ghoti,

@ghoti: giusto, avrei dovuto dirlo. L'ho incluso nella risposta ora. Grazie.
Thor,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.