import pandas as pd import json import requests import os import logging import numpy as np import webbrowser import tempfile from requests.exceptions import RequestException from datetime import datetime # --- Configuração de logging --- logging.basicConfig( level=logging.INFO, format='[%(levelname)s] %(asctime)s - %(message)s', datefmt='%Y-%m-%d %H:%M:%S' ) # --- Configurações principais --- ARQUIVO_JSON = 'df_cursos_cadastrados.json' URL = 'https://cdn3.gnarususercontent.com.br/2929-pandas/cursos_cadastrados.json' DIRETORIO_BACKUP = 'backups' def obter_e_gerenciar_dados(url, arquivo_local): """ Verifica e gerencia o arquivo JSON local, baixando-o se não existir, atualizando-o se solicitado e criando um backup da versão anterior. """ if os.path.exists(arquivo_local): logging.info(f'O arquivo "{arquivo_local}" já existe.') while True: resposta = input("Deseja atualizar o arquivo com os dados da URL? (s/n): ").strip().lower() if resposta == 's': logging.info('Atualização solicitada. Criando backup e baixando novo arquivo...') fazer_backup(arquivo_local) break elif resposta == 'n': logging.info('Mantendo o arquivo local. Lendo dados existentes...') with open(arquivo_local, 'r', encoding='utf-8') as f: return json.load(f) else: print("Resposta inválida. Por favor, digite 's' para sim ou 'n' para não.") else: logging.info(f'O arquivo "{arquivo_local}" não existe. Baixando...') try: response = requests.get(url, timeout=10) response.raise_for_status() dados = response.json() with open(arquivo_local, 'w', encoding='utf-8') as f: json.dump(dados, f, ensure_ascii=False, indent=4) logging.info(f'Arquivo "{arquivo_local}" salvo com sucesso.') return dados except (RequestException, json.JSONDecodeError, IOError) as e: logging.critical(f'Falha ao obter ou salvar o arquivo JSON. Erro: {e}') return None def fazer_backup(arquivo_local): """ Cria um diretório de backup se não existir e salva uma cópia do arquivo com timestamp, removendo backups antigos para manter apenas um. """ if not os.path.exists(DIRETORIO_BACKUP): os.makedirs(DIRETORIO_BACKUP) logging.info(f"Diretório '{DIRETORIO_BACKUP}' criado.") for arquivo_existente in os.listdir(DIRETORIO_BACKUP): os.remove(os.path.join(DIRETORIO_BACKUP, arquivo_existente)) logging.info(f"Backup antigo '{arquivo_existente}' removido.") timestamp = datetime.now().strftime('%Y-%m-%d_%H-%M-%S') nome_backup = f"{os.path.splitext(os.path.basename(arquivo_local))[0]}_{timestamp}.json" caminho_backup = os.path.join(DIRETORIO_BACKUP, nome_backup) with open(arquivo_local, 'r', encoding='utf-8') as f_orig, open(caminho_backup, 'w', encoding='utf-8') as f_backup: f_backup.write(f_orig.read()) logging.info(f'Backup do arquivo salvo como "{caminho_backup}".') def normalizar_e_processar_dados(dados_json): """ Normaliza o JSON, trata strings vazias, dados nulos, remove duplicatas, e otimiza os tipos de dados para menor consumo de memória. Também renomeia as colunas para melhor legibilidade. """ logging.info("Iniciando a normalização do JSON.") try: df = pd.json_normalize(dados_json) if 'tags' in df.columns: logging.info("Normalizando a coluna 'tags'.") df = df.explode('tags').reset_index(drop=True) df.rename(columns={'tags': 'tag'}, inplace=True) col_mapping = { 'instrutor.nome': 'nome_instrutor', 'instrutor.email': 'email_instrutor', 'instrutor.telefone': 'telefone_instrutor' } df.rename(columns=col_mapping, inplace=True) # Converte colunas numéricas e de data, marcando valores inválidos como NaN ou NaT logging.info("Convertendo e validando tipos de dados.") df['preco'] = pd.to_numeric(df['preco'], errors='coerce') df['concluintes'] = pd.to_numeric(df['concluintes'], errors='coerce', downcast='integer') df['data_inicio'] = pd.to_datetime(df['data_inicio'], errors='coerce') df['data_conclusao'] = pd.to_datetime(df['data_conclusao'], errors='coerce') # Preenche valores faltantes em colunas de objeto com 'Desconhecido' df[df.select_dtypes(include='object').columns] = df.select_dtypes(include='object').fillna('Desconhecido') logging.info("Otimizando tipos de dados para menor consumo de memória.") df_otimizado = df.copy() for col in df_otimizado.select_dtypes(include='object').columns: if df_otimizado[col].nunique() / len(df_otimizado[col]) < 0.5: df_otimizado[col] = df_otimizado[col].astype('category') for col in df_otimizado.select_dtypes(include=['int64', 'float64']).columns: df_otimizado[col] = pd.to_numeric(df_otimizado[col], downcast='integer') if df_otimizado[col].dtype == 'int64': df_otimizado[col] = pd.to_numeric(df_otimizado[col], downcast='signed') elif df_otimizado[col].dtype == 'float64': df_otimizado[col] = pd.to_numeric(df_otimizado[col], downcast='float') logging.info("Verificando e tratando duplicatas.") num_duplicatas = df_otimizado.duplicated().sum() if num_duplicatas > 0: logging.warning(f"Foram encontradas {num_duplicatas} linhas duplicadas. Removendo...") df_otimizado = df_otimizado.drop_duplicates().reset_index(drop=True) return df_otimizado except Exception as e: logging.critical(f'Erro durante o processamento do DataFrame. Erro: {e}') return None def exibir_df_em_html(df_para_exibir, titulo): """ Gera um arquivo HTML temporário com o DataFrame e o abre no navegador. """ try: # Cria uma cópia para formatação de exibição df_display = df_para_exibir.copy() # Formata datas para o padrão brasileiro date_cols = ['data_inicio', 'data_conclusao'] for col in date_cols: if col in df_display.columns: df_display[col] = pd.to_datetime(df_display[col], errors='coerce').dt.strftime('%d/%m/%Y').fillna('Desconhecido') # Formata preço para moeda Real if 'preco' in df_display.columns: df_display['preco'] = df_display['preco'].apply(lambda x: f"R$ {x:,.2f}".replace(",", "X").replace(".", ",").replace("X", ".") if pd.notnull(x) else 'Desconhecido') # Cria um arquivo HTML temporário with tempfile.NamedTemporaryFile('w', delete=False, suffix='.html', encoding='utf-8') as f: html = df_display.to_html(index=False, justify='left', classes='table table-striped') f.write(f"

