Usando o awk para dividir o arquivo de texto a cada 10.000 linhas

7

Eu tenho um grande arquivo de texto gzip'd. Eu gostaria de algo como:

zcat BIGFILE.GZ | \
    awk (snag 10,000 lines and redirect to...) | \
    gzip -9 smallerPartFile.gz

o awk parte lá em cima, eu basicamente quero que ele pegue 10.000 linhas e envie para gzip e repita até que todas as linhas no arquivo de entrada original sejam consumidas. Eu encontrei um script que afirma fazer isso, mas quando eu corro em meus arquivos e, em seguida, difiro o original para os que foram divididos e, em seguida, mesclados, as linhas estão faltando. Então, algo está errado com a parte awk e não tenho certeza de qual parte está quebrada.

O objetivo:

  • Leia o arquivo de origem uma vez para toda a operação
  • Divida a origem em partes menores, delimitadas por nova linha. Diga, 10.000 linhas por arquivo
  • Compacte os arquivos de destino que são criados como resultado da ação de divisão e faça isso sem uma etapa extra depois que esse script for processado.

Aqui está o código. Alguém pode me dizer por que isso não produz um arquivo que pode ser dividido e mesclado e, em seguida, diferenciado para o original com sucesso?

# Generate files part0.dat.gz, part1.dat.gz, etc.
# restore with: zcat foo* | gzip -9 > restoredFoo.sql.gz (or something like that)
prefix="foo"
count=0
suffix=".sql"

lines=10000 # Split every 10000 line.

zcat /home/foo/foo.sql.gz |
while true; do
  partname=${prefix}${count}${suffix}

  # Use awk to read the required number of lines from the input stream.
  awk -v lines=${lines} 'NR <= lines {print} NR == lines {exit}' >${partname}

  if [[ -s ${partname} ]]; then
    # Compress this part file.
    gzip -9 ${partname}
    (( ++count ))
  else
    # Last file generated is empty, delete it.
    rm -f ${partname}
    break
  fi
done
    
por Sneaky Wombat 08.10.2012 / 22:59

5 respostas

5

Eu sugiro fazer toda a manutenção dentro de awk , isso funciona aqui com o GNU awk:

BEGIN { file = "1" }

{ print | "gzip -9 > " file ".gz" }

NR % 10000 == 0 {
  close("gzip -9 > " file ".gz")
  file = file + 1
}

Isso salvará 10000 linhas para 1.gz , as próximas 10000 para 2.gz , etc. Use sprintf se você quiser mais flexibilidade na geração de nome de arquivo.

Atualizado com um teste

Os dados de teste usados são de até 300k, encontrados aqui .

wc -lc primes; md5sum primes

Saída:

25997 196958 primes
547d527ec50c2799fa6ce96dba3c26c0  primes

Agora, se o programa awk acima foi salvo em split.awk e executado assim (com o GNU awk):

awk -f split.awk primes

Três arquivos (1.gz, 2.gz e 3.gz) são produzidos. Testando estes arquivos:

for f in {1..3}; do gzip -dc $f.gz >> foo; done

Teste:

diff source.file foo

A saída não deve ser nada se os arquivos forem os mesmos.

E os mesmos testes acima:

gzip -dc [1-3].gz | tee >(wc -lc) >(md5sum) > /dev/null

Saída:

25997  196958
547d527ec50c2799fa6ce96dba3c26c0  -

Isso mostra que o conteúdo é o mesmo e que os arquivos são divididos conforme o esperado.

    
por 09.10.2012 / 23:04
3

A resposta mais curta (e mais útil): você viu o comando Unix split ?

    
por 09.10.2012 / 00:01
3

A resposta curta é que awk está lendo sua entrada (o canal de zcat , nesse caso) um bloco por vez (em que um bloco é de 512 bytes ou um múltiplo dele, dependendo do sistema operacional) . Então, no momento em que tem o 10000º caractere de nova linha (marcador de fim de linha) na memória, ele também tem a 10001ª linha, a 10002ª, e muito provavelmente mais (ou possivelmente menos) na memória também. Isso é um problema porque significa que esses caracteres foram lidos do canal e não estão mais disponíveis para a próxima iteração de awk .

    
por 08.10.2012 / 23:47
3

Eu pensei sobre isso e encontrei um jeito, nada eficiente, que irá descomprimir completamente cada arquivo para cada peça, o que significa que se você quiser dividir em 20 partes, ele irá descompactar os arquivos grandes 20 vezes. Mas ele não armazena o arquivo inteiro, apenas a parte compactada, por isso, embora o armazenamento seja eficiente, ele é ineficiente.

O script deve ser executado com o primeiro argumento, o arquivo gzip grande e o segundo argumento, o número de linhas a serem divididas.

#!/bin/bash
GZIP_FILE=$1
SPLIT_LINES=$2
TOTAL_LINES='zcat $GZIP_FILE|wc -l'
START=0
NEXT_START=0
while [ $NEXT_START -lt $TOTAL_LINES ]; do
        NEXT_START=$(( $NEXT_START + $SPLIT_LINES ))
        echo .
        zcat $GZIP_FILE|sed -n ${START},${NEXT_START}p |gzip -9 > ${GZIP_FILE}.lines-${START}-${NEXT_START}.gz
        START=$NEXT_START
done

Isso criará no mesmo diretório de cada arquivo um arquivo chamado gzip e adicionando ".lines- $ startline- $ endline.gz"

Espero que você esteja bem desperdiçando CPU:)

    
por 09.10.2012 / 00:46
1

Você tem uma alternativa para o awk. Aqui está como você pode fazer isso com o GNU split ou o GNU paralelo.

A divisão do GNU tem uma opção --filter e algo muito próximo do que você está tentando fazer está descrito no manual:

'--filter=COMMAND'
     With this option, rather than simply writing to each output file,
     write through a pipe to the specified shell COMMAND for each
     output file.  COMMAND should use the $FILE environment variable,
     which is set to a different output file name for each invocation
     of the command.  For example, imagine that you have a 1TiB
     compressed file that, if uncompressed, would be too large to
     reside on disk, yet you must split it into individually-compressed
     pieces of a more manageable size.  To do that, you might run this
     command:

          xz -dc BIG.xz | split -b200G --filter='xz > $FILE.xz' - big-

     Assuming a 10:1 compression ratio, that would create about fifty
     20GiB files with names 'big-xaa.xz', 'big-xab.xz', 'big-xac.xz',
     etc.

Então, no seu caso, você poderia fazer:

zcat bigfile.gz | split -l 10000 --filter='gzip -9 > $FILE.gz' - big-

Uma boa alternativa para dividir seria usar o GNU paralelo, isso permitiria paralelizar a compactação:

zcat bigfile.gz | parallel --pipe -N 10000 'gzip > {#}.gz'
    
por 06.05.2013 / 15:37

Tags