Ir para o conteúdo

Guia para Desenvolvedores R

Guia prático para acessar dados agrícolas brasileiros em R, usando o agrobr como referência de implementação. Os exemplos pedem R ≥ 4.4 (operador %||% do R base).

Licenças dos Dados

Antes de implementar acesso a qualquer fonte, consulte a página de licenças. Este guia inclui exemplos apenas para fontes com licença livre ou CC BY-NC (não-comercial com atribuição) e para a CONAB CEASA (zona_cinza; veja a página de licenças). Para armadilhas técnicas de todas as fontes (incluindo as restritas), veja Armadilhas por Fonte.


Equivalências Python → R

Python (agrobr) R equivalente Pacote
httpx (async HTTP) httr2::request() httr2
BeautifulSoup + lxml rvest::read_html() rvest, xml2
Playwright (headless) chromote::ChromoteSession chromote
pandas.DataFrame tibble / data.frame tibble
DuckDB (cache) DBI + duckdb duckdb
Pydantic v2 (validação) checkmate ou validação manual checkmate
structlog (logging) logger::log_info() logger
chardet (encoding) stringi::stri_enc_detect() stringi
openpyxl / calamine / read_excel readxl::read_excel() readxl
pdfplumber (PDF) pdftools::pdf_text() pdftools
asyncio (paralelismo) furrr + future furrr

Sobre async

O agrobr é async-first (httpx + asyncio). R é single-thread, então requests sequenciais com httr2 + Sys.sleep() para rate limiting funcionam bem. Para paralelismo, furrr + future ajuda.


Pacotes R Existentes

Estes pacotes já cobrem parte do escopo:

Pacote O que faz Cobre qual fonte
sidrar Acesso à API SIDRA/IBGE IBGE (PAM, LSPA, PPM)
nasapower Dados NASA POWER NASA POWER
GetBCBData Séries BCB BCB (parcial)
rbcb API BCB BCB (parcial)
deflateBR Deflacionar séries BR Utilidade auxiliar
comexr Cliente da API do ComexStat ComexStat
rb3 Arquivos públicos da B3 (ajustes de futuros, curvas de juros, índices) B3 (parcial)
datazoom.amazonia Dados da Amazônia Legal (PRODES, DETER, MapBiomas, comércio exterior) Desmatamento e MapBiomas (parcial)

Nenhum pacote do CRAN cobre CEPEA, CONAB (nenhum módulo), ANDA, ABIOVE, IMEA, DERAL ou Queimadas.


Exemplos por Fonte

CEPEA (headless browser)

Licença: CC BY-NC 4.0

Uso não-comercial livre com atribuição.

O CEPEA fica atrás do Cloudflare, que pode responder 403 a requests diretos com httr2. Por isso o exemplo usa chromote (headless Chrome nativo do R):

url é o endereço da página do indicador no site do CEPEA.

library(chromote)
library(rvest)

buscar_cepea <- function(url) {
  b <- ChromoteSession$new()
  b$Page$navigate(url = url)
  Sys.sleep(3)

  html <- b$Runtime$evaluate("document.documentElement.outerHTML")$result$value
  b$close()

  page <- read_html(html)
  tabelas <- page |> html_table()
  tabelas[[1]]
}

url_soja <- "<página do indicador da soja no site do CEPEA>"
df_soja <- buscar_cepea(url_soja)

CONAB CEASA (HTTP puro)

Licença: zona cinza (API não documentada, credenciais públicas do frontend)

Sem browser

API REST do Pentaho acessível com httr2 direto.

library(httr2)
library(jsonlite)

buscar_ceasa <- function(produto = NULL) {
  url <- paste0(
    "https://pentahoportaldeinformacoes.conab.gov.br",
    "/pentaho/plugin/cda/api/doQuery"
  )

  req <- request(url) |>
    req_url_query(
      path = "/home/PROHORT/precoDia.cda",
      dataAccessId = "MDXProdutoPreco"
    ) |>
    req_auth_basic("pentaho", "password") |>
    req_headers(
      `Accept` = "application/json",
      `Accept-Language` = "pt-BR"
    ) |>
    req_timeout(30) |>
    req_retry(max_tries = 3, backoff = ~ 2)

  resp <- req |> req_perform()

  dados <- resp |> resp_body_json()
  cabecalhos <- vapply(dados$metadata[-1], function(m) m$colName, character(1))
  datas <- as.Date(sub(".*\\((\\d{2}/\\d{2}/\\d{4})\\).*", "\\1", cabecalhos), "%d/%m/%Y")
  ceasas <- sub("\\s*\\(\\d{2}/\\d{2}/\\d{4}\\).*$", "", cabecalhos)
  ceasas <- trimws(gsub("\\s*\r\\s*", " - ", ceasas))

  df <- do.call(rbind, lapply(dados$resultset, function(r) {
    precos <- r[-1]
    ok <- !vapply(precos, is.null, logical(1))
    if (!any(ok)) return(NULL)
    data.frame(
      produto = r[[1]], ceasa = ceasas[ok], data = datas[ok],
      preco = unlist(precos[ok]),
      stringsAsFactors = FALSE
    )
  }))

  if (!is.null(produto)) {
    df <- df[grepl(produto, df$produto, ignore.case = TRUE), ]
  }

  tibble::as_tibble(df)
}