{titulo}

{html}") temp_path = f.name print(f"Gerando HTML temporário em: {temp_path}") webbrowser.open_new_tab('file://' + os.path.realpath(temp_path)) print("Uma nova aba do navegador foi aberta com a amostra do DataFrame.") except Exception as e: logging.error(f"Não foi possível gerar ou abrir o HTML no navegador. Erro: {e}") print("\nExibindo no terminal como alternativa (sem formatação):") print(df_para_exibir.head()) # --- Execução principal --- dados_json = obter_e_gerenciar_dados(URL, ARQUIVO_JSON) if dados_json: df_cursos = normalizar_e_processar_dados(dados_json) if df_cursos is not None: print('\n' + '-'*50) print("### Processamento Concluído ###") try: mem_original = pd.json_normalize(dados_json).memory_usage(deep=True).sum() / 1024 mem_otimizada = df_cursos.memory_usage(deep=True).sum() / 1024 print(f"Memória original: {mem_original:.2f} KB") print(f"Memória otimizada: {mem_otimizada:.2f} KB") print(f"Redução de memória: {((mem_original - mem_otimizada) / mem_original) * 100:.2f}%") except Exception: logging.warning("Não foi possível calcular a memória original para comparação.") print("\n" + '-'*50) print(f"DataFrame Final com {df_cursos.shape[0]} linhas e {df_cursos.shape[1]} colunas.") print("\n### Colunas e Tipos de Dados ###") df_cursos.info() print("\n" + '-'*50) print("\n### Exibição do DataFrame ###") df_para_exibir = df_cursos while True: resposta_exibicao = input("Deseja exibir [1] dados completos ou [2] apenas dados válidos? (1/2): ").strip() if resposta_exibicao == '1': print("Exibindo amostra do DataFrame completo...") break elif resposta_exibicao == '2': print("Exibindo amostra do DataFrame com dados válidos. Linhas com valores nulos em 'concluintes', 'preco', 'data_inicio' ou 'data_conclusao' serão removidas.") df_para_exibir = df_cursos.dropna(subset=['concluintes', 'preco', 'data_inicio', 'data_conclusao']).reset_index(drop=True) print(f"DataFrame validado agora tem {len(df_para_exibir)} linhas.") break else: print("Opção inválida. Por favor, digite '1' ou '2'.") exibir_df_em_html(df_para_exibir.head(), "Amostra do DataFrame Final") print('\n' + '-'*50)