Acelerar a separação de um grande arquivo de texto com base no conteúdo de linha em Bash

votos
7

I ter um ficheiro de texto muito grande (cerca de 20 GB e 300 milhões de linhas), que contém três colunas separadas por abas:

word1 word2 word3
word1 word2 word3
word1 word2 word3
word1 word2 word3

palavra1, word2, e word3 são diferentes em cada linha. word3 especifica da classe da linha, e repete com frequência para diferentes linhas (tendo milhares de valores diferentes). O objetivo é separar o arquivo pela classe linha (word3). Ou seja word1 e word2 deve ser armazenado em um arquivo chamado word3, para todas as linhas. Por exemplo, para a linha:

a b c

a string a b deve ser anexado ao arquivo chamado c.

Agora eu sei como isso pode ser feito com um loop while, a leitura linha a linha de um arquivo, e anexando o arquivo apropriado para cada linha:

while IFS='' read -r line || [[ -n $line ]]; do
    # Variables
    read -a line_array <<< ${line}
    word1=${line_array[0]}
    word2=${line_array[1]}
    word3=${line_array[2]}

    # Adding word1 and word2 to file word3
    echo ${word1} ${word2} >> ${word3}  
done < inputfile

Ele funciona, mas é muito lento (apesar de eu ter uma estação de trabalho rápido com um SSD). Como isso pode ser acelerar? Eu já tentei realizar este procedimento em / dev / shm, e dividos o arquivo em 10 peças e executar o script acima em paralelo para cada arquivo. Mas ainda é bastante lento. Existe uma maneira de acelerar ainda mais esta up?

Publicado 20/10/2018 em 14:07
fonte usuário
Em outras línguas...                            


5 respostas

votos
4

Vamos gerar um arquivo de exemplo:

$ seq -f "%.0f" 3000000 | awk -F $'\t' '{print $1 FS "Col_B" FS int(2000*rand())}' >file

Isso gera um arquivo de 3 milhões de linhas com 2.000 valores diferentes na coluna 3 semelhante a este:

$ head -n 3 file; echo "..."; tail -n 3 file
1   Col_B   1680
2   Col_B   788
3   Col_B   1566
...
2999998 Col_B   1562
2999999 Col_B   1803
3000000 Col_B   1252

Com um simples awkvocê pode gerar os arquivos que você descreve desta forma:

$ time awk -F $'\t' '{ print $1 " " $2 >> $3; close($3) }' file
real    3m31.011s
user    0m25.260s
sys     3m0.994s

Assim que awk irá gerar os 2.000 arquivos do grupo em cerca de 3 minutos 31 segundos. Certamente mais rápido do que Bash, mas isso pode ser mais rápido, pré-ordenação processo pela terceira coluna e escrever cada arquivo de grupo de uma só vez.

Você pode usar o Unix sortutilidade em um tubo e alimentar a saída para um script que pode separar os grupos classificados para diferentes arquivos. Use a -sopção com sorte o valor do terceiro campo serão os únicos campos que vão mudar a ordem das linhas.

Desde podemos supor sorttem dividido o arquivo em grupos com base na coluna 3 do arquivo, o script só precisa detectar quando que as mudanças de valor:

$ time sort -s -k3 file | awk -F $'\t' 'fn != ($3 "") { close(fn); fn = $3 } { print $1 " " $2 > fn }'
real    0m4.727s
user    0m5.495s
sys     0m0.541s

Por causa da eficiência adquirida por pré-classificação, o mesmo processo net termina em 5 segundos.

Se tiver certeza de que as 'palavras' da coluna 3 são apenas ascii (ou seja, você não precisa lidar com UTF-8), você pode definir LC_ALL=Cpara velocidade adicional :

$ time LC_ALL=C sort -s -k3 file | awk -F $'\t' 'fn != ($3 "") { close(fn); fn = $3 } { print $1 " " $2 > fn }'
real    0m3.801s
user    0m3.796s
sys     0m0.479s

