Guia para Desenvolvedores R¶
Guia prático para acessar dados agrícolas brasileiros em R, usando o agrobr como referência de implementação.
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). 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 / 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 |
Nenhum pacote R cobre CEPEA, CONAB (nenhum módulo), ANDA, ABIOVE, IMEA, DERAL, ComexStat, Desmatamento, Queimadas, MapBiomas ou B3.
Exemplos por Fonte¶
CEPEA (headless browser)¶
Licença: CC BY-NC 4.0
Uso não-comercial livre com atribuição.
O CEPEA usa Cloudflare, então httr2 direto recebe 403.
Usar chromote (headless Chrome nativo do R):
library(chromote)
library(rvest)
buscar_cepea <- function(produto) {
slugs <- list(
soja = "soja", milho = "milho", boi = "boi-gordo",
cafe = "cafe", algodao = "algodao", trigo = "trigo",
arroz = "arroz", acucar = "acucar", frango = "frango",
suino = "suino", etanol = "etanol", leite = "leite",
laranja = "laranja"
)
slug <- slugs[[produto]]
if (is.null(slug)) stop(paste("Produto nao suportado:", produto))
url <- paste0("https://www.cepea.org.br/br/indicador/", slug, ".aspx")
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]]
}
df_soja <- buscar_cepea("soja")
CONAB CEASA (HTTP puro)¶
Licença: Dados públicos
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 = "/public/Prohort/Precos.cda",
dataAccessId = "precos",
userid = "pentaho",
password = "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()
rows <- dados$resultset
df <- do.call(rbind, lapply(rows, function(r) {
data.frame(
produto = r[[1]], ceasa = r[[2]], preco = r[[3]],
stringsAsFactors = FALSE
)
}))
if (!is.null(produto)) {
df <- df[grepl(produto, df$produto, ignore.case = TRUE), ]
}
tibble::as_tibble(df)
}
df <- buscar_ceasa("tomate")
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) {
urls <- list(
soja = "https://www.gov.br/conab/.../soja/view",
milho = "https://www.gov.br/conab/.../milho/view"
)
url <- urls[[produto]]
if (is.null(url)) stop(paste("Produto nao mapeado:", produto))
tmp <- tempfile(fileext = ".xls")
req <- request(url) |>
req_headers(`User-Agent` = "Mozilla/5.0") |>
req_timeout(60)
resp <- req |> req_perform()
writeBin(resp_body_raw(resp), tmp)
readxl::read_xls(tmp)
}
IBGE/SIDRA¶
library(sidrar)
pam <- get_sidra(
api = "/t/5457/n3/all/v/214,216/p/2023/c81/2713"
)
lspa <- get_sidra(
api = "/t/6588/n3/all/v/214,216/p/202406/c81/2713"
)
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.economia.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, stringsAsFactors = FALSE)
}
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().
Normalização em R¶
Culturas¶
Porte essencial do agrobr/normalize/crops.py (156 variantes → 41 canônicos):
CULTURAS <- c(
"soja" = "soja", "soja em grao" = "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 (156 variantes) em agrobr/normalize/crops.py
)
normalizar_cultura <- function(nome) {
key <- tolower(trimws(nome))
if (key %in% names(CULTURAS)) return(CULTURAS[[key]])
key_sem_acento <- stringi::stri_trans_general(key, "Latin-ASCII")
nomes_sem_acento <- stringi::stri_trans_general(names(CULTURAS), "Latin-ASCII")
idx <- match(key_sem_acento, nomes_sem_acento)
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 <- 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]]
preco_saca * (1000 / peso)
}
sacas_para_toneladas(100, "sc60kg") # 6.0
preco_saca_para_tonelada(150, "sc60kg") # 2500
Encoding¶
library(stringi)
decodificar_response <- function(raw_bytes) {
det <- stri_enc_detect(raw_bytes)[[1]]
encoding <- det$Encoding[1]
confianca <- det$Confidence[1]
if (confianca > 0.7) {
return(stri_encode(raw_bytes, from = encoding, to = "UTF-8"))
}
for (enc in c("UTF-8", "ISO-8859-1", "Windows-1252")) {
tryCatch(
return(stri_encode(raw_bytes, from = enc, to = "UTF-8")),
error = function(e) NULL
)
}
iconv(rawToChar(raw_bytes), from = "UTF-8", to = "UTF-8", sub = "?")
}
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]] %||% 0
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", 2.0, {
request("https://...") |> req_perform()
})
Alternativa idiomática com httr2:
req <- request("https://apisidra.ibge.gov.br/...") |>
req_throttle(rate = 1 / 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 = ~ 2 # exponential backoff base 2
)
Cache com DuckDB¶
library(DBI)
library(duckdb)
con <- dbConnect(duckdb(), dbdir = "~/.agrobr/cache/agrobr.duckdb")
cache_get <- function(con, fonte, produto, ttl_horas = 4) {
query <- sprintf(
"SELECT * FROM cache
WHERE fonte = '%s' AND produto = '%s'
AND collected_at > NOW() - INTERVAL '%d hours'
ORDER BY collected_at DESC LIMIT 1",
fonte, produto, ttl_horas
)
tryCatch(dbGetQuery(con, query), error = function(e) NULL)
}
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 # Via chromote
| +-- 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/
4 dos 5 módulos CONAB funcionam sem browser
CEASA, custo de produção, progresso e série histórica usam HTTP puro.
Apenas o boletim de safras correntes precisa de chromote. 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 | -- |
| 7 | CEPEA (headless) | chromote |
-- |
| 8 | CONAB Boletim (headless) | chromote |
-- |
| 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