Aplicando uma função matemática a cada linha em um arquivo grande

2

Eu tenho um arquivo grande que lista uma coluna longa unix vezes um valor para cada linha, incrementando em intervalos de 0,01s. Para um dia de dados, isso equivale a 8,64 milhões de linhas.

135699840000
135699840001
135699840002
135699840003
135699840004

Eu gostaria de executar um comando em cada linha deste arquivo, que calcula o número de data de série para cada linha - um contador de dias a partir de um ano de referência 01/01/0000 que o matlab usa para o tempo.

735235.0000000000
735235.0000001157
735235.0000002314
735235.0000003472
735235.0000004629

Eu sou novo em codificação, mas consegui fazer isso funcionar usando um loop while. No entanto, isso é terrivelmente ineficiente e está levando horas para ser executado.

while read epochtimerange; do
echo "scale=10; (($epochtimerange/(100*86400))+719529)" |bc
done < epochtimerangetmp.txt > serialdaterangetmp.txt

Acho que deve haver uma maneira de executar isso usando o awk, mas não consigo fazer isso funcionar. É importante que eu consiga manter minha precisão de 10 casas decimais na minha saída.

Alguém poderia me ajudar? Obrigado.

    
por L. Marsden 11.11.2016 / 14:47

3 respostas

1

A abordagem simples: Use ex para modificar as linhas e passar todo o buffer (arquivo modificado) através de bc . Em seguida, imprima a versão modificada.

printf '%s\n' '%s:.*:&/8640000+719529:' 0a scale=10 . '%!bc' %p 'q!' | ex file.txt

Saída no seu arquivo de amostra:

735235.0000000000
735235.0000001157
735235.0000002314
735235.0000003472
735235.0000004629

Ou para salvar as alterações, em vez de apenas imprimi-las:

printf '%s\n' '%s:.*:&/8640000+719529:' 0a scale=10 . '%!bc' x | ex file.txt

Explicação:

Para ver quais comandos são passados para ex , execute o comando printf sozinho:

$ printf '%s\n' '%s:.*:&/8640000+719529:' 0a scale=10 . '%!bc' %p 'q!'
%s:.*:&/8640000+719529:
0a
scale=10
.
%!bc
%p
q!

Vamos dividi-los como ex agora. O primeiro é bastante complexo, então vou formatar a explicação especialmente:

%s:.*:&/8640000+719529:
%  - For every line of the buffer (file)
 s  - Run a substitute command
  :  - Using ':' as the regex delimiter
   .*  - Match each entire line
     :  - and replace with
      &  - The entire line, followed by
       /8640000+719529  - this text
                      :  - End command

0a significa "acrescentar texto após a linha 0", em outras palavras, no início do buffer (arquivo).

O texto scale=10 é o texto literal a ser adicionado.

O . em uma linha por si só encerra o comando "append".

O comando %!bc passa o conteúdo de todo o buffer como entrada padrão para o comando externo bc e substitui todo o buffer pela saída que é produzida.

O %p significa imprimir todo o buffer (para a saída padrão).

q! significa sair sem salvar as alterações.

Se você tem um arquivo muito, muito grande , nas dezenas de milhões de linhas, isso aparentemente causa problemas. Eu pesquisei possíveis soluções para isso usando ex e há algumas maneiras que poderia ser feito, mas eu finalmente descartei essa abordagem em favor de uma muito, muito mais simples, que ainda usa apenas ferramentas especificadas POSIX .

Use split para dividir seu arquivo em partes e execute o comando especificado anteriormente em cada bloco e cat a saída resultante todos juntos:

split -l 1000000 -a 3 file.txt myprefix.
for f in myprefix.???; do
  printf '%s\n' '%s:.*:&/8640000+719529:' 0a scale=10 . '%!bc' %p 'q!' |
    ex "$f"
done > myoutputfile.txt
rm myprefix.???

O comando split é usado aqui para dividir file.txt em blocos, cada um com até um milhão de linhas (com o restante colocado em um arquivo também, é claro). Como -a 3 é especificado, o sufixo nos blocos terá 3 caracteres. myprefix.aaa , myprefix.aab , etc.

Cada arquivo pode ser processado por ex individualmente e sem necessidade de salvar as alterações, uma vez que apenas redirecionaremos a saída desse loop inteiro para myoutputfile.txt (e, em seguida, removeremos os arquivos do fragmento, para limpeza).

    
por 11.11.2016 / 23:20
6

