Escrevendo relatórios sobre arquivos de dados com data e hora; requisitos complexos
Embora a pergunta original tenha sido um pouco complicada, o contexto da pergunta tornou-a bastante difícil. Circunstâncias adicionais foram (como discutido no chat):
- O script precisava combinar vários arquivos com registro de data e hora em um relatório, possivelmente distribuídos em várias pastas com o carimbo do dia (dependendo do intervalo de tempo definido).
- O script precisava ser capaz de selecionar um intervalo de tempo, não apenas dos carimbos de hora nos nomes dos arquivos, mas também de um subintervalo lido nas linhas dos arquivos.
- As seções de tempo (trimestres) sem dados devem informar a saída "zerada"
- O tempo formato na saída, tanto no nome do relatório quanto nas linhas relatadas (por 15 minutos), era necessário em um formato diferente do de entrada.
- Linhas a serem processadas necessárias para atender a uma condição, que o script precisava verificar
- Os dados relevantes podem estar em posições diferentes na linha
- O script precisava ser
python2
- O script teve que considerar uma diferença (variável) entre a hora local e o UTC.
- Opções extras adicionadas: opção para exportar para csv básico, colunas opcionais ativadas / desativadas
- Por último, mas não menos importante: as quantidades de dados a serem processadas eram maiores que enormes ; milhares de arquivos, centenas de milhares de linhas por arquivo, muitos GB's, milhões de linhas. Em outras palavras: os procedimentos tinham que ser inteligentes e eficientes para poder processar os dados dentro de um período de tempo razoável.
Explicação
O resultado final é abrangente demais para explicar em detalhes, mas, para aqueles que estão interessados, as manchetes:
- Todos os cálculos de tempo foram feitos (não é de admirar) no tempo da época
- Lendo as linhas dos arquivos, o primeiro era verificar a condição por linha , para diminuir o número de linhas a serem processadas imediatamente
- As linhas Frome, o registro de data e hora, após converter para epoch, foram divididas por 900 (segundos, 15 minutos), arredondadas para baixo (levando
int(n)
) e subseqüentemente multiplicadas por 900 novamente para calcular a seção de 15 minutos a que pertenciam - As linhas foram classificadas e agrupadas em seguida por
itertools
'groupby
e os resultados por grupo foram gerados com a ajuda deifilter
(python2
) - Os relatórios foram criados pela primeira vez por arquivo , pois os relatórios eram por 15 minutos . A saída relatada por arquivo não poderia ser mais que dezenas de linhas. Nenhum problema possível para armazenar temporariamente na memória.
- Depois que todos os arquivos e linhas relevantes foram processados dessa forma, todos os relatórios foram finalmente combinados em um relatório final
O roteiro acaba por fazer o trabalho muito bem, apesar da quantidade de dados. Durante o processamento, o processador mostra cerca de 70% de ocupação no meu sistema com mais de 10 anos de idade, funcionando estável. O computador ainda é muito útil para outras tarefas.
O script
#!/usr/bin/env python2
import time
import datetime
from itertools import groupby, ifilter
from operator import itemgetter
import sys
import os
import math
"""
folders by day stamp: 20161211 (yyymmdd)
files by full readable (start) time 20161211093512 (yyyymmddhhmmss) + header / tail
records inside files by full start time 20161211093512 (yyyymmddhhmmss)
commands are in UTC, report name and time section inside files: + timeshift
"""
################## settings ##################
# --- format settings (don't change) ---
readable = "%Y%m%d%H%M%S"
outputformat = "%d-%m-%Y %H:%M"
dateformat = "%Y%m%d"
#---------- time settings ----------
# interval (seconds)
interval = 900
# time shift UTC <> local (hrs)
timeshift = 3.5
# start from (minutes from now in the past)
backintime = 700
# ---- dynamically set values -------
# condition (string/position)
iftrue = ["mies", 2]
# relevant data (timestamp, result)
data = [0, 1]
# datafolder
datafolder = "/home/jacob/Bureaublad/KasIII"
# ----- output columns------
# 0 = timestamp, 1 = total, 2 = SUCCESS, 3 = FAILS
# don't change the order though, distances will mess up
items = [0, 1, 2, 3]
# include simple csv file
csv = True
###############################################
start = sys.argv[1]
end = sys.argv[2]
output_path = sys.argv[3]
timeshift = timeshift*3600
def extraday():
"""
function to determine what folders possibly contain relevant files
options: today or *also* yesterday
"""
current_time = [
getattr(datetime.datetime.now(), attr) \
for attr in ['hour', 'minute']]
minutes = (current_time[0]*60)+current_time[1]
return backintime >= minutes
extraday()
def set_layout(line):
# take care of a nice output format
line = [str(s) for s in line]
dist1 = (24-len(line[0]))*" "
dist2 = (15-len(line[1]))*" "
dist3 = (15-len(line[2]))*" "
distances = [dist1, dist2, dist3, ""]
displayed = "".join([line[i]+distances[i] for i in items])
return displayed
# return line[0]+dist1+line[1]+dist2+line[2]+dist3+line[3]
def convert_toepoch(pattern, stamp):
"""
function to convert readable format (any) into epoch
"""
return int(time.mktime(time.strptime(stamp, pattern)))
def convert_toreadable(pattern, stamp, shift=0):
"""
function to convert epoch into readable (any)
possibly with a time shift
"""
return time.strftime(pattern, time.gmtime(stamp+shift))
def getrelevantfiles(backtime):
"""
get relevant files from todays subfolder, from starttime in the past
input format of backtime is minutes
"""
allrelevant = []
# current time, in epoch, to select files
currt = int(time.time())
dirs = [convert_toreadable(dateformat, currt)]
# if backintime > today's "age", add yesterday
if extraday():
dirs.append(convert_toreadable(dateformat, currt-86400))
print("Reading from: "+str(dirs))
# get relevant files from folders
for dr in dirs:
try:
relevant = [
[f, convert_toepoch(readable, f[7:21])]
for f in os.listdir(os.path.join(datafolder, dr))
]
allrelevant = allrelevant + [
os.path.join(datafolder, dr, f[0])\
for f in relevant if f[1] >= currt-(backtime*60)
]
except (IOError, OSError):
print "Folder not found:", dr
return allrelevant
def readfile(file):
"""
create the line list to work with, meeting the iftrue conditions
select the relevant lines from the file, meeting the iftrue condition
"""
lines = []
with open(file) as read:
for l in read:
l = l.split(",")
if l[iftrue[1]].strip() == iftrue[0]:
lines.append([l[data[0]], l[data[1]]])
return lines
def timeselect(lines):
"""
select lines from a list that meet the start/end time
input is the filtered list of lines, by readfile()
"""
return [l for l in lines if int(start) <= int(l[0]) < int(end)]
def convert_tosection(stamp):
"""
convert the timestamp in a line to the section (start) it belongs to
input = timestamp, output = epoch
"""
rsection = int(convert_toepoch(readable, stamp)/interval)*interval
return rsection
reportlist = []
foundfiles = getrelevantfiles(backintime)
if foundfiles:
# the actual work, first reports per file, add them to reportlist
for f in foundfiles:
# create report per file
# get lines that match condition, match the end/start
lines = timeselect(readfile(f))
# get the (time) relevant lines inside the file
for item in lines:
# convert stamp to section
item[0] = convert_tosection(item[0])
lines.sort(key=lambda x: x[0])
for item, occurrence in groupby(lines, itemgetter(0)):
occ = list(occurrence)
total = len(occ)
# ifilter is python2 specific (<> filterfalse in 3)
success = len(list(ifilter(lambda x: x[1].strip() == "00", occ)))
fails = total-success
reportlist.append([item, total, success, fails])
finalreport = []
# then group the reports per file into one
reportlist.sort(key=lambda x: x[0])
for item, occurrence in groupby(reportlist, itemgetter(0)):
occ = [it[1:] for it in list(occurrence)]
output = [str(sum(i)) for i in zip(*occ)]
output.insert(0, item)
finalreport.append(output)
# create timeframe to fill up emty sections
framestart = int(convert_toepoch(readable, start)/interval)*interval
frameend = int(math.ceil(convert_toepoch(readable, end)/interval))*interval
timerange = list(range(framestart, frameend, interval))
currlisted = [r[0] for r in finalreport]
extra = [item for item in timerange if not item in currlisted]
# add missing time sections
for item in extra:
finalreport.append([item, 0, 0, 0])
finalreport.sort(key=lambda x: x[0])
print(str(len(finalreport))+" timesections reported")
# define output file
fname1 = convert_toreadable(
readable,
convert_toepoch(readable, start),
timeshift)
fname2 = convert_toreadable(
readable,
convert_toepoch(readable, end),
timeshift)
filename = "report_"+fname1+"_"+fname2
outputfile = os.path.join(output_path, filename)
# edit the time stamp into the desired output format, add time shift
with open(outputfile, "wt") as report:
report.write(set_layout(["starttime", "total", "SUCCESS", "FAILED"])+"\n")
for item in finalreport:
item[0] = convert_toreadable(outputformat, item[0], timeshift)
report.write(set_layout(item)+"\n")
if csv:
with open(outputfile+".csv", "wt") as csv_file:
csv_file.write(",".join(["starttime", "total", "SUCCESS", "FAILED"])+"\n")
for item in finalreport:
csv_file.write(",".join(item)+"\n")
else:
print("no files to read")
Uma pequena amostra da saída
starttime total SUCCESS FAILED
12-12-2016 03:30 2029 682 1347
12-12-2016 03:45 2120 732 1388
12-12-2016 04:00 2082 745 1337
12-12-2016 04:15 2072 710 1362
12-12-2016 04:30 2004 700 1304
12-12-2016 04:45 2110 696 1414
12-12-2016 05:00 2148 706 1442
12-12-2016 05:15 2105 704 1401
12-12-2016 05:30 2040 620 1420
12-12-2016 05:45 2030 654 1376
12-12-2016 06:00 2067 692 1375
12-12-2016 06:15 2079 648 1431
12-12-2016 06:30 2030 706 1324
12-12-2016 06:45 2085 713 1372
12-12-2016 07:00 2064 726 1338
12-12-2016 07:15 2113 728 1385