Divida o arquivo grande em partes sem dividir a entrada

8

Eu tenho um arquivo .msg bastante grande formatado no formato UIEE.

$ wc -l big_db.msg
8726593 big_db.msg

Essencialmente, o arquivo é composto de entradas de vários tamanhos que se parecem com isso:

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 é um exemplo de duas entradas, separadas por uma linha em branco. Desejo dividir esse arquivo grande em arquivos menores sem quebrar uma entrada em dois arquivos.

Cada entrada individual é separada por uma nova linha (uma linha completamente em branco) no arquivo. Desejo dividir esse arquivo de linha de 8,7 milhões em 15 arquivos. Entendo que splitexistem ferramentas como essa, mas não tenho certeza de como dividir o arquivo, mas apenas o dividi em uma nova linha, para que uma única entrada não seja dividida em vários arquivos.

user2036066
fonte
csplittambém existe.
mikeserv
Você pode criar arquivos temporários?
Braiam
@ Braiam, não sei o que você quer dizer, mas acho que sim. Eu tenho acesso total pelo sistema de arquivos.
user2036066
ele significa a criação de arquivos que são usados temporária para o processo
polímer
1
Por que exatamente 15 arquivos, se posso perguntar? São os prefixos antes do tubo |(como UR, AA, TI) relevante para a contagem de arquivos, mesmo a mesma para ser exato?
polímer

Respostas:

2

Aqui está uma solução que poderia 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 permitindo que o primeiro sedescreva o sedscript do segundo . O segundo sedprimeiro reúne todas as linhas de entrada até encontrar uma linha em branco. Em seguida, ele grava todas as linhas de saída em um arquivo. O primeiro sedescreve um script para o segundo, instruindo-o sobre onde escrever sua saída. No meu caso de teste, esse script ficou assim:

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

Eu testei assim:

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

Isso me forneceu um arquivo de 6000 linhas, com a seguinte aparência:

<iteration#>
and
more
lines
here
#blank

... repetido 1000 vezes.

Depois de executar o script acima:

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

RESULTADO

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
fonte
3

Usando a sugestão de csplit:

Divisão com base nos números de linha

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

Exemplo

Digamos que eu tenha um arquivo com 1000 linhas.

$ seq 1000 > file.txt

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

resulta em arquivos assim:

$ 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

Você pode contornar a limitação estática de precisar especificar o número de repetições pré-calculando os números com base no número de linhas em seu arquivo específico com antecedência.

$ 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

Divisão com base em linhas em branco

Se, por outro lado, você deseja simplesmente dividir um arquivo em linhas em branco contidas no arquivo, você pode usar esta versão split:

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

Exemplo

Digamos que adicionei 4 linhas em branco ao texto file.txtacima e crie o arquivo file2.txt. Você pode ver que eles foram adicionados manualmente da seguinte forma:

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

21
--
72

73
--
112

113
--
178

179

O exemplo acima mostra que eu os adicionei entre os números correspondentes no meu arquivo de amostra. Agora, quando eu executo o csplitcomando:

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

Você pode ver que agora tenho 4 arquivos que foram divididos com base na linha em branco:

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

Referências

slm
fonte
Editei o OP com minha tentativa de usá-lo e não consegui fazê-lo funcionar.
user2036066
O arquivo não foi dividido em uma nova linha em branco, que é o que tenho tentado realizar.
user2036066
@ user2036066 - você deseja dividir o arquivo em 15 blocos de arquivos, certificando-se de que não haja divisão em uma linha parcial ou algo mais?
slm
@ user2036066 - aguarde para que o arquivo tenha 14 a 15 linhas completamente em branco em que você deseja dividir?
slm
Editou o op novamente com mais contexto @slm
user2036066
3

Se você não se importa com os pedidos dos registros, pode:

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

Caso contrário, você precisará primeiro obter o número de registros primeiro e saber quantos colocar em cada arquivo de saída:

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
fonte
Usando awk para Split em linhas em branco foi o meu primeiro pensamento, também - +1
godlygeek
O que são file.ine file.out?
mikeserv
1

Se você deseja dividir apenas no final de uma linha, poderá fazê-lo com a -lopção for split.

Se você deseja dividir em uma linha em branco ( \n\n), aqui está como eu faria isso em ksh. Não testei e provavelmente não é o ideal, mas algo nessa linha funcionaria:

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
fonte
1
É possível que eu tenha lido errado, mas op está perguntando como dividir \n\n, eu acho.
mikeserv
Isso realmente não me ajuda, porque isso ainda dividirá o arquivo no meio da entrada. Eu preciso dele para que o arquivo seja dividido apenas em uma linha em branco.
user2036066
Sim, eu interpretei errado, desculpe. Pode não ser a melhor maneira, eu apenas leria o arquivo original em um loop com um contador de quantas linhas você passou e, depois de atingir o número que deseja dividir, comece a gerar um novo arquivo no próximo linha em branco.
hornj
Tentativa de testar este script agora.
user2036066
1
Eu acho que o OP não está perguntando como dividir \n\n, mas sim para não dividir no meio de uma linha. Ele está chamando uma nova linha de uma linha em branco.
polímer
0

Tentar awk

awk 'BEGIN{RS="\n\n"}{print $0 > FILENAME"."FNR}' big_db.msg
dchirikov
fonte
Tentativa de solução agora
user2036066
2
Esta solução cria um novo arquivo para cada entrada, que não é o que eu quero.
user2036066
0

Se você não se importa com a ordem dos registros, mas com particular interesse em obter um certo número de arquivos de saída, a resposta de Stephane é a maneira que eu iria. Mas tenho a sensação de que você pode se importar mais em especificar um tamanho que cada arquivo de saída não deve exceder. Isso realmente facilita, porque você pode ler seu arquivo de entrada e coletar registros até atingir esse tamanho e, em seguida, iniciar um novo arquivo de saída. Se isso funcionar para você, a maioria das linguagens de programação pode lidar com sua tarefa com um script curto. Aqui está uma implementação 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
}

Coloque isso em um arquivo, digamos program.awk, e execute-o com awk -v maxlen=10000 -f program.awk big_db.msgo valor de maxlenmais bytes desejados em qualquer arquivo. Ele usará 500k como padrão.

Se você deseja obter um número definido de arquivos, provavelmente a maneira mais fácil é apenas dividir o tamanho do arquivo de entrada pelo número de arquivos que deseja e, em seguida, adicionar um pouco ao número a ser obtido maxlen. Por exemplo, para obter 15 arquivos dos seus 8726593 bytes, divida por 15 para obter 581773 e adicione alguns, talvez dê maxlen=590000ou maxlen=600000. Se você quiser fazer isso repetidamente, seria possível configurar o programa para fazer isso.

David Z
fonte