É um fato conhecido que os shells têm uma velocidade de processamento muito lenta.
O que você pede poderia ser implementado no shell assim:

#!/bin/bash
while read line; do
    bc <<<"scale=10;($line/(100*86400))+719529"
done <datafile

Demora cerca de 1,1 segundos para processar 1000 linhas.
O lote total de 8,640 milhões deve levar cerca de 2 horas e 41 minutos.

Além disso, os resultados numéricos de bc são arredondados incorretamente.
As cinco linhas do seu exemplo produzirão esses valores:

735235.0000000000
735235.0000001157
735235.0000002314
735235.0000003472
735235.0000004629

Vamos alterar a precisão para 20 para ver mais dígitos:

735235.00000000000000000000
735235.00000011574074074074
735235.00000023148148148148
735235.00000034722222222222
735235.00000046296296296296

Por exemplo, o terceiro que termina em 2314 é arredondado incorretamente, o próximo dígito após o 4 mostrado é um 8 , ele deveria ter sido arredondado para 5 .

AWK

Podemos ter uma solução mais rápida com o awk. Implementar o que você pede no awk ficará assim:

$ awk '{printf ("%.10f\n",($0/(100*86400))+719529)}' datafile

735235.0000000000
735235.0000001157
735235.0000002314
735235.0000003473
735235.0000004630

São necessários apenas 0,006 (6 milissegundos) para processar 1000 linhas. O total de 8,64 milhões de linhas deve ser processado em cerca de 50 segundos.
Mas o awk já está acima de sua faixa de precisão. Por padrão, ele usa uma representação de valores de ponto flutuante de 64 bits. Essa representação tem cerca de 15 dígitos decimais de precisão . Seus resultados de dados têm uma parte inteira de 6 dígitos, a parte decimal pode ser estimada como correta apenas para o 8º dígito.
De fato, se tentarmos estender o número de dígitos:

awk '{printf ("%.20f\n",($0/(100*86400))+719529)}' datafile

recebemos apenas ruído:

735235.00000000000000000000
735235.00000011571682989597
735235.00000023143365979195
735235.00000034726690500975
735235.00000046298373490572

Compare com os resultados mais precisos de bc:

735235.00000000000000000000
735235.00000000000000000000

735235.00000011571682989597
735235.00000011574074074074

735235.00000023143365979195
735235.00000023148148148148

735235.00000034726690500975
735235.00000034722222222222

735235.00000046298373490572
735235.00000046296296296296

Para realmente resolver esse problema, precisamos de um awk mais preciso.

Multiprecision AWK

Se você está usando o GNU awk (vou chamá-lo de gawk por aqui) e ele foi compilado com MPFR (biblioteca de ponto flutuante de precisão múltipla) você pode obter mais precisão.

Verifique se o seu awk tem a biblioteca (é só pedir sua versão):

$ awk --version
GNU Awk 4.1.3, API: 1.1 (GNU MPFR 3.1.5, GNU MP 6.1.1)
Copyright (C) 1989, 1991-2015 Free Software Foundation.

E modifique o comando awk para usar a precisão disponível:

gawk -M -v PREC=100 '{printf ("%.20f\n",($0/(100*86400))+719529)}' datafile

735235.00000000000000000000
735235.00000011574074074074
735235.00000023148148148148
735235.00000034722222222222
735235.00000046296296296296

Os resultados são os mesmos que os de alta precisão bc.
Neste caso, obtemos a velocidade do awk e a precisão de bc.

O comando final para os 10 dígitos decimais que você pede é:

gawk -M -v PREC=100 '{printf ("%.10f\n",($0/(100*86400))+719529)}' datafile

735235.0000000000
735235.0000001157
735235.0000002315
735235.0000003472
735235.0000004630

Todos os valores são arredondados corretamente.

    
por 11.11.2016 / 23:08
2

Fazer isso no shell será muito lento.

$ awk '{printf "%.10f\n", (($1/(100*86400))+719529)}' filename
735235.0000000000
735235.0000001157
735235.0000002314
735235.0000003473
735235.0000004630

Como você pode ver na última entrada, você obterá resultados de arredondamento ligeiramente diferentes.

    
por 11.11.2016 / 15:08

Tags