BDMEP Downloader
Projeto: Automação de download de dados climáticos do INMET/BDMEP
Contexto: Doutorado UnB — Tópicos Avançados em Modelagem Ambiental (TAMA)
Acesso online: bdmep.ruiogawa.net
Repositório: github.com/ruiogawa/bdmep-downloader
Última atualização: 2026-05-23
!!! warning "Ferramenta não oficial" Este projeto não tem vínculo com o INMET. Os dados são fornecidos pelo INMET via BDMEP. Não me responsabilizo pela integridade ou disponibilidade dos dados obtidos por esta ferramenta.
Motivação
O BDMEP (Banco de Dados Meteorológicos para Ensino e Pesquisa) do INMET exige um fluxo manual burocrático para baixar dados:
- Preencher formulário no site
- Aguardar e-mail de confirmação
- Clicar no link do e-mail
- Aguardar processamento
- Baixar o arquivo ZIP
Esta ferramenta automatiza todas essas etapas, entregando o arquivo diretamente ao usuário sem nenhuma interação manual.
Funcionalidades
- Seleção de estações convencionais (M) ou automáticas (T)
- Filtro de variáveis disponíveis por tipo de estação (recarrega ao mudar o tipo)
- Seleção de período (data inicial e final)
- Download automático do arquivo ZIP com os dados
- Log em tempo real do processo de automação
- Fallback via "Confirmar Pendentes" para requisições submetidas diretamente no site
Como usar
- Selecione o tipo de estação (Convencional ou Automática)
- Escolha a estação pelo código ou nome
- Marque as variáveis desejadas
- Defina o período (data inicial e final)
- Selecione o formato de saída (separador decimal)
- Clique em Baixar Dados e aguarde o download automático
O log exibido na tela mostra cada etapa do processo em tempo real. O download começa automaticamente assim que o arquivo estiver pronto.
Acesso online
Acesse diretamente pelo navegador, sem instalar nada:
Executar localmente no seu computador
Prefere rodar no próprio computador? Basta ter Python instalado — não é necessário nenhuma configuração de servidor ou conta em nuvem.
Após iniciar, o app fica disponível em http://localhost:5000 no seu navegador. Ele roda localmente e não envia nenhum dado para servidores externos além do próprio INMET.
Pré-requisitos
- Python 3.10 ou superior
- pip (incluído com o Python)
- Acesso à internet (o app interage com o site do INMET)
Linux e macOS
# 1. Clone o repositório
git clone https://github.com/ruiogawa/bdmep-downloader.git
cd bdmep-downloader
# 2. Crie um ambiente virtual
python3 -m venv venv
source venv/bin/activate
# 3. Instale as dependências
pip install flask requests playwright
# 4. Instale o navegador headless usado pela automação
playwright install chromium
# 5. Execute
python3 Bdmep_app.py
Com o servidor rodando, abra o navegador em: http://localhost:5000
Para encerrar, pressione Ctrl+C no terminal.
Windows
:: 1. Clone o repositório (ou baixe o ZIP pelo GitHub e extraia)
git clone https://github.com/ruiogawa/bdmep-downloader.git
cd bdmep-downloader
:: 2. Crie um ambiente virtual
python -m venv venv
venv\Scripts\activate
:: 3. Instale as dependências
pip install flask requests playwright
:: 4. Instale o navegador headless
playwright install chromium
:: 5. Execute
python Bdmep_app.py
Com o servidor rodando, abra o navegador em: http://localhost:5000
!!! tip "Dica Windows"
Caso apareça uma mensagem pedindo dependências adicionais do sistema ao rodar playwright install chromium, execute o terminal como Administrador e repita o comando.
Para encerrar, pressione Ctrl+C no terminal.
Com Docker
Se preferir usar Docker:
git clone https://github.com/ruiogawa/bdmep-downloader.git
cd bdmep-downloader
docker compose up -d
Acesse em: http://localhost:5010
Para parar: docker compose down
!!! note "Quando usar cada opção"
- Acesso online (bdmep.ruiogawa.net): mais rápido, sem instalação, ideal para uso pontual.
- Python local (localhost:5000): recomendado para quem vai usar com frequência ou em ambientes sem acesso externo.
- Docker local (localhost:5010): ideal para quem já usa Docker e quer isolamento de dependências.
Arquitetura
┌─────────────────────────────────────┐
│ Flask Web App (localhost:5000) │
│ │
│ Frontend HTML/JS │
│ └─ formulário de seleção │
│ │
│ Backend Python │
│ ├─ /api/estacoes │ ← proxy apibdmep.inmet.gov.br
│ ├─ /api/variaveis │ ← proxy apitempo.inmet.gov.br (por tipo de estação)
│ ├─ /api/submeter │ ← dispara job em thread
│ ├─ /api/progresso/<job_id> │ ← polling de status
│ ├─ /api/download/<job_id> │ ← serve o ZIP
│ └─ /api/confirmar-pendentes │ ← confirma sem email
│ │
│ Worker Thread │
│ ├─ Playwright (Chromium) │ ← automação + interceptação de rede
│ └─ requests (download) │
└─────────────────────────────────────┘
Detalhes técnicos
APIs descobertas
| Endpoint | Método | Descrição |
|---|---|---|
https://apibdmep.inmet.gov.br/{tipo}/R/{regiao} |
GET | Lista estações por tipo (M/T) e região |
https://apitempo.inmet.gov.br/BNDMET/atributos/{estacao}/{tipo} |
GET | Lista variáveis disponíveis por estação de referência |
https://apibdmep.inmet.gov.br/requisicao/count |
POST email= |
Lista requisições pendentes do email |
https://apibdmep.inmet.gov.br/requisicao/status/{hash} |
GET | Consulta status e confirma sem email |
Estações de referência para variáveis
Para listar as variáveis corretas é necessário consultar uma estação de referência compatível com o tipo selecionado pelo usuário:
ESTACAO_REF = {
"M": "83377", # Brasília convencional
"T": "A001", # Brasília automática
}
O backend recebe tipo_estacao como parâmetro em /api/variaveis e escolhe a estação de referência adequada antes de consultar a API do INMET.
Fluxo de status da requisição
GET /status/{hash} → ["array"] = status 1 (na fila)
GET /status/{hash} → {"status": "2"} = processando
GET /status/{hash} → {"status": "3"} = concluído
Bypass do e-mail de confirmação
Fazer GET /requisicao/status/{hash} antes de clicar no e-mail já confirma a requisição, pulando completamente essa etapa.
Problemas encontrados e soluções
Proteção F5 BIG-IP (anti-bot)
Problema: O servidor usa cookies TS0160fa37 e Abacaxi gerados pela proteção F5 BIG-IP. Chamadas diretas via requests Python criam um hash na base de dados, mas os parâmetros (estações, variáveis) são descartados.
Solução: Usar Playwright (Chromium headless) para carregar o site real e obter cookies legítimos. Além disso, o browser é lançado com flags de stealth para evitar detecção:
browser = p.chromium.launch(
headless=True,
args=[
"--no-sandbox",
"--disable-blink-features=AutomationControlled",
"--disable-dev-shm-usage",
"--disable-infobars",
]
)
E o navigator.webdriver é removido via add_init_script:
Object.defineProperty(navigator, 'webdriver', {get: () => undefined});
window.chrome = { runtime: {} };
CORS bloqueia fetch() cross-origin
Problema: page.evaluate() com fetch() para apibdmep.inmet.gov.br a partir de bdmep.inmet.gov.br retorna status: 0, TypeError: Failed to fetch.
Solução: Abandonar o fetch() e fazer automação real da UI — clicar em cada passo do formulário com Playwright.
Checkboxes CSS-hidden
Problema: As estações e variáveis no formulário são <input type="checkbox"> com width: 0; height: 0. Playwright não clica em elementos sem dimensão.
Solução: Usar page.evaluate() para setar diretamente no DOM:
const cb = document.querySelector('input[name="variaveis"][value="' + code + '"]');
cb.checked = true;
cb.dispatchEvent(new Event('change', {bubbles: true}));
Variáveis diferentes entre tipos de estação
Problema: O backend sempre consultava a estação 83377 (convencional) para listar variáveis, independente do tipo selecionado pelo usuário. Ao selecionar estações automáticas, os códigos de variáveis eram incompatíveis → nenhum checkbox marcado no formulário → servidor rejeita → nenhuma requisição criada.
Solução: Dois ajustes simultâneos:
- Backend: recebe
tipo_estacaoem/api/variaveise usaESTACAO_REFpara selecionar a estação correta. - Frontend: ao mudar o tipo de estação, além de recarregar as estações, também recarrega as variáveis e passa
tipo_estacaona requisição:
document.querySelectorAll("input[name=tipo_estacao]").forEach(r =>
r.addEventListener("change", () => {
deselecionarTodos("lista-estacoes");
deselecionarTodos("lista-variaveis");
carregarEstacoes();
carregarVariaveis(); // essencial: recarrega com tipo correto
})
);
async function carregarVariaveis() {
const tipo_dados = document.querySelector("input[name=tipo_dados]:checked").value;
const tipo_estacao = document.querySelector("input[name=tipo_estacao]:checked").value;
const resp = await fetch(`/api/variaveis?tipo=${tipo_dados}&tipo_estacao=${tipo_estacao}`);
...
}
Captura do hash da requisição
Problema: Após o Playwright clicar em Confirmar, o servidor cria a requisição e retorna o hash (bcrypt $2a$10$...) no corpo da resposta HTTP. Sem esse hash não é possível confirmar nem baixar os dados.
Tentativa inicial (frágil): aguardar 3–6s e fazer diff do endpoint /count — sujeito a race condition se o servidor demorar mais.
Solução: Interceptar a resposta de rede diretamente no Playwright com page.on("response", ...):
hash_capturado = []
def on_response(response):
try:
if "requisicao" in response.url and response.request.method == "POST":
body = response.json()
if isinstance(body, list):
for item in body:
if isinstance(item, dict) and "hash" in item:
hash_capturado.append(item["hash"])
elif isinstance(body, dict) and "hash" in body:
hash_capturado.append(body["hash"])
except Exception:
pass
page.on("response", on_response)
O diff de /count permanece como fallback caso a interceptação não capture o hash (ex.: resposta chega após o browser fechar).
URL de download desconhecida
Problema: O padrão de URL para o ZIP gerado era desconhecido. O hash bcrypt tem formato $2a$10$<53chars> e não pode ser usado diretamente em uma URL.
Solução: Função _baixar_zip() tenta 6 variações em sequência, detectando o ZIP pelos magic bytes PK (independente do Content-Type retornado):
{FRONTEND}/{hash_sem_prefixo}.zip— parte após$2a$10${FRONTEND}/{hash_url_encoded}.zip{API_BASE}/requisicao/download/{hash_curto}{API_BASE}/requisicao/download/{hash_encoded}{FRONTEND}/{hash_completo}.zip{API_BASE}/download/{hash_curto}.zip
Estrutura do formulário BDMEP
O formulário em bdmep.inmet.gov.br é jQuery/vanilla JS (não React). Todos os passos existem no DOM simultaneamente, alternados por display: block/none.
Seletores-chave
| Elemento | Seletor CSS |
|---|---|
| Próximo (instruções) | a.instrucoes_proximo |
| Próximo (passo 1) | a.form1_proximo |
| Próximo (passo 2) | a.form2_proximo |
| Confirmar | a.confirmacao_confirmar |
input.email |
|
| Data início | #datepickerInicio (formato dd/mm/yyyy) |
| Data fim | #datepickerFim (formato dd/mm/yyyy) |
| Tipo de dados | input[name="tipo_dados"][value="D/H/M"] |
| Tipo de estação | input[name="tipo_estacao"][value="M/T"] |
| Separador decimal | input[name="tipo_pontuacao"][value="P/V"] |
| Abrangência | input[name="abrangencia"][value="P"] (P = País) |
| Variáveis | input[name="variaveis"][value="{codigo}"] |
| Estações | input[name="estacoes"][value="{codigo}"] |
Fluxo da automação Playwright
carregar bdmep.inmet.gov.br (obtém cookies F5, registra on_response)
↓
click a.instrucoes_proximo
↓
preencher input.email → click a.form1_proximo
↓
setar radios (tipo_dados, tipo_estacao, tipo_pontuacao, abrangencia=P)
setar datas (#datepickerInicio, #datepickerFim)
marcar checkboxes de variáveis via JS (códigos do tipo correto)
marcar checkboxes de estações via JS
↓
click a.form2_proximo
↓
click a.confirmacao_confirmar ← dispara o POST /requisicao real
↓
on_response captura o hash da resposta HTTP
Variáveis disponíveis por tipo de estação
Estações convencionais (M) — diários (D)
9 variáveis: Evaporação Piché, Insolação, Precipitação, Temp. Máxima, Temp. Média, Temp. Mínima, Umidade Média, Umidade Mínima, Vento Velocidade Média
Estações automáticas (T) — diários (D)
10 variáveis: Precipitação, Pressão Atmosférica, Temp. Ponto de Orvalho, Temp. Máxima, Temp. Média, Temp. Mínima, Umidade Média, Umidade Mínima, Vento Rajada Máxima, Vento Velocidade Média
Formato do CSV gerado pelo BDMEP
- Separador:
;(ponto e vírgula) - Encoding:
latin-1 - 9 linhas de metadados antes dos dados
- Última coluna sempre vazia (ponto e vírgula final — normal)
- Abrir no Excel: Dados → De Texto/CSV → separador = ponto e vírgula
Funções principais
_submeter_via_browser()
Abre Chromium headless com flags de stealth, navega pelo formulário BDMEP passo a passo e clica em Confirmar. Registra page.on("response", ...) para interceptar o hash diretamente da resposta de rede. Retorna:
{
"status": 200,
"text": "Formulário submetido via automação de navegador",
"hash": hash_capturado[0] if hash_capturado else None,
}
_baixar_zip(s, hash_req, log_fn)
Tenta 6 variações de URL para baixar o ZIP. Detecta ZIP pelos magic bytes PK caso o Content-Type seja incorreto ou ausente.
_aguardar_e_baixar(s, hash_req, job, log)
Confirma a requisição via GET no endpoint de status (bypass do email), aguarda o status chegar a "3" (loop com sleep de 8s, timeout de 10 min) e baixa o ZIP via _baixar_zip(). Função compartilhada entre _processar_job() e _confirmar() (Confirmar Pendentes).
_processar_job()
Worker que roda em thread separada:
- Snapshot dos hashes existentes via
/count(referência para o fallback) - Chama
_submeter_via_browser()— obtém hash via interceptação de rede - Se
hashnão foi interceptado: aguarda 6s e detecta novo hash via diff de/count - Se ainda não há hash: retorna erro com mensagem detalhada de possíveis causas
- Chama
_aguardar_e_baixar()
Dependências
| Pacote | Uso |
|---|---|
| Flask | Servidor web |
| Playwright | Automação de navegador Chromium headless + interceptação de rede |
| Requests | Chamadas à API e download do ZIP |
Arquivos do projeto
| Arquivo | Descrição |
|---|---|
Bdmep_app.py |
Aplicação Flask principal (backend + frontend HTML embutido) |
Dockerfile |
Imagem Docker baseada em python:3.12-slim |
docker-compose.yml |
Orquestração para deploy local ou em servidor |
Próximos passos
- [ ] Pacote para execução local sem instalação de Python (PyInstaller)
- [x] Confirmar padrão de URL de download do ZIP — resolvido com
_baixar_zip()(6 variações + magic bytes) - [ ] Pós-processamento opcional para remover colunas vazias do CSV
Referências
- BDMEP: https://bdmep.inmet.gov.br
- API requisições: https://apibdmep.inmet.gov.br
- API tempo/atributos: https://apitempo.inmet.gov.br
- Playwright Python: https://playwright.dev/python/
Autor
Desenvolvido por Rui Ogawa
📧 ruiogawa@gmail.com
🐙 github.com/ruiogawa/bdmep-downloader
Licença MIT