df <- buscar_ceasa("tomate")

Cada coluna da resposta traz no cabeçalho a data do último preço daquela CEASA, que pode ter meses ou anos; por isso o exemplo guarda a data ao lado do preco.

CONAB Série Histórica (HTTP puro)

Licença: Dados públicos

Sem browser

Download direto de XLS via URLs fixas.

library(httr2)
library(readxl)

buscar_serie_historica <- function(produto, aba = "Produção") {
  base <- paste0(
    "https://www.gov.br/conab/pt-br/atuacao/informacoes-agropecuarias",
    "/safras/series-historicas/graos/"
  )
  arquivos <- list(
    soja = "soja/sojaseriehist.xls",
    milho = "milho/milhototalseriehist.xls"
  )

  arquivo <- arquivos[[produto]]
  if (is.null(arquivo)) stop(paste("Produto nao mapeado:", produto))

  tmp <- tempfile(fileext = ".xls")
  req <- request(paste0(base, arquivo)) |>
    req_headers(`User-Agent` = "Mozilla/5.0") |>
    req_timeout(60)

  resp <- req |> req_perform()
  writeBin(resp_body_raw(resp), tmp)

  bruto <- read_xls(tmp, sheet = aba, col_names = FALSE, .name_repair = "minimal")
  cabecalho <- which(bruto[[1]] == "REGIÃO/UF")
  dados <- bruto[-seq_len(cabecalho), ]
  names(dados) <- c("regiao_uf", unlist(bruto[cabecalho, -1]))
  dados <- dados[!is.na(dados[[2]]), ]
  dados[-1] <- lapply(dados[-1], as.numeric)
  dados
}

producao_soja <- buscar_serie_historica("soja")

O XLS tem uma aba por métrica (Área, Produtividade, Produção) e linhas de título acima do cabeçalho REGIÃO/UF. A coluna da safra corrente pode vir marcada como Previsão no cabeçalho: é estimativa, não número fechado.

IBGE/SIDRA

library(sidrar)

pam <- get_sidra(
  api = "/t/5457/n3/all/v/214,216/p/2023/c782/40124"
)

lspa <- get_sidra(
  api = "/t/6588/n3/all/v/35,109/p/202406/c48/39443"
)

Rate limit SIDRA

Adicione Sys.sleep(1) entre chamadas ao SIDRA.

NASA POWER

library(nasapower)

clima <- get_power(
  community = "ag",
  lonlat = c(-55.0, -12.5),
  pars = c("T2M", "T2M_MAX", "T2M_MIN", "PRECTOTCORR", "RH2M"),
  dates = c("2024-01-01", "2024-12-31"),
  temporal_api = "daily"
)

ComexStat (HTTP puro)

library(httr2)

buscar_exportacao <- function(ano) {
  url <- paste0(
    "https://balanca.mdic.gov.br/balanca/bd/",
    "comexstat-bd/ncm/EXP_", ano, ".csv"
  )

  req <- request(url) |>
    req_headers(`User-Agent` = "Mozilla/5.0") |>
    req_timeout(120)

  resp <- req |> req_perform()

  tmp <- tempfile(fileext = ".csv")
  writeBin(resp_body_raw(resp), tmp)

  read.csv2(tmp, colClasses = c(CO_NCM = "character"))
}

df <- buscar_exportacao(2024)

Separador ponto e vírgula

CSVs do ComexStat usam ; como separador. Usar read.csv2() ou readr::read_csv2() em vez de read.csv(). Leia CO_NCM como texto: o código tem 8 dígitos e perde o zero à esquerda se virar número (café: 09011110).


Normalização em R

Culturas

Porte essencial do agrobr/normalize/crops.py (158 variantes → 43 canônicos):

