Dividi file di grandi dimensioni in blocchi senza dividere la voce


8

Ho un file .msg piuttosto grande formattato nel formato UIEE.

$ wc -l big_db.msg
8726593 big_db.msg

In sostanza, il file è composto da voci di varie lunghezze che assomigliano a questo:

UR|1
AA|Condon, Richard
TI|Prizzi's Family
CN|Collectable- Good/Good
MT|FICTION
PU|G.P. Putnam & Sons
DP|1986
ED|First Printing.
BD|Hard Cover
NT|0399132104
KE|MAFIA
KE|FICTION
PR|44.9
XA|4
XB|1
XC|BO
XD|S

UR|10
AA|Gariepy, Henry
TI|Portraits of Perseverance
CN|Good/No Jacket
MT|SOLD
PU|Victor Books
DP|1989
BD|Mass Market Paperback
NT|1989 tpb g 100 meditations from the Book of Job "This book...help you
NT| persevere through the struggles of your life..."
KE|Bible
KE|religion
KE|Job
KE|meditations
PR|28.4
XA|4
XB|5
XC|BO
XD|S

Questo è un esempio di due voci, separate da una riga vuota. Vorrei dividere questo grande file in file più piccoli senza interrompere una voce in due file.

Ogni singola voce è separata da una nuova riga (una riga completamente vuota) nel file. Desidero suddividere questo file da 8,7 milioni in 15 file. Capisco che splitesistono strumenti come questi, ma non sono del tutto sicuro di come dividere il file ma lo ho solo diviso su una nuova riga in modo che una singola voce non venga suddivisa in più file.


csplitesiste anche.
Mikeserv,

Puoi creare file temporanei?
Braiam,

@Braiam, non sono sicuro di cosa intendi ma penso di sì. Ho pieno accesso al file system.
user2036066

intende creare file che vengono utilizzati temporaneamente per il processo
polym

1
Perché esattamente 15 file, se posso chiedere? Sono i prefissi prima che il tubo |(come UR, AA, TI) rilevanti per il conteggio dei file, anche lo stesso per l'esattezza?
polimero

Risposte:


2

Ecco una soluzione che potrebbe funzionare:

seq 1 $(((lines=$(wc -l </tmp/file))/16+1)) $lines |
sed 'N;s|\(.*\)\(\n\)\(.*\)|\1d;\1,\3w /tmp/uptoline\3\2\3|;P;$d;D' |
sed -ne :nl -ne '/\n$/!{N;bnl}' -nf - /tmp/file

Funziona permettendo al primo seddi scrivere la seconda sedsceneggiatura. Il secondo sedprimo raccoglie tutte le righe di input fino a quando non incontra una riga vuota. Quindi scrive tutte le righe di output in un file. Il primo sedscrive uno script per il secondo che lo indica su dove scrivere il suo output. Nel mio caso di test, quello script sembrava così:

1d;1,377w /tmp/uptoline377
377d;377,753w /tmp/uptoline753
753d;753,1129w /tmp/uptoline1129
1129d;1129,1505w /tmp/uptoline1505
1505d;1505,1881w /tmp/uptoline1881
1881d;1881,2257w /tmp/uptoline2257
2257d;2257,2633w /tmp/uptoline2633
2633d;2633,3009w /tmp/uptoline3009
3009d;3009,3385w /tmp/uptoline3385
3385d;3385,3761w /tmp/uptoline3761
3761d;3761,4137w /tmp/uptoline4137
4137d;4137,4513w /tmp/uptoline4513
4513d;4513,4889w /tmp/uptoline4889
4889d;4889,5265w /tmp/uptoline5265
5265d;5265,5641w /tmp/uptoline5641

L'ho provato in questo modo:

printf '%s\nand\nmore\nlines\nhere\n\n' $(seq 1000) >/tmp/file

Questo mi ha fornito un file di 6000 righe, che assomigliava a questo:

<iteration#>
and
more
lines
here
#blank

... ripetuto 1000 volte.

