Skip to content

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:

  1. Preencher formulário no site
  2. Aguardar e-mail de confirmação
  3. Clicar no link do e-mail
  4. Aguardar processamento
  5. 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

  1. Selecione o tipo de estação (Convencional ou Automática)
  2. Escolha a estação pelo código ou nome
  3. Marque as variáveis desejadas
  4. Defina o período (data inicial e final)
  5. Selecione o formato de saída (separador decimal)
  6. 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:

👉 bdmep.ruiogawa.net


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

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:

  1. Backend: recebe tipo_estacao em /api/variaveis e usa ESTACAO_REF para selecionar a estação correta.
  2. Frontend: ao mudar o tipo de estação, além de recarregar as estações, também recarrega as variáveis e passa tipo_estacao na 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):

  1. {FRONTEND}/{hash_sem_prefixo}.zip — parte após $2a$10$
  2. {FRONTEND}/{hash_url_encoded}.zip
  3. {API_BASE}/requisicao/download/{hash_curto}
  4. {API_BASE}/requisicao/download/{hash_encoded}
  5. {FRONTEND}/{hash_completo}.zip
  6. {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
Email 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:

  1. Snapshot dos hashes existentes via /count (referência para o fallback)
  2. Chama _submeter_via_browser() — obtém hash via interceptação de rede
  3. Se hash não foi interceptado: aguarda 6s e detecta novo hash via diff de /count
  4. Se ainda não há hash: retorna erro com mensagem detalhada de possíveis causas
  5. 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


Autor

Desenvolvido por Rui Ogawa
📧 ruiogawa@gmail.com
🐙 github.com/ruiogawa/bdmep-downloader


Licença MIT