CULTURAS <- c(
  "soja" = "soja", "soja em grao" = "soja",
  "soybean" = "soja", "soybeans" = "soja",
  "milho" = "milho", "milho total" = "milho",
  "corn" = "milho", "maize" = "milho",
  "milho 1a safra" = "milho_1", "milho 2a safra" = "milho_2",
  "cafe" = "cafe", "coffee" = "cafe",
  "algodao" = "algodao", "cotton" = "algodao",
  "trigo" = "trigo", "wheat" = "trigo",
  "arroz" = "arroz", "rice" = "arroz",
  "feijao" = "feijao",
  "boi" = "boi", "boi gordo" = "boi", "cattle" = "boi",
  "acucar" = "acucar", "sugar" = "acucar",
  "cana" = "cana", "sugarcane" = "cana"
  # Mapeamento completo (158 variantes) em agrobr/normalize/crops.py
)

normalizar_cultura <- function(nome) {
  key <- tolower(trimws(nome))

  if (key %in% names(CULTURAS)) return(CULTURAS[[key]])

  sem_acento <- function(x) stringi::stri_trans_general(x, "NFKD; [:Nonspacing Mark:] Remove")
  idx <- match(sem_acento(key), sem_acento(names(CULTURAS)))
  if (!is.na(idx)) return(CULTURAS[[idx]])

  gsub(" ", "_", key)
}

normalizar_cultura("Soja em Grao")    # "soja"
normalizar_cultura("milho 2a safra")  # "milho_2"
normalizar_cultura("ALGODAO")         # "algodao"

Safras

INICIO_SAFRA_MES <- 7L  # Julho

normalizar_safra <- function(safra) {
  safra <- gsub("\\s*/\\s*", "/", trimws(safra))

  if (grepl("^\\d{4}/\\d{2}$", safra)) return(safra)

  if (grepl("^\\d{2}/\\d{2}$", safra)) {
    partes <- strsplit(safra, "/")[[1]]
    ano <- as.integer(partes[1])
    prefixo <- ifelse(ano >= 50, "19", "20")
    return(paste0(prefixo, partes[1], "/", partes[2]))
  }

  if (grepl("^\\d{4}/\\d{4}$", safra)) {
    partes <- strsplit(safra, "/")[[1]]
    return(paste0(partes[1], "/", substr(partes[2], 3, 4)))
  }

  stop(paste("Formato de safra invalido:", safra))
}

safra_atual <- function(data = Sys.Date()) {
  ano <- as.integer(format(data, "%Y"))
  mes <- as.integer(format(data, "%m"))
  if (mes >= INICIO_SAFRA_MES) {
    paste0(ano, "/", substr(as.character(ano + 1L), 3, 4))
  } else {
    paste0(ano - 1L, "/", substr(as.character(ano), 3, 4))
  }
}

normalizar_safra("24/25")       # "2024/25"
normalizar_safra("2024/2025")   # "2024/25"
safra_atual()                   # depende da data

Unidades

PESO_SACA_KG <- list(sc60kg = 60, sc50kg = 50, sc40kg = 40)
PESO_ARROBA_KG <- 15
PESO_BUSHEL_KG <- list(soja = 27.2155, milho = 25.4012, trigo = 27.2155)

sacas_para_toneladas <- function(sacas, tipo = "sc60kg") {
  peso <- PESO_SACA_KG[[tipo]]
  if (is.null(peso)) stop(paste("Tipo de saca invalido:", tipo))
  sacas * peso / 1000
}

preco_saca_para_tonelada <- function(preco_saca, tipo = "sc60kg") {
  peso <- PESO_SACA_KG[[tipo]]
  if (is.null(peso)) stop(paste("Tipo de saca invalido:", tipo))
  preco_saca * (1000 / peso)
}

sacas_para_toneladas(100, "sc60kg")       # 6
preco_saca_para_tonelada(150, "sc60kg")   # 2500

Encoding

library(stringi)

decodificar_response <- function(raw_bytes) {
  if (stri_enc_isutf8(raw_bytes)) {
    return(stri_encode(raw_bytes, from = "UTF-8", to = "UTF-8"))
  }

  texto <- iconv(list(raw_bytes), from = "windows-1252", to = "UTF-8")
  if (!is.na(texto)) return(texto)

  iconv(list(raw_bytes), from = "ISO-8859-1", to = "UTF-8")
}

Rate Limiting em R

rate_limiters <- new.env(parent = emptyenv())

com_rate_limit <- function(fonte, delay_s, expr) {
  agora <- proc.time()["elapsed"]
  ultimo <- rate_limiters[[fonte]] %||% -Inf

  espera <- delay_s - (agora - ultimo)
  if (espera > 0) Sys.sleep(espera)

  resultado <- force(expr)
  rate_limiters[[fonte]] <- proc.time()["elapsed"]
  resultado
}

# Uso com httr2:
com_rate_limit("cepea", 5.0, {
  request("https://...") |> req_perform()
})

