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