Organizando dados com shell script

Muitas vezes você pode ter uma grande quantidade de arquivos contendo dados e deseja organizá-las, de modo que cada linha corresponda a informação obtida em um determinado horário. Geralmente os algoritmos desenvolvidos buscam um melhor desempenho da máquina apoiando-se em padrões como nomes de arquivos, ordenamento de datas utilizando dia do ano e número de segundos desde uma determinada data, etc.

Caso os dados estejam realmente uma bagunça, aqui segue um algoritmo inverso: data e horário são criados em uma string que é buscada em toda a base de dados, copiando as linhas encontradas para um novo arquivo; um comando em awk apaga as linhas duplicadas sem alterar a ordem (ou seja, sem utilizar o comando “sort”). É bem mais demorado computacionalmente (por isso inclui um comando juntando toda a informação em um único arquivo, para não precisar ficar abrindo e fechando arquivos para leitura), mas é uma opção caso não possa contar com uma melhor organização na base de dados.

#!/bin/bash
# Programa para ler linhas de arquivo contendo datas e organizar em ordem cronológica, sem repetições
# Formato da linha: 1,2013,169,937,0,2.332,.878 onde id,ano,dia_do_ano,HHMM,dado1,dado2,dado3
# obs: 1 = 00:01, 100 = 01:00

function coloca_zero(){
    a=$1
    b=`echo "$a" | wc -L | awk '{print $1}'`
    while [ $b != 2 ]; do
        b=$((b+1))
        a=0$a
    done
    echo $a
}

cat *PI* > dados.dat
# Opção para concatenar arquivos sem quebra de linha como último caractere
#for file in $(ls *PI*); do
# cat $file
# echo
#done > dados.dat
# Outra opção
#paste --delimiter=\\n --serial *PI*

linha=1
for ano in $(seq 2012 2014); do
	for dia_do_ano in $(seq 1 366); do
		for hora in $(seq 0 23); do
			for min in $(seq 0 59); do
				if [ "$hora" -eq "0" ]; then
					horas=''
					minuto=$min
				else
					horas=$hora
					minuto=`coloca_zero "$min"`
				fi
				exp=`echo 1,$ano,$dia_do_ano,$horas$minuto,`
				grep $exp dados.dat
			done
		done
	done
done > arq.dat

#Eliminar linhas duplicadas SEM alterar ordem das linhas
#sort arq.dat | uniq > arq_final.dat
awk '!($0 in a) {a[$0];print}' arq.dat > arq_PI.dat

rm dados.dat arq.dat

Esse outro script tem a função de calcular médias a cada cinco minutos com eventuais falhas na sequência de dados. É composto de duas funções: “coloca_zero”, que deixa dia, hora e minuto com dois dígitos, e “calcula_media”, que calcula a média coluna por coluna (usando “bc“) e monta a linha a ser impressa no arquivo. Essa média envolve os valores obtidos nos cinco minutos anteriores; por exemplo, dos minutos 55, 56, 57, 58 e 59 a média será representada no minuto 00 da hora (ou dia, se for o caso) seguinte.

Atualizações: Incluída a opção de corrigir os dados conforme o fuso horário (por exemplo, em São Paulo, para passar de hora UTC para HLE, usar fuso=-3). E também segue uma alternativa à função coloca_zero(): mes=`expr $m + 0`; mes_zero=`printf “%02d” $mes` . A respeito do comando cut, usar “-f2” imprime só a segunda coluna, enquanto que “-f2-” (com um traço depois) imprime todas as colunas da segunda para frente.


#!/bin/bash
# Script para calcular médias a cada cinco minutos
# com eventuais falhas na sequência de dados
# Formato dos dados:
# 2013 10 20 08 33   16.47  93.96  927.8137    0.00    0.00     4.57

infile='dados.dat'
fuso=-3

function coloca_zero(){
    a=$1
    b=`echo "$a" | wc -L | awk '{print $1}'`
    while [ $b != 2 ]; do
        b=$((b+1))
        a=0$a
    done
    echo $a
}

