Dividir archivos grandes en trozos sin dividir la entrada

8

Tengo un archivo .msg bastante grande formateado en el formato UIEE.

$ wc -l big_db.msg
8726593 big_db.msg

Básicamente, el archivo está compuesto por entradas de varias longitudes que se parecen a esto:

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

Este es un ejemplo de dos entradas, separadas por una línea en blanco. Deseo dividir este archivo grande en archivos más pequeños sin dividir una entrada en dos archivos.

Cada entrada individual está separada por una nueva línea (una línea completamente en blanco) en el archivo. Deseo dividir este archivo de 8,7 millones de líneas en 15 archivos. Entiendo que splitexisten herramientas como , pero no estoy muy seguro de cómo dividir el archivo, sino que solo se divide en una nueva línea para que una sola entrada no se divida en varios archivos.

usuario2036066
fuente
csplitTambién existe.
mikeserv
¿Puedes crear archivos temporales?
Braiam
@Braiam, no estoy seguro de lo que quieres decir, pero creo que sí. Tengo acceso completo sobre el sistema de archivos.
user2036066
se refiere a la creación de archivos que se usan temporalmente para el proceso
polym
1
¿Por qué exactamente 15 archivos, si puedo preguntar? Son los prefijos antes de la tubería |(como UR, AA, TI) relevante para el recuento de los archivos, incluso el mismo para ser exactos?
Polym

Respuestas:

2

Aquí hay una solución que podría funcionar:

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

Funciona permitiendo que el primero sedescriba el sedguión del segundo . El segundo sedprimero reúne todas las líneas de entrada hasta que encuentra una línea en blanco. Luego escribe todas las líneas de salida en un archivo. El primero sedescribe un guión para el segundo y le indica dónde escribir su salida. En mi caso de prueba, ese script se veía así:

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

Lo probé así:

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

Esto me proporcionó un archivo de 6000 líneas, que se veía así:

<iteration#>
and
more
lines
here
#blank

... repetido 1000 veces.

Después de ejecutar el script anterior:

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

SALIDA

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
mikeserv
fuente
3

Usando la sugerencia de csplit:

División basada en números de línea

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

Ejemplo

Digamos que tengo un archivo con 1000 líneas.

$ seq 1000 > file.txt

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

da como resultado archivos como este:

$ 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

Puede sortear la limitación estática de tener que especificar el número de repeticiones calculando previamente los números en función del número de líneas en su archivo en particular con anticipación.

$ 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

División basada en líneas en blanco

Si, por otro lado, desea dividir simplemente un archivo en líneas en blanco contenidas en el archivo, puede usar esta versión de split:

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

Ejemplo

Digamos que he agregado 4 líneas en blanco a lo file.txtanterior, y crea el archivo file2.txt. Puede ver que se han agregado manualmente de la siguiente manera:

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

21
--
72

73
--
112

113
--
178

179

Lo anterior muestra que los he agregado entre los números correspondientes dentro de mi archivo de muestra. Ahora cuando ejecuto el csplitcomando:

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

Puede ver que ahora tengo 4 archivos que se han dividido según la línea en blanco:

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

Referencias

slm
fuente
Edité el OP con mi intento de usar esto y no pude hacerlo funcionar.
user2036066
El archivo no se dividió en una nueva línea en blanco, que es lo que he estado tratando de lograr.
user2036066
@ user2036066: ¿desea dividir el archivo en 15 fragmentos de archivo asegurándose de que no haya división en una línea parcial o algo más?
slm
@ user2036066: ¿espera para que el archivo tenga 14-15 líneas completamente en blanco en las que desea dividir?
slm
Editó la
operación
3

Si no le importan las órdenes de los registros, puede hacer lo siguiente:

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

De lo contrario, primero necesitaría obtener el número de registros, para saber cuántos poner en cada archivo de salida:

gawk -vRS= -v "n=$(gawk -vRS= 'END {print NR}' file.in)" '
  {printf "%s", $0 RT > "file.out." int((NR-1)*15/n)}' file.in
Stéphane Chazelas
fuente
El uso de awk a Split con líneas en blanco fue mi primer pensamiento, también - 1
godlygeek
¿Qué son file.iny file.out?
mikeserv
1

Si está buscando dividir solo al final de una línea, debería poder hacerlo con la -lopción para split.

Si está buscando dividir en una línea en blanco ( \n\n), así es como lo haría en ksh. No lo he probado, y probablemente no sea lo ideal, pero algo en esta línea funcionaría:

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
hornj
fuente
1
Es posible que haya leído mal, pero op está preguntando cómo separarse \n\n, creo.
mikeserv
Eso realmente no me ayuda porque eso aún dividirá el archivo a mitad de la entrada. Lo necesito para que el archivo solo se divida en una línea en blanco.
user2036066
Sí, leí mal, lo siento. Puede que no sea la mejor manera, simplemente leería el archivo original en un bucle con un contador de cuántas líneas ha pasado, y una vez que llegue al número que desea dividir, comience a generar un nuevo archivo en el siguiente linea en blanco.
hornj
Intentando probar este script ahora mismo.
user2036066
1
Creo que OP no está preguntando cómo dividirse \n\n, sino más bien no dividirse en medio de una línea. Él está llamando a una nueva línea una línea en blanco.
Polym
0

Tratar awk

awk 'BEGIN{RS="\n\n"}{print $0 > FILENAME"."FNR}' big_db.msg
dchirikov
fuente
Intentando esta solución ahora mismo
user2036066
2
Esta solución crea un nuevo archivo para cada entrada, que no es lo que quiero en absoluto.
user2036066
0

Si no le importa el orden de los registros, pero le interesa obtener una cierta cantidad de archivos de salida, la respuesta de Stephane es el camino que seguiría. Pero tengo la sensación de que podría importarle más especificar un tamaño que cada archivo de salida no debería exceder. En realidad, eso lo hace más fácil porque puede leer su archivo de entrada y recopilar registros hasta alcanzar ese tamaño, y luego comenzar un nuevo archivo de salida. Si eso funciona para usted, la mayoría de los lenguajes de programación pueden manejar su tarea con un script corto. Aquí hay una implementación 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
}

Ponga esto en un archivo, por ejemplo program.awk, y ejecútelo awk -v maxlen=10000 -f program.awk big_db.msgdonde el valor de maxlensea ​​la mayor cantidad de bytes que desee en cualquier archivo. Utilizará 500k por defecto.

Si desea obtener un número determinado de archivos, probablemente la forma más fácil es dividir el tamaño de su archivo de entrada por el número de archivos que desea y luego agregar un poco a ese número para obtener maxlen. Por ejemplo, para obtener 15 archivos de sus 8726593 bytes, divídalos por 15 para obtener 581773, y agregue algunos, así que tal vez dé maxlen=590000o maxlen=600000. Si desea hacer esto repetidamente, sería posible configurar el programa para hacerlo.

David Z
fuente