Do comentários:

1) Por favor, adicione uma linha para explicar por que precisamos da expressão entre colchetes emfn != ($3 "") :

A awkconstrução de fn != ($3 "") {action}uma atalho eficaz para fn != $3 || fn=="" {action}o uso a que você considerar mais legível.

2) Não sei se isso também funciona se o arquivo é maior do que a memória disponível, então isso pode ser um fator limitante. :

Corri o primeiro eo último awk com 300 milhões de discos e 20.000 arquivos de saída. O último com sorte fez a tarefa em 12 minutos. O primeiro levou 10 horas ...

Pode ser que a versão espécie realmente escala melhor desde a abertura e fechamento acrescentando 20.000 arquivos de 300 milhões de vezes leva um tempo. É mais eficiente para conspirar e transmitir dados semelhantes.

3) Eu estava pensando em espécie no início, mas depois senti que pode não ser o mais rápido, porque temos que ler todo o arquivo duas vezes com esta abordagem. :

Este é o caso de dados puramente aleatório; Se os dados reais são um pouco ordenada, há uma troca com a leitura do arquivo duas vezes. O primeiro awk seria significativamente mais rápido com menos dados aleatórios. Mas então ele também vai levar tempo para determinar se o arquivo está classificada. Se você sabe de arquivos é maioritariamente classificadas, use o primeiro; se é provável um pouco desordenado, utilize o último.

Respondeu 20/10/2018 em 19:12
fonte usuário

votos
3

Você pode usar awk:

awk -F $'\t' '{ print $1 " " $2 >> $3; close($3) }' file
Respondeu 20/10/2018 em 14:17
fonte usuário

votos
2

Esta solução utiliza GNU paralelo, mas pode ser sintonizado com as outras awksoluções. Também tem uma boa barra de progresso:

parallel -a data_file --bar 'read -a arr <<< {}; echo "${arr[0]} ${arr[1]}" >> ${arr[2]}'
Respondeu 20/10/2018 em 14:34
fonte usuário

votos
2

Use awk, por exemplo:

awk -F '{ print $1 FS $2 > $3 }' FILES

Ou este script Perl (escrito por mim) - eu não vou repassar-lo aqui, pois é um pouco mais. awkdeve ser um pouco mais lento, uma vez que (re) abre os arquivos para cada linha. Isto é melhor do que o script Perl sempre que você tem mais de 250 diferentes valores / arquivos de saída (ou qualquer que seja o seu sistema operacional tem como limite para o número de filehandles abertos simultaneamente). O script Perl tenta segurar todos os dados na memória, o que é muito mais rápido, mas pode ser problemático para grandes entradas.

A solução para uma grande contagem de arquivos de saída foi postado por oguzismail usuário:

awk '{ print $1 FS $2 >> $3; close($3) }' file

Este (re) abre o arquivo de saída para cada linha e não vai correr para o problema de ter muitos filehandles saída aberto aberto ao mesmo tempo. (Re) abrindo o arquivo pode ser mais lenta, mas supostamente não é.

Edit: Corrigido awkinvocação - é impressa toda a linha para a saída, em vez das duas primeiras colunas.
Respondeu 20/10/2018 em 14:18
fonte usuário

votos
1

Você pergunta é muito semelhante em natureza à É possível paralelizar awk escrita a vários arquivos através GNU paralelo?

Se o disco pode lidar com isso:

splitter() {
  mkdir -p $1
  cd $1
  awk -F $'\t' '{ print $1 " " $2 >> $3; close($3) }'
}
export -f splitter
# Do the splitting in each dir 
parallel --pipepart -a myfile --block -1 splitter {%}
# Merge the results
parallel 'cd {}; ls' ::: dir-* | sort -u | parallel 'cat */{} > {}'
# Cleanup dirs
rm -r */
Respondeu 20/10/2018 em 16:53
fonte usuário

Cookies help us deliver our services. By using our services, you agree to our use of cookies. Learn more