function calcula_media(){
	arq=$1
	minprint2=$2
	hh=$3
	dd=$4
	colunas=14
	nlinhas=$(cat $arq | wc -l)
	for (( cont=1; cont<=$colunas; cont++ )); do
		# Escolher precisão conforme a variável
		if [ "$cont" -ge 1 ] && [ "$cont" -le 5 ]; then
			SC=0 # data/horário
		elif [ "$cont" -eq 8 ]; then
			SC=4 # pressão
		else
			SC=2
		fi
		S=`echo "("$(cut -d' ' -f"$cont" $arq | tr "\n" "+")"0)/$nlinhas"`
		R=`bc -l << fim 		scale=$SC 		$S fim` # não deixar caracter logo antes do fim 		if [ "$cont" -eq 5 ]; then 			R=$(($R+3)) 		fi 		echo $R >> temp3
	done
	# Passar tudo pra uma linha só, com campos separados por espaço
	var=`cat temp3 | sed ':a;N;s/\n/ /g;ta'`
	rm temp3
	# alterar minuto para indicar que é média dos últimos 5 minutos
	var1=`echo $var | cut -d' ' -f1`
	var2=`echo $var | cut -d' ' -f2`
	var2=`coloca_zero "$var2"`
	#var3=$dd
	#var4=$hh
	#var5=$minprint2
	var6=`echo $var | cut -d' ' -f6`
	var7=`echo $var | cut -d' ' -f7`
	var8=`echo $var | cut -d' ' -f8`
	var9=`echo $var | cut -d' ' -f9`
	var10=`echo $var | cut -d' ' -f10`
	var11=`echo $var | cut -d' ' -f11`
	var12=`echo $var | cut -d' ' -f12`
	var13=`echo $var | cut -d' ' -f13`
	var14=`echo $var | cut -d' ' -f14`
	var15=`echo $var | cut -d' ' -f15`
	# Passar de UTC para HLE (tirar 3h)
	horatemp=$(($hh+$fuso))
	if [ "$horatemp" -lt 0 ]; then
		diatemp=$(($dia-1))
		# arrumar hora
		case $horatemp in
			"-1")
			horatemp=23;
			;;
			"-2")
			horatemp=22;
			;;
			"-3")
			horatemp=21;
			;;
		esac
	else
		diatemp=$dia
	fi
	# Correção minuto/hora/dia
	if [ "$minprint2" -eq 60 ]; then
		minprint2=00
		horatemp2=$(($horatemp+1))
		hh2=`coloca_zero "$horatemp2"`
		if [ "$horatemp2" -eq 24 ]; then
			hh2=00
			dia=$(($diatemp+1))
			diatemp=`coloca_zero "$dia"`
		fi
		horatemp2=$hh2
	else
		horatemp2=$horatemp
	fi
	var3=`coloca_zero "$diatemp"`
	var4=`coloca_zero "$horatemp2"`
	var5=`coloca_zero "$minprint2"`
	echo $var1-$var2-$var3 $var4:$var5,$var6,$var7,$var8,$var9,$var10,$var11,$var12,$var13,$var14
}
ano=2014
mm=01
for dia in $(seq 8 23); do
	dd=`coloca_zero "$dia"`
	for hora in $(seq 0 23); do
		hh=`coloca_zero "$hora"`
		for minuto in $(seq 0 5 59); do
			#min=`coloca_zero "$minuto"`
			minprint=$(($minuto+5))
			minprint2=`coloca_zero "$minprint"`
			# Montar strings a serem pesquisadas dentro de 5 min
			for i in $(seq 0 4); do
				min=$(($minuto+$i))
				min2=`coloca_zero "$min"`
				# Correção minuto/hora/dia para busca
				if [ "$min" -eq 60 ]; then
					min2=00
					horatemp2=$(($hora+1))
					hh=`coloca_zero "$horatemp2"`
					if [ "$hora" -eq 24 ]; then
						hh=00
						horatemp2=0
						dia=$(($dia+1))
						dd=`coloca_zero "$dia"`
					fi
				else
					horatemp2=$hora
				fi
				#echo $horatemp2
				exp="$ano $mm $dd $hh $min2"
				echo $exp
				grep "$exp" $infile >> temp
			done
			echo fim_da_media
			size_file=`wc -c temp | cut -d' ' -f1`
			if [ $size_file -ne 0 ]; then
				# Substituir sequências de caracteres em branco repetidos por um único
				tr -s ' ' < temp > temp2
				# Calcular media e gravar em novo arquivo; apagar temporarios
				media=`calcula_media temp2 $minprint2 $horatemp2 $dd`
				#new_file=estacao_$ano-$mm-$dd.dat
				new_file=estacao_tdjunto_$ano-$mm.dat
				echo $media >> files/$new_file
				rm temp2
			fi
			rm temp
		done
	done
done 2> organiza.log

O algoritmo grava todos os dados em um mesmo arquivo. Para separar em arquivos diários, segue a rotina “separa_dias.sh”:


#!/bin/bash
# Programa para ler linhas de arquivo contendo datas e separar em arquivos diários
# Formato da linha: 2013-10-04 22:45,20.19,89.00,928.5127,0,0,3.85,8.10,145.81,145.77

function coloca_zero(){
    a=$1
    b=`echo "$a" | wc -L | awk '{print $1}'`
    while [ $b != 2 ]; do
        b=$((b+1))
        a=0$a
    done
    echo $a
}

for ano in $(seq 2013 2014); do
    for mes in $(seq 1 12); do
		for dia in $(seq 1 31); do
			mm=`coloca_zero "$mes"`
			dd=`coloca_zero "$dia"`
			exp=`echo $ano-$mm-$dd`
			grep $exp estacao_tdjunto.dat > estacao_$ano-$mm-$dd.dat
	    done
    done
done

Por último, caso precise substituir os valores de NaN (not a number) por -999 (por exemplo), utilize:

for i in `ls *.dat`
do
sed "s/NA/-999/g" $i > $i.NEW
mv $i.NEW $i
done

Segue também um outro script para fazer o inverso: criar um arquivo para cada data (se essa data existir) a partir de um arquivo com várias linhas de diferentes horários e dias.

#!/bin/bash
# Script para separar dados em arquivos diários

file_in=GERAL_2015.DAT
ano=2015
# Para cada número entre 1 e 366, realizar comandos
for dia in $(seq 1 366); do
	filename='PP_'$ano'-'$dia'.dat'
	# Contar número de linhas que contenham o texto entre aspas
	var=`grep "1,$ano,$dia," $file_in | wc -l`
	# Gravar arquivo somente se tiver linhas a serem copiadas
	if [ $var -ne 0 ]; then
		echo $filename
		grep "1,$ano,$dia," $file_in > $filename
	fi
done

Algo que facilita a vida nesse arquivo é que os dias estão em “dia do ano”.

Extra sobre o comando sort

Esse comando serve para montar uma lista a partir dos registros da 1ª coluna de um arquivo, sem repetições:

cat dados/est1.csv | awk -F',' '{print $1}' | sort | uniq -d

Pode ser útil para identificar quais são os sensores que estão em um mesmo arquivo, por exemplo. Para organizar todas as linhas e colunas com uma coluna como referência, sue o parâmetro “-k” seguido do número da coluna de referência (começando de 1, da esquerda para a direita).

Leave a Reply

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *

Esse site utiliza o Akismet para reduzir spam. Aprenda como seus dados de comentários são processados.