Dopo aver eseguito lo script sopra:

set -- /tmp/uptoline*
echo $# total splitfiles
for splitfile do
    echo $splitfile
    wc -l <$splitfile
    tail -n6 $splitfile
done    

PRODUZIONE

15 total splitfiles
/tmp/uptoline1129
378
188
and
more
lines
here

/tmp/uptoline1505
372
250
and
more
lines
here

/tmp/uptoline1881
378
313
and
more
lines
here

/tmp/uptoline2257
378
376
and
more
lines
here

/tmp/uptoline2633
372
438
and
more
lines
here

/tmp/uptoline3009
378
501
and
more
lines
here

/tmp/uptoline3385
378
564
and
more
lines
here

/tmp/uptoline3761
372
626
and
more
lines
here

/tmp/uptoline377
372
62
and
more
lines
here

/tmp/uptoline4137
378
689
and
more
lines
here

/tmp/uptoline4513
378
752
and
more
lines
here

/tmp/uptoline4889
372
814
and
more
lines
here

/tmp/uptoline5265
378
877
and
more
lines
here

/tmp/uptoline5641
378
940
and
more
lines
here

/tmp/uptoline753
378
125
and
more
lines
here

3

Utilizzando il suggerimento di csplit:

Suddivisione in base ai numeri di riga

$ csplit file.txt <num lines> "{repetitions}"

Esempio

Supponiamo di avere un file con 1000 righe al suo interno.

$ seq 1000 > file.txt

$ csplit file.txt 100 "{8}"
288
400
400
400
400
400
400
400
400
405

si traduce in file in questo modo:

$ wc -l xx*
  99 xx00
 100 xx01
 100 xx02
 100 xx03
 100 xx04
 100 xx05
 100 xx06
 100 xx07
 100 xx08
 101 xx09
   1 xx10
1001 total

È possibile aggirare la limitazione statica di dover specificare il numero di ripetizioni pre-calcolando i numeri in base al numero di righe nel proprio file in anticipo.

$ lines=100
$ echo $lines 
100

$ rep=$(( ($(wc -l file.txt | cut -d" " -f1) / $lines) -2 ))
$ echo $rep
8

$ csplit file.txt 100 "{$rep}"
288
400
400
400
400
400
400
400
400
405

Suddivisione basata su righe vuote

Se invece desideri semplicemente suddividere un file su righe vuote contenute nel file, puoi utilizzare questa versione di split:

$ csplit file2.txt '/^$/' "{*}"

Esempio

Supponi di aver aggiunto 4 righe vuote a quanto file.txtsopra e crea il file file2.txt. Puoi vedere che sono stati aggiunti manualmente in questo modo:

$ grep -A1 -B1 "^$" file2.txt
20

21
--
72

73
--
112

113
--
178

179

Quanto sopra mostra che li ho aggiunti tra i numeri corrispondenti all'interno del mio file di esempio. Ora quando eseguo il csplitcomando:

$ csplit file2.txt '/^$/' "{*}"
51
157
134
265
3290

Puoi vedere che ora ho 4 file che sono stati suddivisi in base alla riga vuota:

$ grep -A1 -B1 '^$' xx0*
xx01:
xx01-21
--
xx02:
xx02-73
--
xx03:
xx03-113
--
xx04:
xx04-179

Riferimenti


Ho modificato l'OP con il mio tentativo di utilizzarlo e non sono riuscito a farlo funzionare.
user2036066

Il file non è stato diviso su una nuova riga vuota, che è ciò che ho cercato di realizzare.
user2036066

@ user2036066 - vuoi dividere il file in 15 blocchi di file assicurandoti che non ci sia divisione su una linea parziale o qualcos'altro?
slm

@ user2036066 - aspetta che il file abbia 14-15 righe completamente vuote su cui vuoi dividere?
slm

Modificato di nuovo l'operazione con più contesto @slm
user2036066

3

Se non ti interessano gli ordini dei record, puoi fare:

