V1.0
This commit is contained in:
328
softwares/test/meteorologia_aeroportos/README.md
Normal file
328
softwares/test/meteorologia_aeroportos/README.md
Normal file
@@ -0,0 +1,328 @@
|
|||||||
|
# Dataset IED — Meteorologia e Transporte Aéreo Brasileiro
|
||||||
|
|
||||||
|
Plataforma de coleta, processamento e visualização de dados meteorológicos e de transporte aéreo brasileiro, com foco em aeroportos operados pelo sistema DECEA/ICEA.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Sumário
|
||||||
|
|
||||||
|
- [Pré-requisitos](#pré-requisitos)
|
||||||
|
- [Instalação](#instalação)
|
||||||
|
- [Estrutura do Projeto](#estrutura-do-projeto)
|
||||||
|
- [Módulos e Uso](#módulos-e-uso)
|
||||||
|
- [Scraper de Meteorologia](#1-scraper-de-meteorologia)
|
||||||
|
- [Pipeline Completo](#2-pipeline-completo)
|
||||||
|
- [Concatenação de CSVs](#3-concatenação-de-csvs)
|
||||||
|
- [Dashboard Interativo](#4-dashboard-interativo)
|
||||||
|
- [Diagnóstico de Outliers](#5-diagnóstico-de-outliers)
|
||||||
|
- [Banco de Dados](#banco-de-dados)
|
||||||
|
- [Categorias ICAO](#categorias-icao)
|
||||||
|
- [Solução de Problemas](#solução-de-problemas)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Pré-requisitos
|
||||||
|
|
||||||
|
| Requisito | Versão mínima | Observação |
|
||||||
|
|-----------|--------------|------------|
|
||||||
|
| Python | 3.10+ | [python.org](https://www.python.org/downloads/) |
|
||||||
|
| Google Chrome | Qualquer versão recente | Necessário para o Selenium (scraper) |
|
||||||
|
| Git | Qualquer | Para clonar o repositório |
|
||||||
|
|
||||||
|
> O `webdriver-manager` baixa automaticamente o ChromeDriver compatível com a versão do Chrome instalada. Não é necessário instalar o ChromeDriver manualmente.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Instalação
|
||||||
|
|
||||||
|
### 1. Clonar o repositório
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone ssh://git@git.ppgao.ita.br:2222/reboucassr/dataset.git
|
||||||
|
cd dataset
|
||||||
|
```
|
||||||
|
|
||||||
|
> Caso não tenha acesso SSH configurado, solicite ao administrador a chave de acesso ou use HTTPS se disponível.
|
||||||
|
|
||||||
|
### 2. Criar e ativar o ambiente virtual
|
||||||
|
|
||||||
|
Navegue até o diretório do módulo de meteorologia:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd softwares/test
|
||||||
|
```
|
||||||
|
|
||||||
|
**Windows (PowerShell):**
|
||||||
|
```powershell
|
||||||
|
python -m venv .venv # Instala o ambiente virtual no Windows
|
||||||
|
.\.venv\Scripts\Activate.ps1 # Ativa o ambiente virtual
|
||||||
|
```
|
||||||
|
|
||||||
|
**Linux / macOS:**
|
||||||
|
```bash
|
||||||
|
python3 -m venv .venv
|
||||||
|
source .venv/bin/activate # Para Linux e macOS
|
||||||
|
```
|
||||||
|
|
||||||
|
> No Windows, se receber erro de política de execução, execute antes:
|
||||||
|
> `Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser`
|
||||||
|
|
||||||
|
### 3. Instalar dependências
|
||||||
|
|
||||||
|
Com o ambiente virtual ativado:
|
||||||
|
|
||||||
|
```PowerShell
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
Principais pacotes instalados:
|
||||||
|
|
||||||
|
| Pacote | Finalidade |
|
||||||
|
|--------|-----------|
|
||||||
|
| `selenium` | Automação do navegador para scraping |
|
||||||
|
| `webdriver-manager` | Gerencia o ChromeDriver automaticamente |
|
||||||
|
| `beautifulsoup4` + `lxml` | Parsing de HTML |
|
||||||
|
| `pandas` | Processamento e análise de dados |
|
||||||
|
| `streamlit` | Dashboard interativo |
|
||||||
|
| `plotly` | Gráficos interativos |
|
||||||
|
|
||||||
|
---
|
||||||
|
### 4. Executar o projeto
|
||||||
|
|
||||||
|
Com o ambiente virtual ativado:
|
||||||
|
|
||||||
|
```PowerShell
|
||||||
|
cd meteorologia_aeroportos
|
||||||
|
streamlit run dashboard.py
|
||||||
|
```
|
||||||
|
|
||||||
|
> O dashboard será executado na porta 8501.
|
||||||
|
|
||||||
|
|
||||||
|
## Estrutura do Projeto
|
||||||
|
|
||||||
|
```
|
||||||
|
dataset/
|
||||||
|
├── softwares/
|
||||||
|
│ └── test/
|
||||||
|
│ └── meteorologia_aeroportos/
|
||||||
|
│ ├── dados/ # Banco SQLite + CSVs por aeródromo
|
||||||
|
│ │ ├── met.db # Banco de dados principal
|
||||||
|
│ │ ├── SBGR/ # CSVs do aeroporto de Guarulhos
|
||||||
|
│ │ └── SBAN/ # CSVs de outros aeródromos
|
||||||
|
│ ├── scraper_meteorologia.py # Coleta dados do ICEA/DECEA
|
||||||
|
│ ├── pipeline.py # Orquestrador completo
|
||||||
|
│ ├── concat_meteorologia.py # Mescla arquivos CSV
|
||||||
|
│ ├── db.py # Lógica do banco SQLite
|
||||||
|
│ ├── dashboard.py # Dashboard Streamlit
|
||||||
|
│ ├── _diag_outliers.py # Diagnóstico de qualidade
|
||||||
|
│ └── requirements.txt
|
||||||
|
├── tabelas/
|
||||||
|
│ ├── raw/ # Dados brutos ANAC
|
||||||
|
│ ├── preproc/ # Dados intermediários
|
||||||
|
│ └── proc/ # Dados processados finais
|
||||||
|
└── textos/
|
||||||
|
├── artigos/ # Artigos acadêmicos
|
||||||
|
├── livros/
|
||||||
|
└── relatorios/ # Relatórios ANAC e setor
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Módulos e Uso
|
||||||
|
|
||||||
|
> Todos os comandos abaixo devem ser executados a partir do diretório `softwares/test/meteorologia_aeroportos/`, com o ambiente virtual ativado.
|
||||||
|
|
||||||
|
### 1. Scraper de Meteorologia
|
||||||
|
|
||||||
|
Coleta dados de superfície horários do portal ICEA/DECEA para um dado aeródromo.
|
||||||
|
|
||||||
|
**Coletar todos os anos disponíveis:**
|
||||||
|
```bash
|
||||||
|
python scraper_meteorologia.py --aerodrome SBGR --all-years
|
||||||
|
```
|
||||||
|
|
||||||
|
**Coletar um intervalo de anos específico:**
|
||||||
|
```bash
|
||||||
|
python scraper_meteorologia.py --aerodrome SBGR --start-year 2020 --end-year 2025
|
||||||
|
```
|
||||||
|
|
||||||
|
**Executar com navegador visível (sem modo headless — útil para depuração):**
|
||||||
|
```bash
|
||||||
|
python scraper_meteorologia.py --aerodrome SBGR --start-year 2023 --end-year 2023 --no-headless
|
||||||
|
```
|
||||||
|
|
||||||
|
**Parâmetros disponíveis:**
|
||||||
|
|
||||||
|
| Parâmetro | Tipo | Descrição |
|
||||||
|
|-----------|------|-----------|
|
||||||
|
| `--aerodrome` | string | Código ICAO do aeródromo (ex: `SBGR`, `SBSP`, `SBAN`) |
|
||||||
|
| `--all-years` | flag | Coleta desde o início do histórico disponível |
|
||||||
|
| `--start-year` | int | Ano inicial do intervalo |
|
||||||
|
| `--end-year` | int | Ano final do intervalo |
|
||||||
|
| `--no-headless` | flag | Abre o Chrome de forma visível |
|
||||||
|
|
||||||
|
Os arquivos CSV são salvos em `dados/{CÓDIGO_ICAO}/`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. Pipeline Completo
|
||||||
|
|
||||||
|
Orquestra todo o fluxo: verifica cobertura existente, executa scraping incremental, valida dados com limites físicos, calcula variáveis analíticas e persiste no banco SQLite.
|
||||||
|
|
||||||
|
**Processar todos os anos para um aeródromo:**
|
||||||
|
```bash
|
||||||
|
python pipeline.py --aerodrome SBGR --all-years
|
||||||
|
```
|
||||||
|
|
||||||
|
**Processar intervalo específico:**
|
||||||
|
```bash
|
||||||
|
python pipeline.py --aerodrome SBGR --start-year 2020 --end-year 2025
|
||||||
|
```
|
||||||
|
|
||||||
|
**Manter arquivos temporários após o processamento:**
|
||||||
|
```bash
|
||||||
|
python pipeline.py --aerodrome SBGR --all-years --no-cleanup
|
||||||
|
```
|
||||||
|
|
||||||
|
**Parâmetros disponíveis:**
|
||||||
|
|
||||||
|
| Parâmetro | Tipo | Descrição |
|
||||||
|
|-----------|------|-----------|
|
||||||
|
| `--aerodrome` | string | Código ICAO do aeródromo |
|
||||||
|
| `--all-years` | flag | Processa todo o histórico disponível |
|
||||||
|
| `--start-year` | int | Ano inicial |
|
||||||
|
| `--end-year` | int | Ano final |
|
||||||
|
| `--no-cleanup` | flag | Mantém CSVs intermediários após upsert |
|
||||||
|
|
||||||
|
> **Recomendado para uso normal.** O pipeline é idempotente: pode ser executado múltiplas vezes sem duplicar dados.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. Concatenação de CSVs
|
||||||
|
|
||||||
|
Mescla os arquivos CSV trimestrais baixados pelo scraper em uma tabela única por aeródromo.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python concat_meteorologia.py --dados-dir dados --validate --cleanup
|
||||||
|
```
|
||||||
|
|
||||||
|
**Parâmetros:**
|
||||||
|
|
||||||
|
| Parâmetro | Tipo | Descrição |
|
||||||
|
|-----------|------|-----------|
|
||||||
|
| `--dados-dir` | string | Diretório raiz dos CSVs (padrão: `dados`) |
|
||||||
|
| `--validate` | flag | Aplica validação por limites físicos |
|
||||||
|
| `--cleanup` | flag | Remove arquivos temporários após concatenação |
|
||||||
|
|
||||||
|
> Este passo é executado automaticamente pelo `pipeline.py`. Use-o de forma isolada apenas se precisar reprocessar os CSVs sem coletar novos dados.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4. Dashboard Interativo
|
||||||
|
|
||||||
|
Interface web para análise e visualização das séries temporais meteorológicas.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
streamlit run dashboard.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Acesse no navegador: **http://localhost:8501**
|
||||||
|
|
||||||
|
**Funcionalidades do dashboard:**
|
||||||
|
|
||||||
|
- Seleção de aeródromo e variável meteorológica
|
||||||
|
- Reamostagem temporal: Horária, Diária, Semanal, Mensal
|
||||||
|
- Gráficos de série temporal interativos (Plotly)
|
||||||
|
- Rosa dos ventos
|
||||||
|
- Análise de teto e visibilidade com classificação ICAO
|
||||||
|
- Distribuição estatística por variável
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5. Diagnóstico de Outliers
|
||||||
|
|
||||||
|
Analisa a qualidade dos dados no banco, exibindo estatísticas sobre valores tratados por limites físicos.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python _diag_outliers.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Exibe contagens e proporções de outliers registrados na tabela `outlier_log` por variável meteorológica.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Banco de Dados
|
||||||
|
|
||||||
|
O banco SQLite (`dados/met.db`) possui três tabelas:
|
||||||
|
|
||||||
|
| Tabela | Descrição |
|
||||||
|
|--------|-----------|
|
||||||
|
| `observations` | Série temporal horária com 10 variáveis meteorológicas |
|
||||||
|
| `outlier_log` | Registro de valores anômalos tratados (auditoria) |
|
||||||
|
| `aerodromes` | Catálogo de aeródromos disponíveis |
|
||||||
|
|
||||||
|
**Variáveis na tabela `observations`:**
|
||||||
|
|
||||||
|
| Coluna | Descrição | Unidade |
|
||||||
|
|--------|-----------|---------|
|
||||||
|
| `T` | Temperatura | °C |
|
||||||
|
| `Td` | Ponto de orvalho | °C |
|
||||||
|
| `UR` | Umidade relativa | % |
|
||||||
|
| `QNH` | Pressão atmosférica | hPa |
|
||||||
|
| `WS` | Velocidade do vento | kt |
|
||||||
|
| `WG` | Rajada de vento | kt |
|
||||||
|
| `WD` | Direção do vento | graus |
|
||||||
|
| `VIS` | Visibilidade | dam |
|
||||||
|
| `TETO` | Teto de nuvens | dam |
|
||||||
|
| `PREC` | Precipitação acumulada | mm |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Categorias ICAO
|
||||||
|
|
||||||
|
O dashboard classifica as condições meteorológicas segundo os critérios ICAO:
|
||||||
|
|
||||||
|
| Categoria | Teto (dam) | Visibilidade (dam) | Condição |
|
||||||
|
|-----------|------------|-------------------|----------|
|
||||||
|
| **LIFR** | < 31 | < 16 | IFR Severo |
|
||||||
|
| **IFR** | 31–91 | 16–48 | Voo por instrumentos |
|
||||||
|
| **MVFR** | 91–300 | 48–800 | VFR Marginal |
|
||||||
|
| **VMC** | > 300 | > 800 | Condições visuais |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Solução de Problemas
|
||||||
|
|
||||||
|
**Erro de ChromeDriver / Selenium:**
|
||||||
|
- Verifique se o Google Chrome está instalado na máquina.
|
||||||
|
- O `webdriver-manager` tentará baixar o ChromeDriver automaticamente; verifique se há acesso à internet.
|
||||||
|
- Em ambientes corporativos com proxy, pode ser necessário configurar `HTTP_PROXY` / `HTTPS_PROXY`.
|
||||||
|
|
||||||
|
**Erro ao ativar o ambiente virtual no Windows:**
|
||||||
|
```powershell
|
||||||
|
Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser
|
||||||
|
```
|
||||||
|
|
||||||
|
**Dashboard não abre / porta ocupada:**
|
||||||
|
```bash
|
||||||
|
streamlit run dashboard.py --server.port 8502
|
||||||
|
```
|
||||||
|
|
||||||
|
**Banco de dados não encontrado:**
|
||||||
|
- Execute o `pipeline.py` ao menos uma vez para criar e popular o banco `dados/met.db`.
|
||||||
|
|
||||||
|
**Dados ausentes para um aeródromo:**
|
||||||
|
- Verifique se o código ICAO está correto e disponível no portal ICEA: https://pesquisa.icea.decea.mil.br/superficie_list/
|
||||||
|
- Nem todos os aeródromos possuem histórico completo desde 1947.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Fonte dos Dados
|
||||||
|
|
||||||
|
- **Meteorologia:** Portal ICEA/DECEA — https://pesquisa.icea.decea.mil.br/superficie_list/
|
||||||
|
- **Transporte Aéreo:** ANAC — Relatório Anual 2024 e séries históricas de demanda e oferta
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Projeto de pesquisa acadêmica — IED / ITA*
|
||||||
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
@@ -156,6 +156,50 @@ def list_aerodromes() -> list[str]:
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def list_catalog() -> list[dict]:
|
||||||
|
"""Retorna catálogo completo da tabela aerodromes. Fallback: só aeros com dados."""
|
||||||
|
if _db_available():
|
||||||
|
import db as _db
|
||||||
|
conn = _db.get_connection(DB_PATH)
|
||||||
|
_db.ensure_schema(conn)
|
||||||
|
catalog = _db.list_all_aerodromes(conn)
|
||||||
|
conn.close()
|
||||||
|
if catalog:
|
||||||
|
return catalog
|
||||||
|
return [{"icao": a, "nome": "", "uf": ""} for a in list_aerodromes()]
|
||||||
|
|
||||||
|
|
||||||
|
def _fmt_aero(d: dict) -> str:
|
||||||
|
"""Formata dict de aeródromo para exibição no selectbox."""
|
||||||
|
nome = d.get("nome", "")
|
||||||
|
uf = d.get("uf", "")
|
||||||
|
if nome and uf:
|
||||||
|
return f"{d['icao']} — {nome} ({uf})"
|
||||||
|
if nome:
|
||||||
|
return f"{d['icao']} — {nome}"
|
||||||
|
return d["icao"]
|
||||||
|
|
||||||
|
|
||||||
|
def _save_csv_to_folder(df: pd.DataFrame, aerodrome: str,
|
||||||
|
start: str, end: str) -> str | None:
|
||||||
|
"""Abre diálogo nativo de pasta e salva o DataFrame como CSV. Retorna caminho ou None."""
|
||||||
|
import tkinter as tk
|
||||||
|
from tkinter import filedialog
|
||||||
|
root = tk.Tk()
|
||||||
|
root.withdraw()
|
||||||
|
root.wm_attributes("-topmost", True)
|
||||||
|
folder = filedialog.askdirectory(title="Escolha a pasta de destino")
|
||||||
|
root.destroy()
|
||||||
|
if not folder:
|
||||||
|
return None
|
||||||
|
s = start.replace("-", "")
|
||||||
|
e = end.replace("-", "")
|
||||||
|
filename = f"{aerodrome}_{s}_{e}.csv"
|
||||||
|
filepath = Path(folder) / filename
|
||||||
|
df.to_csv(filepath, index=False, encoding="utf-8-sig")
|
||||||
|
return str(filepath)
|
||||||
|
|
||||||
|
|
||||||
def get_period(aerodrome: str) -> tuple[str, str]:
|
def get_period(aerodrome: str) -> tuple[str, str]:
|
||||||
"""Retorna (inicio, fim) dos dados disponíveis para o aeródromo."""
|
"""Retorna (inicio, fim) dos dados disponíveis para o aeródromo."""
|
||||||
if _db_available():
|
if _db_available():
|
||||||
@@ -649,7 +693,7 @@ def chart_climatology(anl: pd.DataFrame, var: str, title: str,
|
|||||||
# ══════════════════════════════════════════════════════════════════════════════
|
# ══════════════════════════════════════════════════════════════════════════════
|
||||||
# SIDEBAR
|
# SIDEBAR
|
||||||
# ══════════════════════════════════════════════════════════════════════════════
|
# ══════════════════════════════════════════════════════════════════════════════
|
||||||
aerodromes = list_aerodromes()
|
catalog = list_catalog()
|
||||||
|
|
||||||
with st.sidebar:
|
with st.sidebar:
|
||||||
st.markdown("## ✈️ MET Aeroportuário")
|
st.markdown("## ✈️ MET Aeroportuário")
|
||||||
@@ -657,11 +701,12 @@ with st.sidebar:
|
|||||||
st.caption(fonte)
|
st.caption(fonte)
|
||||||
st.markdown("---")
|
st.markdown("---")
|
||||||
|
|
||||||
if not aerodromes:
|
if not catalog:
|
||||||
st.warning("Nenhum dado encontrado.\nExecute a coleta primeiro.")
|
st.warning("Nenhum dado encontrado.\nExecute a coleta primeiro.")
|
||||||
st.stop()
|
st.stop()
|
||||||
|
|
||||||
sel_aero = st.selectbox("Aeródromo", aerodromes)
|
sel_dict = st.selectbox("Aeródromo", catalog, format_func=_fmt_aero)
|
||||||
|
sel_aero = sel_dict["icao"]
|
||||||
p_start, p_end = get_period(sel_aero)
|
p_start, p_end = get_period(sel_aero)
|
||||||
st.caption(f"📅 Disponível: `{p_start}` → `{p_end}`")
|
st.caption(f"📅 Disponível: `{p_start}` → `{p_end}`")
|
||||||
st.markdown("---")
|
st.markdown("---")
|
||||||
@@ -781,12 +826,42 @@ with tab0:
|
|||||||
st.markdown("### Aquisição de Dados — ICEA/DECEA")
|
st.markdown("### Aquisição de Dados — ICEA/DECEA")
|
||||||
st.markdown("Baixa dados de superfície do site da ICEA e armazena no banco SQLite local.")
|
st.markdown("Baixa dados de superfície do site da ICEA e armazena no banco SQLite local.")
|
||||||
|
|
||||||
|
_btn_col, _info_col = st.columns([1, 3])
|
||||||
|
with _btn_col:
|
||||||
|
if st.button("🔄 Atualizar catálogo ICEA",
|
||||||
|
help="Acessa o site ICEA e baixa a lista completa de aeródromos disponíveis"):
|
||||||
|
from scraper_meteorologia import fetch_aerodrome_catalog
|
||||||
|
import db as _db
|
||||||
|
with st.spinner("Acessando site ICEA…"):
|
||||||
|
_new_catalog = fetch_aerodrome_catalog(headless=True)
|
||||||
|
_conn = _db.get_connection(DB_PATH)
|
||||||
|
_db.ensure_schema(_conn)
|
||||||
|
_n = _db.upsert_aerodromes(_conn, _new_catalog)
|
||||||
|
_conn.close()
|
||||||
|
st.success(f"{_n} aeródromos gravados no catálogo.")
|
||||||
|
st.rerun()
|
||||||
|
with _info_col:
|
||||||
|
_n_cat = len(catalog)
|
||||||
|
st.caption(
|
||||||
|
f"Catálogo atual: **{_n_cat}** aeródromos. "
|
||||||
|
"Use o botão ao lado para atualizar do site ICEA."
|
||||||
|
if _n_cat else
|
||||||
|
"Catálogo vazio — clique em **Atualizar catálogo ICEA** para baixar a lista do site."
|
||||||
|
)
|
||||||
|
|
||||||
col_form, col_log = st.columns([1, 1.6])
|
col_form, col_log = st.columns([1, 1.6])
|
||||||
|
|
||||||
with col_form:
|
with col_form:
|
||||||
with st.form("form_coleta"):
|
with st.form("form_coleta"):
|
||||||
aero_input = st.text_input("Aeródromo (ICAO)", value="SBGR",
|
_coleta_catalog = catalog if catalog else [{"icao": "SBGR", "nome": "", "uf": ""}]
|
||||||
placeholder="Ex: SBGR, SBSP, SBGL").upper().strip()
|
_default_idx = next(
|
||||||
|
(i for i, d in enumerate(_coleta_catalog) if d["icao"] == sel_aero), 0
|
||||||
|
)
|
||||||
|
_sel_coleta = st.selectbox(
|
||||||
|
"Aeródromo (ICAO)", _coleta_catalog,
|
||||||
|
index=_default_idx, format_func=_fmt_aero,
|
||||||
|
)
|
||||||
|
aero_input = _sel_coleta["icao"]
|
||||||
|
|
||||||
modo = st.radio("Modo de coleta", [
|
modo = st.radio("Modo de coleta", [
|
||||||
"Todos os períodos (histórico completo)",
|
"Todos os períodos (histórico completo)",
|
||||||
@@ -822,9 +897,6 @@ with tab0:
|
|||||||
res_box = st.empty()
|
res_box = st.empty()
|
||||||
|
|
||||||
if submitted:
|
if submitted:
|
||||||
if not aero_input:
|
|
||||||
st.error("Informe o código ICAO.")
|
|
||||||
else:
|
|
||||||
all_years = (modo == "Todos os períodos (histórico completo)")
|
all_years = (modo == "Todos os períodos (histórico completo)")
|
||||||
update_only= (modo == "Atualizar (apenas dados novos)")
|
update_only= (modo == "Atualizar (apenas dados novos)")
|
||||||
|
|
||||||
@@ -1029,6 +1101,39 @@ with tab7:
|
|||||||
|
|
||||||
st.dataframe(show_df, use_container_width=True, height=480)
|
st.dataframe(show_df, use_container_width=True, height=480)
|
||||||
st.caption(f"{len(show_df):,} observações · {len(sel)} variáveis selecionadas")
|
st.caption(f"{len(show_df):,} observações · {len(sel)} variáveis selecionadas")
|
||||||
|
|
||||||
|
_exp_left, _exp_right = st.columns([1, 1])
|
||||||
|
|
||||||
|
with _exp_left:
|
||||||
csv_bytes = show_df.to_csv(index=False, encoding="utf-8-sig").encode("utf-8-sig")
|
csv_bytes = show_df.to_csv(index=False, encoding="utf-8-sig").encode("utf-8-sig")
|
||||||
st.download_button("⬇️ Baixar CSV filtrado", csv_bytes,
|
st.download_button(
|
||||||
f"{sel_aero}_export.csv", "text/csv")
|
"⬇️ Baixar CSV (variáveis selecionadas)",
|
||||||
|
csv_bytes,
|
||||||
|
f"{sel_aero}_export.csv",
|
||||||
|
"text/csv",
|
||||||
|
use_container_width=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
with _exp_right:
|
||||||
|
if st.button("📁 Salvar CSV completo em pasta…",
|
||||||
|
use_container_width=True,
|
||||||
|
help="Abre um seletor de pasta e salva todas as variáveis meteorológicas"):
|
||||||
|
# monta DataFrame completo com todos as variáveis disponíveis
|
||||||
|
_full_labels = {
|
||||||
|
"T": "Temperatura Bulbo Seco (°C)", "Td": "Ponto de Orvalho (°C)",
|
||||||
|
"UR": "Umidade Relativa (%)", "QNH": "Pressão QNH (hPa)",
|
||||||
|
"WS": "Velocidade do Vento (kt)", "WG": "Rajada Máxima (kt)",
|
||||||
|
"WD": "Direção do Vento (°)", "VIS": "Visibilidade Predominante (dam)",
|
||||||
|
"TETO": "Teto (dam)", "PREC": "Precipitação (mm)",
|
||||||
|
}
|
||||||
|
_all_cols = [c for c in _full_labels if c in anl.columns]
|
||||||
|
_export_df = anl[["_dt"] + _all_cols].copy()
|
||||||
|
_export_df["_dt"] = _export_df["_dt"].dt.strftime("%Y-%m-%d %H:%M")
|
||||||
|
_export_df = _export_df.rename(
|
||||||
|
columns={"_dt": "Data/Hora", **{c: _full_labels[c] for c in _all_cols}}
|
||||||
|
)
|
||||||
|
_path = _save_csv_to_folder(_export_df, sel_aero, sel_start, sel_end)
|
||||||
|
if _path:
|
||||||
|
st.success(f"Arquivo salvo em:\n`{_path}`")
|
||||||
|
else:
|
||||||
|
st.info("Exportação cancelada.")
|
||||||
|
|||||||
@@ -6,9 +6,10 @@ Uso standalone:
|
|||||||
python scraper_meteorologia.py --aerodrome SBGR --all-years
|
python scraper_meteorologia.py --aerodrome SBGR --all-years
|
||||||
python scraper_meteorologia.py --aerodrome SBGR --start-year 2020 --end-year 2025
|
python scraper_meteorologia.py --aerodrome SBGR --start-year 2020 --end-year 2025
|
||||||
python scraper_meteorologia.py --aerodrome SBSP --start-year 2023 --end-year 2023 --no-headless
|
python scraper_meteorologia.py --aerodrome SBSP --start-year 2023 --end-year 2023 --no-headless
|
||||||
|
python scraper_meteorologia.py --fetch-catalog # grava catálogo de aeródromos no banco
|
||||||
|
|
||||||
Importável pelo pipeline:
|
Importável pelo pipeline:
|
||||||
from scraper_meteorologia import make_driver, scrape_year, SITE_MIN_DATE
|
from scraper_meteorologia import make_driver, scrape_year, fetch_aerodrome_catalog, SITE_MIN_DATE
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
@@ -152,6 +153,57 @@ def select_aerodrome(driver: webdriver.Chrome, wait: WebDriverWait, code: str) -
|
|||||||
return text
|
return text
|
||||||
|
|
||||||
|
|
||||||
|
def fetch_aerodrome_catalog(headless: bool = True) -> list[dict]:
|
||||||
|
"""
|
||||||
|
Acessa o site ICEA e extrai a lista completa de aeródromos disponíveis.
|
||||||
|
Retorna lista de dicts com chaves 'icao', 'nome', 'uf'.
|
||||||
|
"""
|
||||||
|
driver = make_driver(headless=headless)
|
||||||
|
try:
|
||||||
|
driver.get(BASE_URL)
|
||||||
|
WebDriverWait(driver, 30).until(
|
||||||
|
EC.presence_of_element_located(
|
||||||
|
(By.CSS_SELECTOR, "select.selectpicker, select")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
time.sleep(1.5) # aguarda Bootstrap Select renderizar todas as opções
|
||||||
|
raw_options = driver.execute_script("""
|
||||||
|
var candidates = document.querySelectorAll(
|
||||||
|
'select.selectpicker, select[name*="localidade"], select[name*="aerodrome"], select'
|
||||||
|
);
|
||||||
|
var result = [];
|
||||||
|
for (var s = 0; s < candidates.length; s++) {
|
||||||
|
var sel = candidates[s];
|
||||||
|
if (sel.options.length > 5) {
|
||||||
|
for (var i = 0; i < sel.options.length; i++) {
|
||||||
|
var txt = sel.options[i].text.trim();
|
||||||
|
if (txt) result.push(txt);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
""")
|
||||||
|
finally:
|
||||||
|
driver.quit()
|
||||||
|
|
||||||
|
catalog = []
|
||||||
|
for text in (raw_options or []):
|
||||||
|
if not text or len(text) < 4:
|
||||||
|
continue
|
||||||
|
icao = text[:4].upper()
|
||||||
|
if not re.match(r"[A-Z]{4}", icao):
|
||||||
|
continue
|
||||||
|
# formato típico: "SBGR - GUARULHOS / CUMBICA (SP)"
|
||||||
|
nome = text[4:].lstrip(" -–—").strip()
|
||||||
|
uf_match = re.search(r"\(([A-Z]{2})\)\s*$", nome)
|
||||||
|
uf = uf_match.group(1) if uf_match else ""
|
||||||
|
if uf_match:
|
||||||
|
nome = nome[: uf_match.start()].strip().rstrip("(),- ")
|
||||||
|
catalog.append({"icao": icao, "nome": nome, "uf": uf})
|
||||||
|
return catalog
|
||||||
|
|
||||||
|
|
||||||
def set_period(driver: webdriver.Chrome, start: date, end: date) -> None:
|
def set_period(driver: webdriver.Chrome, start: date, end: date) -> None:
|
||||||
s = start.strftime("%d/%m/%Y")
|
s = start.strftime("%d/%m/%Y")
|
||||||
e = end.strftime("%d/%m/%Y")
|
e = end.strftime("%d/%m/%Y")
|
||||||
@@ -406,8 +458,28 @@ exemplos:
|
|||||||
help="Diretório de saída (padrão: dados/)")
|
help="Diretório de saída (padrão: dados/)")
|
||||||
parser.add_argument("--no-headless", action="store_true",
|
parser.add_argument("--no-headless", action="store_true",
|
||||||
help="Abre janela do Chrome visível (útil para debug)")
|
help="Abre janela do Chrome visível (útil para debug)")
|
||||||
|
parser.add_argument("--fetch-catalog", action="store_true",
|
||||||
|
help="Baixa lista de aeródromos do site ICEA e grava no banco SQLite")
|
||||||
|
parser.add_argument("--db-path", default="dados/met.db", metavar="PATH",
|
||||||
|
help="Caminho do banco SQLite (usado com --fetch-catalog)")
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
# ── fetch-catalog: ação independente ──────────────────────────────────────
|
||||||
|
if args.fetch_catalog:
|
||||||
|
from pathlib import Path
|
||||||
|
import db as _db
|
||||||
|
print("Acessando site ICEA para obter lista de aeródromos…")
|
||||||
|
catalog = fetch_aerodrome_catalog(headless=not args.no_headless)
|
||||||
|
print(f" {len(catalog)} aeródromos encontrados.")
|
||||||
|
db_path = Path(args.db_path)
|
||||||
|
db_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
conn = _db.get_connection(db_path)
|
||||||
|
_db.ensure_schema(conn)
|
||||||
|
n = _db.upsert_aerodromes(conn, catalog)
|
||||||
|
conn.close()
|
||||||
|
print(f" {n} registros gravados em '{db_path}'.")
|
||||||
|
return
|
||||||
|
|
||||||
if not args.all_years and (args.start_year is None or args.end_year is None):
|
if not args.all_years and (args.start_year is None or args.end_year is None):
|
||||||
parser.error("Use --all-years ou informe --start-year e --end-year")
|
parser.error("Use --all-years ou informe --start-year e --end-year")
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user