Alternativa idiomática com httr2 (≥ 1.1.1). Nessa versão req_throttle() passou a usar balde de tokens, e rate = 1 sozinho libera rajadas de até 60 requests; capacity = 1 espaça cada um:

req <- request("https://apisidra.ibge.gov.br/...") |>
  req_throttle(capacity = 1, fill_time_s = 1)  # 1 request por segundo

Retry com httr2

req <- request("https://...") |>
  req_retry(
    max_tries = 3,
    is_transient = \(resp) resp_status(resp) %in% c(408, 429, 500, 502, 503, 504),
    backoff = \(i) min(2^(i - 1), 30)  # 1 s, 2 s, ... (teto de 30 s)
  )

Cache com DuckDB

library(DBI)
library(duckdb)

dir_cache <- tools::R_user_dir("agrobr", "cache")
dir.create(dir_cache, recursive = TRUE, showWarnings = FALSE)
con <- dbConnect(duckdb(), dbdir = file.path(dir_cache, "agrobr.duckdb"))

cache_get <- function(con, fonte, produto, ttl_horas = 4) {
  if (!dbExistsTable(con, "cache")) return(NULL)
  dbGetQuery(
    con,
    "SELECT * FROM cache
     WHERE fonte = ? AND produto = ? AND collected_at > ?
     ORDER BY collected_at DESC LIMIT 1",
    params = list(fonte, produto, Sys.time() - ttl_horas * 3600)
  )
}

cache_set <- function(con, fonte, produto, dados) {
  # Criar tabela se nao existe, inserir dados com timestamp
  # Historico acumula -- nunca deletar dados antigos
}

Estrutura Sugerida para Pacote R

agrobr.r/
+-- DESCRIPTION
+-- NAMESPACE
+-- R/
|   +-- cepea.R              # Via chromote (CC BY-NC)
|   +-- conab_ceasa.R        # HTTP puro (httr2)
|   +-- conab_serie.R        # HTTP puro (httr2)
|   +-- conab_progresso.R    # HTTP puro (httr2)
|   +-- conab_custo.R        # HTTP puro (httr2)
|   +-- conab_safras.R       # HTTP puro (httr2); chromote opcional
|   +-- ibge.R               # Via sidrar ou direto
|   +-- nasa_power.R         # Via nasapower ou direto
|   +-- bcb.R
|   +-- comexstat.R          # HTTP puro (httr2)
|   +-- normalize_crops.R    # Essencial desde o dia 1
|   +-- normalize_dates.R    # Safras
|   +-- normalize_units.R    # Conversoes
|   +-- normalize_encoding.R
|   +-- http_utils.R         # Rate limit, retry, user-agent
|   +-- cache.R              # DuckDB
+-- inst/
|   +-- golden_data/         # Copiar de tests/golden_data/
|   +-- municipios_ibge.json # Copiar de agrobr/normalize/_municipios_ibge.json
+-- tests/
|   +-- testthat/
|       +-- test-cepea.R
|       +-- test-conab.R
|       +-- test-normalize.R
|       +-- test-golden.R    # Validar contra golden data
+-- man/

Os módulos CONAB funcionam sem browser

CEASA, custo de produção, progresso e série histórica usam HTTP puro. O boletim de safras correntes também sai por HTTP; chromote fica só como fallback opcional. Isso simplifica significativamente um port em R.


Prioridade de Implementação

Fase O que implementar Browser? Pacote R existente?
1 normalize_crops.R + http_utils.R Nenhum --
2 CONAB CEASA (HTTP puro) Nenhum --
3 CONAB Série Histórica (HTTP puro) Nenhum --
4 IBGE/SIDRA Nenhum sidrar
5 NASA POWER Nenhum nasapower
6 ComexStat (HTTP puro) Nenhum comexr
7 CEPEA (headless) chromote --
8 CONAB Boletim (HTTP; chromote opcional) Nenhum --
9 Cache DuckDB Nenhum --
10 Demais fontes livres Varia --

Ordem diferente do Python

No Python, CEPEA é prioridade 1 por ter fallback via Notícias Agrícolas (HTTP puro). Em R, fontes HTTP puro devem vir primeiro pois chromote adiciona complexidade. CONAB CEASA e Série Histórica fornecem dados valiosos sem nenhuma dependência de browser.


Recursos

  • Mapeamento de culturas completo: agrobr/normalize/crops.py
  • Safras e datas: agrobr/normalize/dates.py
  • Conversão de unidades: agrobr/normalize/units.py
  • UFs e regiões: agrobr/normalize/regions.py
  • Municípios IBGE (JSON): agrobr/normalize/_municipios_ibge.json
  • Golden tests: tests/golden_data/
  • Mapeamentos de URLs: agrobr/constants.py