gawk -vRS= '{printf "%s", $0 RT > "file.out." (NR-1)%15}' file.in

Altrimenti, dovresti prima ottenere il numero di record, per sapere quanti metterli in ogni file di output:

gawk -vRS= -v "n=$(gawk -vRS= 'END {print NR}' file.in)" '
  {printf "%s", $0 RT > "file.out." int((NR-1)*15/n)}' file.in

Anche usare awk per dividere le righe vuote è stato il mio primo pensiero - +1
godlygeek

Cosa sono file.ine file.out?
Mikeserv,

1

Se stai cercando di dividere solo alla fine di una linea, dovresti essere in grado di farlo con l' -lopzione per split.

Se stai cercando di dividere su una riga vuota ( \n\n), ecco come lo farei in ksh. Non l'ho provato, e probabilmente non è l'ideale, ma qualcosa di simile funzionerebbe:

filenum=0
counter=0
limit=580000

while read LINE
do
  counter=counter+1

  if (( counter >= limit ))
  then
    if [[ $LINE == "" ]]
    then
      filenum=filenum+1
      counter=0
    fi
  fi

  echo $LINE >>big_db$filenum.msg
done <big_db.msg

1
È possibile che ho letto male, ma op sta chiedendo come separarsi \n\n, credo.
Mikeserv,

Questo non mi aiuta davvero perché questo dividerà ancora il file a metà voce. Ne ho bisogno, quindi il file verrà diviso solo su una riga vuota.
user2036066

Sì, ho letto male, scusa. Potrebbe non essere il modo migliore, vorrei solo leggere il file originale in un ciclo con un contatore di quante righe hai passato e una volta che hai colpito il numero che vuoi dividere all'inizio, invia un nuovo file al prossimo riga vuota.
Hornj,

Tentativo di testare questo script in questo momento.
user2036066

1
Penso che OP non stia chiedendo come dividere \n\n, ma piuttosto non dividere in mezzo a una linea. Sta chiamando una nuova riga una riga vuota.
polimero

0

Provare awk

awk 'BEGIN{RS="\n\n"}{print $0 > FILENAME"."FNR}' big_db.msg

Tentativo di questa soluzione in questo momento
user2036066

2
Questa soluzione crea un nuovo file per ogni voce, che non è affatto quello che voglio.
user2036066

0

Se non ti interessa l'ordine dei record ma sei particolarmente interessato a ottenere un certo numero di file di output, la risposta di Stephane è la mia strada. Ma ho la sensazione che ti interessi di più a specificare una dimensione che ogni file di output non dovrebbe superare. Questo in realtà lo rende più facile perché puoi leggere il tuo file di input e raccogliere i record fino a raggiungere quella dimensione, quindi avviare un nuovo file di output. Se questo funziona per te, la maggior parte dei linguaggi di programmazione può gestire il tuo compito con uno script breve. Ecco un'implementazione awk:

BEGIN {
    RS = "\n\n"
    ORS = "\n\n"
    maxlen = (maxlen == 0 ? 500000 : maxlen)
    oi = 1
}

{
    reclen = length($0) + 2
    if (n + reclen > maxlen) {
        oi++
        n = 0
    }
    n += reclen
    print $0 > FILENAME"."oi
}

Mettilo in un file, diciamo program.awk, ed eseguilo awk -v maxlen=10000 -f program.awk big_db.msgdove il valore di maxlenè il maggior numero di byte che vuoi in ogni file. Userà 500k come predefinito.

Se si desidera ottenere un determinato numero di file, probabilmente il modo più semplice è quello di dividere la dimensione del file di input per il numero di file desiderato, quindi aggiungere un po 'a quel numero per ottenere maxlen. Ad esempio, per ottenere 15 file dai tuoi 8726593 byte, dividere per 15 per ottenere 581773 e aggiungerne alcuni, quindi magari dare maxlen=590000o maxlen=600000. Se si desidera farlo ripetutamente, sarebbe possibile configurare il programma per farlo.

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.