Apêndice B - Como os dados em texto foram coletados?

Primeiro, foi necessário construir uma função que permitisse baixar os arquivos de cada decisão em PDF. Para isso, foi construída a seguinte função:

# Definição da função 'baixar_juris'.
# Esta função foi projetada para baixar um documento específico (em formato PDF) do STF.
# Parâmetros:
#   doc_id: A identificação única do documento a ser baixado.
#   path: O caminho da pasta onde o arquivo PDF será salvo.
baixar_juris <- function(doc_id, path) {
  # Define o caminho completo do arquivo de saída usando a função glue() para interpolação de strings.
  output_file_path <- glue::glue("{path}/{doc_id}.pdf")

  # --- Etapa 1: Simulação de Navegador para Obtenção de Cookies ---
  
  # Inicia uma nova sessão de navegador 'headless' (sem interface gráfica) usando o pacote 'chromote'.
  b <- chromote::ChromoteSession$new()
  # Define o 'User-Agent' da sessão para simular um navegador comum, uma prática para evitar bloqueios.
  b$Network$setUserAgentOverride(userAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.0.0 Safari/537.36")
  # Constrói a URL principal do documento.
  url_principal <- glue::glue("https://redir.stf.jus.br/paginadorpub/paginador.jsp?docTP=TP&docID={doc_id}")
  # Navega até a URL e aguarda o carregamento completo da página.
  b$Page$navigate(url_principal, wait_ = TRUE)
  # Pausa a execução por 5 segundos para garantir que todos os scripts da página sejam executados e os cookies sejam definidos.
  Sys.sleep(5)
  # Extrai os cookies que foram gerados pelo servidor durante a visita à página.
  # Estes cookies são essenciais para autenticar a requisição de download subsequente.
  c_value <- b$Network$getCookies()$cookies[[2]]$value
  c_name <- b$Network$getCookies()$cookies[[2]]$name
  c_value2 <- b$Network$getCookies()$cookies[[1]]$value
  c_name2 <- b$Network$getCookies()$cookies[[1]]$name

  # --- Etapa 2: Construção e Execução da Requisição de Download ---

  # Utiliza o pacote 'httr2' para construir uma requisição HTTP customizada.
  httr2::request("https://redir.stf.jus.br/paginadorpub/paginador.jsp") |>
    # Adiciona os parâmetros da URL necessários para identificar o documento.
    httr2::req_url_query(
      docTP = "TP",
      docID = doc_id
    ) |>
    # Adiciona um conjunto detalhado de cabeçalhos (headers) para simular com alta fidelidade
    # uma requisição feita por um navegador legítimo. Isso inclui informações sobre
    # o tipo de conteúdo aceito, idioma, referer, e outros detalhes de segurança.
    httr2::req_headers(
      Accept = "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7",
      `Accept-Language` = "pt-BR,pt;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6,es-ES;q=0.5,es;q=0.4,fr-FR;q=0.3,fr;q=0.2,es-CL;q=0.1",
      `Cache-Control` = "max-age=0",
      Connection = "keep-alive",
      Referer = glue::glue("https://redir.stf.jus.br/paginadorpub/paginador.jsp?docTP=TP&docID={doc_id}"),
      `Sec-Fetch-Dest` = "document",
      `Sec-Fetch-Mode` = "navigate",
      `Sec-Fetch-Site` = "same-origin",
      `Sec-Fetch-User` = "?1",
      `Upgrade-Insecure-Requests` = "1",
      `User-Agent` = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36 Edg/137.0.0.0",
      `sec-ch-ua` = '"Microsoft Edge";v="137", "Chromium";v="137", "Not/A)Brand";v="24"',
      `sec-ch-ua-mobile` = "?0",
      `sec-ch-ua-platform` = '"Windows"',
      # Crucialmente, adiciona os cookies extraídos na etapa anterior para autenticar a sessão.
      Cookie = glue::glue("{c_name2}={c_value2}; {c_name}={c_value}")
    ) |>
    # Executa a requisição e salva o corpo da resposta diretamente em um arquivo no caminho especificado.
    httr2::req_perform(path = glue::glue("{path}/{doc_id}.pdf"))
  
  # Fecha a sessão do navegador 'headless' para liberar os recursos.
  b$close()

  # --- Etapa 3: Verificação e Feedback ---

  # Verifica se o arquivo foi de fato criado no caminho esperado.
  if (!file.exists(output_file_path)) {
    # Se o arquivo não existir, emite uma mensagem de aviso informando a falha.
    message(glue::glue("AVISO: O documento com a identificação '{doc_id}' não foi criado."))
  } else {
    # Se o arquivo existir, emite uma mensagem de sucesso.
    message(glue::glue("Sucesso: Documento '{doc_id}.pdf' salvo."))
  }
}

Depois, a função foi aplicada da seguinte forma. Primeiro se criou uma lista com os códigos das decisões baseadas na tabela com as informações dos acórdãos. Depois, essa lista foi passada como argumento na função criada para baixar os arquivos. Todos os arquivos foram baixados normalmente.

# --- Bloco 1: Filtragem dos Processos de Interesse ---

# Cria um subconjunto do dataframe 'clean_processos'.
processos <- clean_processos |>
  # Mantém apenas os processos da classe "ADPF".
  dplyr::filter(classe == "ADPF") |>
  # Adiciona um segundo filtro para reter apenas os processos autuados entre 2014 e 2024.
  dplyr::filter(lubridate::year(data_autuacao) >= 2014 & lubridate::year(data_autuacao) <= 2024)

# --- Bloco 2: Identificação dos Documentos (Acórdãos) a Serem Baixados ---

# Cria um dataframe contendo os IDs dos documentos a serem baixados.
dec_cod <- clean_acordaos |>
  # Filtra o dataframe de acórdãos para manter apenas aqueles cujo número de processo
  # corresponde aos números presentes no dataframe 'processos' criado anteriormente.
  # Nota: A coluna 'processo_numero' em 'clean_acordaos' não foi renomeada para 'numero' na limpeza anterior.
  # Se o código apresentar erro, a linha correta seria: dplyr::filter(numero %in% processos$numero)
  dplyr::filter(numero %in% processos$numero) |>
  # Adiciona um filtro temporal para incluir apenas acórdãos julgados até o ano de 2024.
  dplyr::filter(lubridate::year(data) <= 2024) |>
  # Seleciona apenas a coluna que contém a URL para o inteiro teor do documento.
  dplyr::select(url) |>
  # Cria uma nova coluna 'doc_id' extraindo a identificação do documento da URL.
  # A função str_split_i() divide a string da URL pelo caractere "=" e retorna o último elemento (-1),
  # que corresponde ao ID do documento.
  dplyr::mutate(doc_id = stringr::str_split_i(url, "=", -1)) |>
  # Seleciona apenas a coluna 'doc_id', resultando em uma lista de todos os IDs a serem baixados.
  dplyr::select(doc_id)

# --- Bloco 3: Execução do Download em Lote ---

# Utiliza a função walk() do pacote 'purrr' para iterar sobre cada elemento do vetor 'dec_cod$doc_id'.
# 'walk' é ideal para quando se deseja executar uma ação com efeito colateral (como salvar um arquivo)
# para cada item, sem a necessidade de retornar um resultado.
purrr::walk(dec_cod$doc_id, ~ {
  # Para cada 'doc_id' (.x), a função 'baixar_juris' é chamada.
  # O ID do documento é passado como primeiro argumento e o caminho de destino "DATA/ACORDAOS" como segundo.
  baixar_juris(.x, "DATA/ACORDAOS")
}, .progress = TRUE) # O argumento .progress = TRUE exibe uma barra de progresso no console.

Com as decisões baixadas, prosseguiu-se com a extração do texto. Para isso, foi criada uma função para extrair o texto dos arquivos, preservando o código. Os textos foram extraidos e inseridos em um data.frame com duas colunas. Uma coluna com o código da decisão e outra coluna com o texto da decisão

# Definição da função 'extrair_texto_juris_stf'.
# Esta função foi projetada para ler uma pasta de arquivos PDF e extrair seu conteúdo textual.
# Parâmetro:
#   dir: O caminho do diretório que contém os arquivos PDF baixados.
extrair_texto_juris_stf <- function(dir) {
  # Lista todos os arquivos no diretório especificado, retornando o caminho completo de cada um.
  lista_dec <- list.files(dir, full.names = TRUE)
  # Converte a lista de arquivos em um dataframe com uma única coluna chamada 'doc'.
  lista_dec <- data.frame(
    doc = lista_dec
  )
  # Cria uma nova coluna 'doc_id' a partir do caminho do arquivo.
  lista_dec <- lista_dec |>
    dplyr::mutate(
      # A expressão extrai o nome do arquivo, remove a extensão .pdf e o isola como ID.
      doc_id = stringr::str_split_i(doc, "\\/", -1) |> stringr::str_remove("\\.pdf")
    )

  # Carrega um objeto 'areas' salvo anteriormente, que provavelmente contém coordenadas
  # específicas para a extração de texto com o 'tabulizer', otimizando a leitura.
  areas <- readRDS("DATA/MISC/area.rds")

  # Utiliza a função map2_df do 'purrr' para iterar sobre os caminhos dos arquivos e seus IDs.
  # 'map2_df' aplica uma função a cada par de elementos e une os resultados em um único dataframe.
  purrr::map2_df(lista_dec$doc, lista_dec$doc_id, ~ {
    # Obtém o número total de páginas do documento PDF atual (.x).
    p <- tabulizer::get_n_pages(.x)

    # Extrai o texto do PDF.
    # 'area = areas' restringe a extração a áreas pré-definidas da página.
    texto <- tabulizer::extract_text(.x, pages = 1:p, area = areas) |>
      # Junta o texto de todas as páginas em uma única string.
      paste(collapse = "") |>
      # Remove espaços em branco excessivos.
      stringr::str_squish() |>
      # Remove espaços em branco no início e no fim do texto.
      stringr::str_trim()

    # Retorna um dataframe de uma linha com o ID do documento e o texto extraído.
    data.frame(doc_id = .y, texto = texto)
  }, .progress = TRUE) # Exibe uma barra de progresso durante a execução.
}

Apliquei a função:

acordaos <- extrair_texto_juris_stf("DATA/ACORDAOS/")
saveRDS(acordaos, "DATA/ACORDAOS/acordaos.rds")

Agora com os textos extraídos e em um objeto no R, fiz alguns tratamentos iniciais, tanto na tabela para unir ela com as informações gerais da ADPF, bem como alguns tratamentos no próprio texto, como remover acentos:

# --- Bloco 1: Carregamento dos Dados Limpos ---

# Carrega os dataframes limpos que foram salvos em etapas anteriores do projeto.
clean_acordaos <- readRDS("DATA/CLEAN/clean_acordaos.rds")
clean_monocraticas <- readRDS("DATA/CLEAN/clean_monocraticas.rds")
# Garante que a coluna 'numero' em 'legitimados_adpf' seja do tipo numérico para a junção.
legitimados_adpf$numero <- as.numeric(legitimados_adpf$numero)

# --- Bloco 2: Enriquecimento e Consolidação Inicial ---

# Adiciona a informação sobre o tipo de legitimado ao dataframe de decisões monocráticas.
clean_monocraticas <- clean_monocraticas |>
  dplyr::left_join(
    legitimados_adpf |> dplyr::select(numero, tipo),
    by = "numero"
  )

# Simplifica o dataframe 'processos_resultado' para conter apenas informações essenciais e únicas.
processos_resultado <- processos_resultado |>
  dplyr::select(numero, tipo, resultado) |>
  dplyr::distinct()

# Adiciona a informação sobre o tipo de legitimado ao dataframe de acórdãos.
dec_adpf <- dplyr::left_join(
  clean_acordaos,
  legitimados_adpf |> dplyr::select(numero, tipo) |> dplyr::distinct(),
  by = "numero"
)

# Combina (empilha) os dataframes de acórdãos e monocráticas em um único conjunto de dados de decisões.
dec_adpf <- dec_adpf |>
  dplyr::bind_rows(
    clean_monocraticas
  )

# Filtra o dataframe de decisões combinado para um subconjunto específico de análise inicial.
dec_adpf <- dplyr::filter(
  dec_adpf,
  # Mantém apenas os processos que estão na lista de referência 'adpf_rec'.
  numero %in% adpf_rec$numero,
  # E cujo tipo de legitimado seja "Partidos".
  tipo == "Partidos"
)

# --- Bloco 3: Enriquecimento Final e Limpeza Textual ---

# Inicia uma cadeia de transformações para finalizar o dataframe.
dec_adpf <- dec_adpf |>
  # Cria a coluna 'doc_id' a partir da URL para permitir a junção com os textos extraídos dos PDFs.
  dplyr::mutate(
    doc_id = stringr::str_split_i(url, "=", -1)
  ) |>
  # Renomeia a coluna 'texto' (que contém a ementa) para 'ementa', para evitar conflito de nomes.
  dplyr::rename(
    ementa = texto
  ) |>
  # Remove a coluna 'tipo' temporariamente para evitar duplicação após o próximo join.
  dplyr::select(-tipo) |>
  # Adiciona a coluna de 'resultado' e a coluna 'tipo' definitiva a partir de 'processos_resultado'.
  dplyr::left_join(
    processos_resultado,
    by = "numero"
  ) |>
  # Filtra novamente para garantir que apenas os legitimados do tipo "Partidos" permaneçam.
  dplyr::filter(tipo == "Partidos") |>
  # Remove a coluna 'tipo', que já cumpriu seu propósito no filtro.
  dplyr::select(-tipo)

# Junta o dataframe de decisões com os textos dos acórdãos extraídos dos PDFs.
dec_adpf <- dec_adpf |>
  dplyr::left_join(
    acordaos, # Dataframe com os textos completos dos acórdãos.
    by = "doc_id"
  )

# Consolida a informação textual e finaliza a limpeza.
dec_adpf <- dec_adpf |>
  # Cria a coluna final 'texto'. Se o texto completo do PDF (texto) existir, usa-o.
  # Caso contrário (para monocráticas ou acórdãos não baixados), usa o texto da ementa.
  dplyr::mutate(texto = ifelse(is.na(texto), ementa, texto)) |>
  # Remove as colunas auxiliares que não são mais necessárias.
  dplyr::select(-ementa, -url, -doc_id) |>
  # Remove os processos que ainda não tiveram um julgamento final.
  dplyr::filter(resultado != "Sem julgamento")

# Remove quaisquer linhas que, após a consolidação, ainda não tenham nenhum conteúdo textual.
dec_adpf <- dec_adpf |> tidyr::drop_na(texto)

# Garante que não haja linhas duplicadas no dataframe final.
dec_adpf <- dec_adpf |> dplyr::distinct()

# Realiza a limpeza final do texto, removendo acentos e caracteres especiais.
dec_adpf <- dec_adpf |> dplyr::mutate(
  texto = decJ::utilitario_remover_acentos(texto),
  texto = gsub("ü", "u", texto),
  texto = gsub("Ü", "U", texto),
  texto = stringr::str_remove_all(texto, "º")
)

# Salva o dataframe final, limpo em formato .rds.
saveRDS(dec_adpf, "DATA/CLEAN/dec_adpf.rds")

O próximo passo foi criar um corpus para fazer alguns tratamentos antes de enviar os textos para o IRAMUTEQ.

Por exemplo, foi preciso retirar carácteres especiais, foi preciso unir algumas palavras ou expressões que juntas tem um significado diferente do que separado. “Controle de Constitucionalidade” significa uma coisa só, se deixarmos separado, o algoritmo vai entender que “controle” é uma coisa, “de” é outra e “constitucionalidade” é outra. Isso não é positivo e pode prejudicar análise. Então são criados dicionários de expressões para juntar essas palavras e tornar “controle_de_constitucionalidade” uma coisa só.

# Cria um objeto 'corpus' a partir do dataframe 'dec_adpf'.
# O corpus é a estrutura de dados fundamental do 'quanteda' para análise de texto.
corpus_adpf <- quanteda::corpus(
  dec_adpf,
  docid_field = "id",    # Especifica a coluna 'id' como o identificador único de cada documento.
  text_field = "texto" # Especifica a coluna 'texto' como a fonte do conteúdo textual.
)

# Realiza a tokenização do corpus, que é o processo de dividir o texto em unidades (tokens).
tokens_adpf <- quanteda::tokens(
  corpus_adpf,
  remove_punct = T,      # Remove sinais de pontuação.
  remove_symbols = T,    # Remove símbolos.
  remove_numbers = T,    # Remove números.
  remove_url = T,        # Remove URLs.
  remove_separators = T, # Remove separadores de palavras.
  split_hyphens = T,     # Divide palavras hifenizadas.
  split_tags = T         # Divide tags (e.g., de redes sociais).
)

# Converte todos os tokens para letras minúsculas para padronização.
tokens_adpf <- quanteda::tokens_tolower(
  tokens_adpf
)

# Define uma função para criar dicionários de n-grams (sequências de 'n' tokens).
dicionario.Criar <- function(token, n = 2, arquivo) {
  # Cria os n-grams a partir dos tokens.
  ngrams <- quanteda::tokens_ngrams(
    token,
    n = n,
    concatenator = " "
  )

  # Cria uma Document-Feature Matrix (DFM) a partir dos n-grams.
  ngrams.dfm <- quanteda::dfm(ngrams)
  # Identifica os 500 n-grams mais frequentes.
  ngrams.freq <- quanteda::topfeatures(ngrams.dfm, n = 500)

  # Salva a lista de n-grams frequentes em um arquivo CSV.
  write.csv2(ngrams.freq,
    file = paste(arquivo, n, "gram.csv", sep = ""),
    quote = F,
    row.names = T,
    fileEncoding = "UTF-8"
  )
}

# O processo a seguir identifica expressões multipalavra (colocações) e as une em um único token.
# A lógica é começar dos n-grams mais longos para os mais curtos.

## Aplicar sobre 6-grams
# Cria um dicionário de 6-grams (passo exploratório, não usado diretamente no 'compound').
dicionario.Criar(tokens_adpf, n = 6, "DATA/MISC/")
# Lê um dicionário pré-selecionado de 6-grams.
dic_6 <- read.delim("DATA/MISC/DIC/dic6.txt", header = FALSE)
# Une as sequências de 6-grams encontradas no texto em um único token (e.g., "supremo_tribunal_federal_etc").
tokens_adpf <- quanteda::tokens_compound(
  tokens_adpf,
  pattern = quanteda::phrase(dic_6$V1),
  concatenator = "_"
)

## Aplicar sobre 5-grams
dicionario.Criar(tokens_adpf, n = 5, "DATA/MISC/")
dic_5 <- read.delim("DATA/MISC/DIC/dic5.txt", header = FALSE)
tokens_adpf <- quanteda::tokens_compound(
  tokens_adpf,
  pattern = quanteda::phrase(dic_5$V1),
  concatenator = "_"
)

## Aplicar sobre 4-grams
dicionario.Criar(tokens_adpf, n = 4, "DATA/MISC/")
dic_4 <- read.delim("DATA/MISC/DIC/dic4.txt", header = FALSE)
tokens_adpf <- quanteda::tokens_compound(
  tokens_adpf,
  pattern = quanteda::phrase(dic_4$V1),
  concatenator = "_"
)

## Aplicar sobre 3-grams
dicionario.Criar(tokens_adpf, n = 3, "DATA/MISC/")
dic_3 <- read.delim("DATA/MISC/DIC/dic3.txt", header = FALSE)
tokens_adpf <- quanteda::tokens_compound(
  tokens_adpf,
  pattern = quanteda::phrase(dic_3$V1),
  concatenator = "_"
)

## Aplicar sobre 2-grams
dicionario.Criar(tokens_adpf, n = 2, "DATA/MISC/")
dic_2 <- read.delim("DATA/MISC/DIC/dic2.txt", header = FALSE)
tokens_adpf <- quanteda::tokens_compound(
  tokens_adpf,
  pattern = quanteda::phrase(dic_2$V1),
  concatenator = "_"
)

# --- Lematização ---
# Lematização é o processo de reduzir uma palavra à sua forma base ou de dicionário (lema).
# Carrega um léxico (dicionário de palavras e seus lemas).
lemas <- read.delim("DATA/MISC/lexique_pt.txt", header = FALSE, col.names = c("palavra", "lema", "cat"))
# Realiza a limpeza dos acentos no dicionário para compatibilidade com os tokens.
lemas <- lemas |>
  dplyr::mutate(
    dplyr::across(c(palavra, lema), decJ::utilitario_remover_acentos)
  )
# Substitui as palavras nos tokens por seus respectivos lemas.
tokens_adpf <- quanteda::tokens_replace(
  tokens_adpf,
  lemas$palavra,
  lemas$lema
)

# --- Preparação para Exportação ---
# Converte a lista de tokens de volta para strings, com os tokens separados por espaços.
iramuteq <- sapply(tokens_adpf, paste, collapse = " ") |>
  # Converte o resultado em um dataframe.
  as.data.frame() |>
  # Converte os nomes das linhas (que são os IDs dos documentos) em uma coluna.
  tibble::rownames_to_column()
# Renomeia as colunas para o formato esperado.
names(iramuteq) <- c("id", "texto")
# Remove a coluna de texto original do dataframe de metadados 'dec_adpf'.
# Isso evita redundância e libera memória, já que o texto processado está em 'iramuteq'.
dec_adpf$texto <- NULL

# Cria o dataframe final para análise, juntando os metadados dos documentos
# com o texto já processado (tokenizado, lematizado, etc.).
corpus_iramuteq <- dplyr::left_join(
  dec_adpf, # Dataframe com os metadados (e.g., resultado, data).
  iramuteq, # Dataframe com o texto processado.
  by = "id" # A junção é feita usando o identificador único do documento.
)

Os corpus para serem inseridos no IRAMUTEQ precisam ser salvo em formato .txt e seguirem uma estrutura padrão bem específica.

Primeiro, a linha com o “código” do texto. Isto é, o identificador e suas variáveis. O “código” começa com “****”, e depois cada variável deve ser indicada por “*variavel_” e logo após o valor da variável. Por exemplo: “**** *uf_RS”. A próxima linha deve conter o texto, logo após o código. E isso é replicado para cada texto:

# Prepara o dataframe final para ser exportado no formato específico do software IRAMUTEQ.
iramuteq <- corpus_iramuteq |>
  # Cria a coluna 'codigo', que conterá a linha de cabeçalho de cada documento.
  dplyr::mutate(
    # A função paste0() concatena as strings para formar o cabeçalho.
    # O formato "**** *variavel_nome" é o padrão do IRAMUTEQ para identificar metadados.
    codigo = paste0(
      "\n**** *id_",
      id,
      " *adpf_",
      numero,
      " *uf_",
      uf,
      " *dt_",
      lubridate::year(as.Date(data)),
      " *res_",
      # Substitui espaços no resultado por underscores para compatibilidade.
      resultado |> stringr::str_replace_all("\\s", "_")
    )
  ) |>
  # Seleciona apenas as colunas 'codigo' e 'texto', na ordem correta para a exportação.
  dplyr::select(codigo, texto)

# Salva o corpus formatado em um arquivo de texto.
readr::write_delim(iramuteq, "IRAMUTEQ/acordaos.txt", 
                   delim = "\n",            # Usa uma nova linha como delimitador, colocando o código e o texto em linhas separadas.
                   col_names = FALSE,       # Não inclui os nomes das colunas no arquivo.
                   quote = "none")          # Não utiliza aspas para envolver o texto.

Os próximos passos foram trabalhados no IRAMUTEQ.