1. Introdução

No texto Extrator Básico de Dados Judiciais, foi mostrado como desenvolver um extrator capaz de extrair dados em HTML, disponíveis no Código Fonte das páginas buscadas. Neste texto, exploraremos as formas pelas quais esse conjunto de dados pode ser convertido em um banco relacional, ou seja, em uma tabela na qual todos os dados possam ser identificados claramente a partir de sua pertinência a uma linha e a uma coluna.

Para iniciar o desenvolvimento dos algoritmos tratados no presente texto, convém que você tenha realizado previamente a atividade proposta no Módulo 10 do curso Data Science e Direito, que consiste em fazer a extração dos dados das ADIs 5000 a 5500, a partir do programa construído naquele Módulo.

Com essas 500 ações, você terá um conjunto de dados suficiente para testar a consistência dos algoritmos que utilizaremos para fazer a organização desses dados em uma tabela CSV, capaz de realizar 5 atividades:

  1. Definir um Modelo de Dados;
  2. Leitura dos arquivos que contém os dados extraídos;
  3. Extrair os dados definidos no modelo;
  4. Limpeza dos dados;
  5. Gravar os dados em uma tabela.

O núcleo do nosso trabalho é articular um modelo de dados com ferramentas de extração precisas, que permitam tabular as informações de maneira adequada. Mas não se deve perder de vista que a sequência dessa atividades não é linear, mas circular. Parte-se de um modelo preliminar de dados, que nos faz buscar alguns elementos, mas o tratamento das informações também nos abre espaço para perceber a existência de dados que não esperávamos encontrar, ou a inviabilidade de dados que pretendíamos extrair. Com isso, a extração dos dados normalmente leva a algum redimensionamento do modelo, em ciclos de redimensionamento, até que fique estabilizado o conjunto das variáveis que serão buscadas.

As estratégias de abertura e gravação são mais simples e inovam pouco com relação à do modelo de extração de dados. Já a limpeza de dados é uma atividade proporcional à existência de dados não-estruturados, em linguagem natural, como os textos das leis impugnadas e das decisões. Nesses casos, costuma ser necessário um certo "polimento" dos dados, visto que eles costumam vir com muita "sujeira" espaços a mais, quebras de linha indevidas, maiúsculas sem padronização e outros elementos que podem ser melhor organizados antes de você gravar os dados. Mesmo em dados estruturados, uma certa limpeza pode ser necessária, pois há dados que não seguem perfeitamente os padrões definidos no modelo (como abreviaturas que não são homogêneas ou utilização de palavras diferentes para designar o mesmo elemento).

2. Definição preliminar do modelo de dados

2.1 Elementos do modelo de dados

O primeiro passo para extrair os dados é ter uma definição mínima do tipo de informação que se pretende conseguir e do modelo de dados que será utilizado na pesquisa.

Como discutido no texto Classificações e Marco Teórico, o modelo de dados é uma estrutura em que estabelecemos uma relação entre certos objetos (unidades de análise), atributos (que são categorias que definem cada variável) e valor (que é um dados que indica o valor específico da variável para um determinado objeto). O resultado dessa estrutura é uma tabela em que:

  1. Os objetos correspondem a cada uma das linhas;
  2. As variáveis correspondem às colunas;
  3. Os valores correspondem ao conteúdo das células;
  4. O universo corresponde ao conjunto dos objetos, e normalmente dá nome à tabela.

Para que essa tabela seja de fato um banco de dados relacional, é preciso que todos os dados possam ser acessados a partir da combinação específica entre a linha e a coluna. Por isso, toda coluna deve ter um nome indicando o atributo nela contido, o que afasta colunas sem nome e colunas com nomes idênticos (tal como já estudamos quando tratamos das tabelas dinâmicas do Excel).

Nossa unidades de análise será o processo, pois é em com base nesse objeto que as informações a serem extraídas estão agregadas. Cada um dos nossos arquivos corresponde a um processo e, por isso, vamos criar uma tabela em que as linhas representam uma ADI específica.

O universo será todo o conjunto das ADIs, embora não seja útil trabalhar com todas elas ao mesmo tempo, exceto quando já tivermos um modelo de dados estabilizado.

2.2 Indicadores

Embora não seja obrigatório, é muito conveniente que cada linha tenha um campo que funcione como indicador, ou seja, que ofereça um nome único para o objeto que é tratado nessa linha. A existência do indicador faz com que, ainda que possa haver "homonímia" entre os valores de um atributo (como duas pessoas com nomes idênticos), haverá um código que possibilitará a distinção desses dois objetos diferentes na tabela.

Nas minhas tabelas sobre o STF, costumo usar como identificador a sigla da Classe + o número do processo, com 5 dígitos. O STF utiliza um indicador, que não aparece nas páginas exibidas nos navegadores, mas que é utilizado na estrutura das páginas e que ele chama de incidente. Se você pesquisar no site do Tribunal a ADI 5000, você será direcionado para a página http://portal.stf.jus.br/processos/detalhe.asp?incidente=4425942, o que indica que esse é o processo cujo incidente é 4425942. Note que a variável "incidente" não é um incidente processual, mas é apenas um indicador único utilizado para nomear a página específica da ADI 5000.

A existência do indicador tem a grande vantagem de possibilitar que integremos tabelas diferentes, pois teremos certeza de que o indicador apontará especificamente para o objeto que ele designa. Podemos unir uma tabela de Partes com uma tabela de Andamentos, desde que ambas sejam estruturadas em termos dos indicadores, que podem funcionar como uma ponte entre os dois conjuntos de informações.

2.3 Atributos

Você precisa analisar as informações disponíveis na internet para verificar quais são as variáveis que você consegue extrair do Código Fonte. Sugiro que você abra o arquivo ADI5000.html com o Spyder, o que vai lhe proporcionar uma visão como essa. Se não houver esse arquivo no seu diretório ADIhtml, faça previamente a extração dele com o Extrator Básico.

Nesse trecho do código, você consegue encontrar alguns dados importantes:

  1. Incidente
  2. Classe
  3. Liminar
  4. Número
  5. Origem
  6. Entrada no STF
  7. Relator
  8. Data da distribuição
  9. Requerente
  10. Requerido

Vamos nos concentrar nesses atributos pois a sua extração é simples e nos possibilita desenvolver as habilidades e as funções necessárias para extrair as informações restantes. Mas antes disso, pensemos um pouco nos seus atributos:

  1. Incidente: identificador com 7 dígitos
  2. Classe: dimensão, range de 40 valores
  3. Liminar: dimensão, binário (dummy)
  4. Número: dimensão (apesar de ser numérico), range de quase 7000
  5. Origem: range de 30 valores
  6. Entrada no STF: dimensão, datas desde 1988
  7. Relator: dimensão, range de 30 valores
  8. Data da distribuição, dimensão, datas desde 1988
  9. Requerente: range de quase 1000 valores
  10. Requerido: range de quase 1000 valores

3. Módulo de leitura dos dados

3.1 Função dsd.carregar_arquivo(nomedoarquivo)

No texto sobre extração de dados, desenvolvemos o seguinte módulo de gravação simplificado, que segue 4 passos.

# Módulo básico de gravação
nomedoarquivo = 'ADI.html'
arquivo = open(nomedoarquivo, 'w', encoding='utf-8')
arquivo.write(html)
arquivo.close()

O módulo de leitura segue o mesmo padrão, com apenas 3 diferenças:

  1. No lugar da opção 'w' (de write), você deve usar a opção 'r' (de read).
  2. No lugar do comando arquivo.write(html) (que escreve a string html no arquivo aberto pela função open), você deve usar o comando arquivo.read(), para ler todo o arquivo que você abriu.
  3. Você deve atribuir o conteúdo do arquivo lido a uma variável, para poder trabalhar com esses dados depois de  fechar o arquivo. Por se tratar de  dados em HTML, usaremos a variável html para esse fim.

É importante você saber esses elementos, caso você precise desenvolver uma função de leitura adaptada especificamente ao seu projeto. Porém, você pode usar a função carregar_arquivo(nomedoarquivo), para atribuir o conteúdo de um arquivo a uma variável.

def carregar_arquivo (nomedoarquivo):
    arquivo = open(nomedoarquivo, 'r', encoding='utf-8')
    html = arquivo.read()
    arquivo.close()
    return html

Para que essa função opere corretamente, você precisa oferecer um nome adequado, incluindo nele o path. Indicar o nome do arquivo, sem o endereçamento adequado, não levará a um resultado correto, pois não será possível encontrar o arquivo a ser aberto. Se você já tiver o nome do arquivo previamente definido (por exemplo, se já houver uma lista de arquivos cujo conteúdo deva ser analisado), basta usar a construção seguinte, para que as informações do arquivo sejam atribuídas à variável html.

import dsd

arquivo_a_ser_carregado = 'ADIhtml//ADI5000.txt'

html = dsd.carregar_arquivo(arquivo_a_ser_carregado)

3.2 Leitura iterada

Uma das operações típicas que faremos é a de fazer uma leitura de todos os arquivos de um determinado diretório. Nesse caso, se você tomar o cuidado de deixar no diretório somente os dados que pretende extrair, poderá utilizar uma função da biblioteca os que gera uma lista de todos os arquivos dentro de um determinado diretório: a os.listdir(path).

Se você utilizar como argumento o path do diretório a ser listado, o algoritmo função retorna uma lista com todos os arquivos contidos nele. Experimente usar o seguinte código, com o cuidado de rodá-lo no diretório em que está contida a pasta ADIhtml, gerada pelo Extrator Básico de Dados.

import os

path = 'ADIhtml'

relacao = os.listdir(path)
print (relacao)

Esse código faz com que a variável relação contenha uma lista, com todos os nomes dos arquivos contidos no diretório indicado entre parênteses, lembrando que esse uso do caminho relativo só funciona porque "ADIhtml" é subdiretório da sua pasta de trabalho.

Cuidado! A função os.list() é ótima porque nos oferece com facilidade os nomes que precisamos para usar a função dsd.carregar_arquivo(). Porém,  você precisa tomar muito cuidado com o que está dentro do diretório, já que serão listados todos os arquivos contidos na pasta, mesmo que se trate de arquivos temporários ou indevidamente gravados no local.
import os

path = 'ADIhtml//'
lista = os.listdir(path)

for nomedoarquivo in lista:
    
    print (nomedoarquivo)

Se você deseja extrair o conteúdo de cada um desses arquivos, basta inserir no loop uma função de leitura, em vez de um comando de impressão, atribuindo o conteúdo do arquivo a uma variável string (que eu tipicamente chamo de html).

Para ter certeza de que funcionou, você pode imprimir as 110 primeiras posições da string html (que vão retornar o url da página do processo na internet, que foi a primeira informação que gravamos no arquivo).

import os, dsd

path = 'ADIhtml\\'

for nomedoarquivo in os.listdir(path):
    
    arquivo_a_ser_carregado = path + nomedoarquivo
    html = dsd.carregar_arquivo(arquivo_a_ser_carregado)

    print (html[:110])
Atenção! Note que a função os.list funciona tanto com o 'ADIhtml//' (usado no código acima) quanto o 'ADIhtml' (usado anteriormente). Porém, é melhor usar o 'ADIhtml//' porque ele permite gerar mais facilmente o nome do arquivo a ser aberto, concatenando o path com o nomedoarquivo.

Observe também que você poder importar duas bibliotecas ao mesmo tempo, bastando usar o import seguido dos nomes dos módulos ou pacotes, separados por vírgulas.

3.3 Leitura iterada de um intervalo de processos

O os.listdir() funciona bem quando você quer extrair todos os processos de um arquivo. Mas o que você pode fazer quando desejar extrair apenas parte dos processos, e não todos eles? Nesse caso, você tem três opções básicas:

  1. Selecione os processos que você quer analisar e coloque em um diretório específico, que possa ser integralmente listado;
  2. Crie um comando para fazer iteração em apenas parte da lista, usando a posição dos elementos, tal como: for nomedoarquivo in lista[:10];
  3. Crie uma lista com um intervalo definido de processos.

Essa última possibilidade precisa se valer do fato que você conhece o modelo de nome que utilizamos para a gravação dos dados. Com isso, podemos criar uma função que gere uma lista de arquivos a partir de quatro componentes:

  1. Classe
  2. Número inicial
  3. Número final
  4. Path

No módulo dsd.py, criamos a função gerar_lista(), que demanda exatamente essas quatro parâmetros e que retorna uma lista com os nomes de arquivos, a partir do modelo que usamos na gravação.

import dsd

# Parâmetros
Classe = 'ADI'
NumeroInicial = 5000
NumeroFinal = 5500
path = 'ADIhtml//'

lista = dsd.gerar_lista(Classe, NumeroInicial, NumeroFinal,'ADIhtml//')
print (lista)

4. Módulo de extração das informações

4.1 Estrutura dos dados em HTML

Agora que você consegue ler os dados que foram gravadas pelo Extrato e definir com precisão a lista dos arquivos a serem lidos, precisamos desenvolver uma estratégia para retirar de cada um desses arquivos as informações que consideramos relevantes para compor nossos bancos de dados, ou seja, as variáveis que estão ligadas a cada unidade de análise.

Para que essa operação seja possível, é necessário compreender a estrutura dos arquivos que contém os códigos fonte em html.

Observe que, praticamente nas mesmas linhas, os conteúdos que nos interessam estão dispostos dentro da mesma estrutura de formatação, dada pelos parâmetros específicos do HTML (sempre entre <>). Isso não ocorre por acaso: essas páginas são modeladas em HTML, com variáveis que são preenchidas pelos dados específicos de cada processo. Por isso, o seu desafio agora é encontrar "marcadores" adequados, que indiquem com precisão os trechos de texto que deverão ser extraídos.

4.2 Extração do campo incidente

O incidente está localizado na linha 2 e, para extraí-lo, é preciso inicialmente encontrar um Marcador de Início, ou seja, uma sequência de texto que termina exatamente  do início do trecho a ser extraído (ou o mais próximo possível).

O trecho de texto imediatamente anterior ao valor do incidente é:

verProcessoAndamento.asp?incidente=

Embora esse marcador pareça suficientemente complexo para que não haja outros iguais no texto, você precisa garantir abrindo o arquivo ADI5500 (sugiro fazer isso com o Spyder) , e fazer uma busca (Ctrl-F) que tenha como argumento o potencial marcador.

Dois resultados interessam:

  1. a existência apenas de uma ocorrência do marcador no html (melhor saída) ou
  2. o fato de ser a primeira ocorrência do marcador no texto (alternativa viável).

Em ambos os casos, essa situação permite que você utilize o comando find para determinar qual é o ponto do início do marcador, dentro da string contida na variável html.

html.find('verProcessoAndamento.asp?incidente=') 

A função find será usada exaustivamente, então você precisa dominá-la bem.

  1. html é o nome da nossa variável string que contém o conteúdo do código fonte.
  2. html.find() é uma função que busca algo dentro da variável html e retorna o número do caractere no texto em que começa o trecho buscado. Lembre-se que o primeiro caractere de uma string, no Python, é 0 e não 1.
  3. Se não houver texto, essa função retorna -1.

Você provavelmente terá chegado a um código semelhante a esse:

import os, dsd

path = 'ADIhtml\\'
lista = os.listdir(path)

for nomedoarquivo in lista:

    html = dsd.carregar_arquivo(path+nomedoarquivo)
    
    inicio = html.find('verProcessoAndamento.asp?incidente=') 
    
    print (nomedoarquivo + ' Incidente inicia na posição ' + str(inicio))

Agora que conseguimos identificar um marcador de início do campo incidente, precisamos de um marcador de fim. Para isso, usaremos também a função find, mas acrescida de um novo parâmetro: o ponto de início da busca.

Um primeiro candidato o começo da busca é a posição definida na variável inicio. Porém, lembre-se que essa variável indica o começo do marcador encontrado, enquanto o começo do valor do campo classe está logo depois do fim do marcador.

Para ajustar corretamente o início, vamos usar uma estrutura de comando que vai ser repetido várias vezes ao longo do nosso módulo de organização de dados:

inicio = inicio + len(MarcadorInicio)

Essa é uma fórmula que desloca o começo da extração da classe para o lugar correto. Com esse ligeiro ajuste do inicio, podemos definir o ponto final do texto a ser extraído por meio do seguinte comando, que busca o MarcadorFim, a partir da posição inicio.

fim = html.find(MarcadorFim, inicio)

Mas que texto deve funcionar como marcador de fim? Essa é uma escolha mais simples porque esse marcador será buscado a partir da posição inicio, então podemos escolher a primeira sequência de caracteres posterior ao trecho buscado e que não se confunda com ele.

Nesse caso, podemos usar os caracteres '">', pois logo depois dele aparece um longo trecho em branco que pode ter muitos caracteres que não são impressos na nossa visualização: tabulações, quebras de linha, espaços, etc. Por esse motivo, é sempre bom ter um marcador que não envolva esses espaços aparentemente vazios, mas que têm códigos que não se pode ver imediatamente.

Veja que as aspas duplas podem servir como marcador, desde que elas venham dentro de aspas simples, porque isso faz com que o programa interprete as aspas duplas como trecho de uma string.

Para extrair um texto quando você sabe o início e o fim, a maneira mais simples é atribuir à variável a ser extraída (nesse caso, ao campo incidente) o valor da string html, começando do início e terminando no fim, o que pode ser feito pelo comando:

    classe = html[inicio:fim]
import os, dsd

path = 'ADIhtml\\'
lista = os.listdir(path)

for nomedoarquivo in lista:

    html = dsd.carregar_arquivo(path+nomedoarquivo)
    
    # extrai campo incidente
    MarcadorInicio = 'verProcessoAndamento.asp?incidente='
    MarcadorFim = '">'
    
    inicio = html.find(MarcadorInicio) + len(MarcadorInicio)
    fim = html.find(MarcadorFim, inicio)
    incidente = html[inicio:fim]
    
    print (nomedoarquivo + ', incidente: ' + incidente)

4.3 Função dsd.extrair()

Para simplificar esse código, o módulo dsd.py traz uma função dsd.extrair(), que retorna a primeira ocorrência, em uma string, do trecho definido pelos marcadores de início e de fim. A construção dela é basicamente a que usamos acima, mas um pouco mais robusta porque ela retorna 'NA' quando o texto do marcador de início não está presente na fonte dos dados.

Para que a função dsd.extrair() opere, é preciso definir 3 parâmetros, nessa ordem, separando por vírgulas.

  1. fonte de dados, que é uma string
  2. marcador de início
  3. marcador de fim.

Com isso, o nosso programa não fica somente mais simples, mas você encontra uma forma simplificada de extrair novas informações, repetindo esse módulo para novos campos, alterando apenas a definição dos marcadores.

import os, dsd

path = 'ADIhtml\\'
lista = os.listdir(path)

for nomedoarquivo in lista:

    html = dsd.carregar_arquivo(path+nomedoarquivo)
    
    # extrai campo incidente
    MarcadorInicio = 'verProcessoAndamento.asp?incidente='
    MarcadorFim = '">'
    incidente = dsd.extrair(html, MarcadorInicio, MarcadorFim)
    
    print (nomedoarquivo + ', incidente: ' + incidente)

Você pode usar o formato acima, que deixa mais clara a estrutura, mas utilizaremos uma forma mais concisa, com os marcadores inseridos diretamente na função. Nesse caso, convém quebrar a linha para manter a legibilidade.

import os, dsd

path = 'ADIhtml\\'
lista = os.listdir(path)

for nomedoarquivo in lista:

    html = dsd.carregar_arquivo(path+nomedoarquivo)
    
    # extrai campo incidente
    incidente = dsd.extrair (html, 
                             'verProcessoAndamento.asp?incidente=',
                             '">')   
    
    print (nomedoarquivo + ', incidente: ' + incidente)

Tome nota! Você pode quebrar linhas e inserir espaços à vontade, quando você está dentro de uma expressão entre parênteses. Quando os argumentos entre parênteses ficarem maiores que a linha, é mais legível quebrar a linha, mantendo os elementos alinhados com o primeiro parêntese.

4.4 Extração dos campos Classe + Liminar + Número

Agora que você sabe extrair um campo simples, passemos à extração de um campo composto por vários elementos. Você já deve ter notado que os campos Classe, Liminar e Número vêm juntos, em um mesmo bloco de texto. Nesse caso, uma saída razoável é extrair todo o bloco e depois extrair dele os dados.

Iniciemos pela extração do bloco de texto com todas as variáveis concatenadas. Veja, por exemplo, a ADI5000, que tem um bloco com o seguinte texto.

AÇÃO DIRETA DE INCONSTITUCIONALIDADE (Med. Liminar) - 5500

Para extrair esse trecho, basta acrescentar uma linha de comando ao algoritmo anterior:

import os, dsd

path = 'ADIhtml\\'
lista = os.listdir(path)

for nomedoarquivo in lista:

    html = dsd.carregar_arquivo(path+nomedoarquivo)
    
    # extrai campo incidente
    incidente = dsd.extrair (html, 
                             'verProcessoAndamento.asp?incidente=',
                             '">')   
    
    # extrai campos classe + liminar + numero
    cln = dsd.extrair(html, 
                      '<div><h3><strong>', 
                      '</strong>')
    
    dados = (incidente, cln)

    print (dados) 

Agora que conseguimos extrair as informações de classe, liminar e número para a variável cln, precisamos buscar marcadores de início e fim que viabilizem a extração dos valores individuais de cada campo:

AÇÃO DIRETA DE INCONSTITUCIONALIDADE (Med. Liminar) - 5500

Quando observamos a estrutura dessa string, vemos que os elementos aparecem em sequência (Classe, Liminar e Número), sendo que a mais fácil de extrair é o número, porque tem um marcador claro de início: o trecho ' - '.

Todavia, como o número termina na última posição, não existe um marcador claro de fim. Para lidar com essa questão, nossa função dsd.extrair() extrai o texto desde o início do documento quando você insere um espaço vazio ("") no marcador de início. De forma similar, quando o marcador de fim é um "", a função extrai o texto até o final da string.

Sabendo disso, é possível extrair o número com um comando simples, que extrai uma substring da variável cln, começando domarcador espaço + hífen + espaço (' - '), e indo até o fim da string.

numero = extrair (cln, ' - ', '')
Observe! Enquanto a variável 'número' é uma string que aparece sempre, o texto 'Med. Liminar' aparece apenas em alguns processos (em que houve pedido de liminar), sendo que nos outros esse texto é vazio. Por isso, a variável Liminar não é uma string, mas é uma variável binária: ou o pedido está presente ou está ausente.

Para saber se há pedido de liminar ou não na petição inicial, podemos usar um comando if:

    if 'Liminar' in cln:
        liminar = 'sim'
    else:
        liminar = 'não'

A extração da classe gera um pouco mais de dificuldade porque o marcador de fim desse campo admite dois valores, a depender da existência ou não da liminar. Quando há liminar, o marcador de fim da classe é ' (Med', mas quando inexiste liminar, o marcador de fim da classe é ' - '. Essa dependência do valor da liminar faz com que possamos inserir a extração da classe na expressão condicional acima:

    if 'Liminar' in cln:
        liminar = 'sim'
        classe = dsd.extrair(cln, '', ' (Med') 
    else:
        liminar = 'não'
        classe = dsd.extrair(cln, '', ' - ') 

Colocando todos os elementos no mesmo código, temos um algoritmo funcional, que gera uma lista com os dados das adis do diretório ADIhtml.

import os, dsd

path = 'ADIhtml\\'
lista = os.listdir(path)

for nomedoarquivo in lista:

    html = dsd.carregar_arquivo(path+nomedoarquivo)
    
    # extrai campo incidente
    incidente = dsd.extrair (html, 
                             'verProcessoAndamento.asp?incidente=',
                             '">')   
    
    # extrai campos classe + liminar + numero
    cln = dsd.extrair(html, 
                      '<div><h3><strong>', 
                      '</strong>')
    
    # extrai numero
    numero = dsd.extrair (cln, ' - ', '')
    
    # extrai liminar e classe    
    if 'Liminar' in cln:
        liminar = 'sim'
        classe = dsd.extrair(cln, '', ' (Med') 
    else:
        liminar = 'não'
        classe = dsd.extrair(cln, '', ' - ') 
    
    
    dados = (incidente, classe, liminar, numero)

    print (dados) 

5. Módulo de gravação dos dados

5.1 Estrutura do CSV

Agora que você  tem uma extração com múltiplos campos, é preciso desenvolver uma estratégia para gravar essas informações em uma tabela.

Utilizaremos uma estrutura de dados chamada de csv, sigla de Comma Separated Values, que é um arquivo nos quais os dados são organizados na forma de uma tabela e gravados por uma estratégia simpples

  1. Cada linha da tabela é uma linha do seu texto;
  2. Em cada linha do texto, os valores de cada célula são separados por vírgulas.

Com isso, o resultado é um arquivo de formato muito simples, visto que uma tabela complexa pode ser gravada com apenas dois elementos de formatação (quebras de linha e vírgulas), o que permite gravar as informações em um arquivo de texto (.txt).

5.2 CSV no Excel

O csv é um arquivo leve e legível por qualquer tipo de programa de dados. Porém, para abri-lo no Excel, é preciso tomar alguns cuidados, pois o uso do comando Abrir tende a não funcionar, pois todos os campos são colocados na primeira coluna:

Arquivo aberto diretamente no Excel

Para abrir arquivos csv no Excel, o melhor é criar um arquivo novo e usar o comando importar dados de txt ou csv.

Após clicar no obter dados de text/csv, você terá a oportunidade de definir o encoding, que aparece como origem do arquivo. Nessa opção, o mais correto é o UTF-8, mas selecionar a opção nenhum também costuma funcionar. O que você não pode fazer é deixar na opção padrão (1252: Europeu Ocidental), pois esse encoding é incapaz de ler a acentuação em português.

Como delimitador (separador), você deve indicar vírgula, mas isso não oferece dificuldade porque costuma ser o padrão. Por vezes, ocorre de o Excel sugerir tabulações como delimitadores, o que também acarreta uma leitura errada do arquivo.

5.3 Gravando um arquivo CSV

Para as nossas finalidades, o arquivo csv tem a grande vantagem de admitir ser gravado em "camadas", uma linha por vez. Como nossa unidade de análise (ou seja, nossas linhas) é o processo, e cada um dos arquivos analisados corresponde a um processo, nós podemos organizar todas as informações relativas à mesma ação e depois gravar uma linha referente a ele.

Para que tudo funcione bem, basta que respeitemos as regras do CSV: usar as quebras de linha para diferenciar as informações de cada processo, usar as vírgulas para separar os valores e usar sempre a mesma ordem de gravação. Com isso, podemos construir uma tabela, linha a linha.

Para realizar essa gravação, criei no módulo dsd.py uma função específica para gravar cada linha:

dsd.write_csv_line (nomedoarquivo, dados)

Para que essa função opere adequadamente, basta que você insira nela o nome do arquivo que será gravado (algo como ADI.csv), e os dados que serão gravados na linha. Como dados, o mais adequado é você gravar uma lista (cujos elementos são separados por vírgulas), mas também pode se tratar de uma string com as vírgulas nos lugares exatos.

No caso do nosso algoritmo, você pode usar o comando seguinte, porque a variável na qual gravamos as informações de cada processo foi chamada de dados.

dsd.write_csv_line('ADI.csv', dados)

Antes de gravar os dados, é preciso que você determine a gravação do cabeçalho, com os nomes dos campos. Também criei uma função para essa finalidade, que é:

dsd.write_csv_header('ADI.csv', campos, gatilho)

Para que essa função opere adequadamente, é preciso criar uma variável nova (chamada campos), para estabelecer uma lista com os nomes de cada atributo. Essa variável exige um pouco de trabalho manual: copiar no código o texto que vai entre [] na variável dados, e atribuir esse valor à variável campos, trocando os parênteses por aspas (ou seja, transformando a lista em uma string):

    dados = [incidente, classe, liminar, numero]
    campos = 'incidente, classe, liminar, numero'

Essa operação manual decorre do fato de que, para o Python, a variável é somente o nome de um valor. Assim, quando tentamos transformar a lista dados em uma string, com o comando str(dados), o resultado será o conteúdo das variáveis, e não o seu nome. Por isso, é preciso inserir, no próprio código, uma string com os nomes dos campos, para que esses nomes formem o cabeçalho da nossa tabela.

Colocando tudo no mesmo código, você fica com o seguinte Gerador de CSV.

import os, dsd

path = 'ADIhtml\\'
lista = os.listdir(path)

for nomedoarquivo in lista:

    html = dsd.carregar_arquivo(path+nomedoarquivo)
    
    # extrai campo incidente
    incidente = dsd.extrair (html, 
                             'verProcessoAndamento.asp?incidente=',
                             '">')   
    
    # extrai campos classe + liminar + numero
    cln = dsd.extrair(html, 
                      '<div><h3><strong>', 
                      '</strong>')
    
    # extrai numero
    numero = dsd.extrair (cln, ' - ', '')
    
    # extrai liminar e classe    
    if 'Liminar' in cln:
        liminar = 'sim'
        classe = dsd.extrair(cln, '', ' (Med') 
    else:
        liminar = 'não'
        classe = dsd.extrair(cln, '', ' - ') 
    
    # define dados a gravar
    dados = (incidente, classe, liminar, numero)
    campos = 'incidente, classe, liminar, numero'
    
    # grava dados
    dsd.write_csv_header('ADIteste.csv', campos)
    dsd.write_csv_line('ADIteste.csv', dados)
            
    print (dados)
Para aprofundar. Se você quiser entender a caixa de máquinas dessas funções dsd.write_csv_header() e dsd.write_csv_line(), leia o texto Entendendo as funções da biblioteca csv.

6. Tratamento dos dados

6.1 Função replace()

A função acima funciona, e nos retorna resultados como:

('5530775', 'AÇÃO DIRETA DE INCONSTITUCIONALIDADE', 'sim', '5994')
('5531738', 'AÇÃO DIRETA DE INCONSTITUCIONALIDADE', 'sim', '5995')
('5531781', 'AÇÃO DIRETA DE INCONSTITUCIONALIDADE', 'sim', '5996')
('5535174', 'AÇÃO DIRETA DE INCONSTITUCIONALIDADE', 'sim', '5997')
('5535298', 'AÇÃO DIRETA DE INCONSTITUCIONALIDADE', 'sim', '5998')
('5535650', 'AÇÃO DIRETA DE INCONSTITUCIONALIDADE', 'sim', '5999')
('5536310', 'AÇÃO DIRETA DE INCONSTITUCIONALIDADE', 'sim', '6000')

Para reduzir o nome da ação para ADI, podemos usar o comando replace:

    ## tratamento do campo classe
    classe = classe.replace('AÇÃO DIRETA DE INCONSTITUCIONALIDADE', 'ADI')
    classe = classe.replace('ACAO DIRETA DE INCONSTITUCIONALIDADE', 'ADI')

Essa estratégia de replace pode ser usada para fazer pequenos ajustes que melhoram a qualidade dos dados gravados na sua tabela.

Para identificar os ajustes que precisam ser feitos, uma boa ferramenta é a função filtro do Excel. Você pode abrir o arquivo no Excel e abrir o filtro para observar os dados, o que mostra os valores contidos em cada coluna em ordem alfabética ou numérica. Com isso, você pode verificar se os valores estão todos no range adequado, ou se há valores muito desviantes (como classes com nomes sem sentido ou caracteres estranhos aos campos que deveriam conter somente algarismos).

Na medida em que você encontra dados com problemas, você pode inserir no seu código alguns comandos voltados a transformar os dados, promovendo uma espécie de limpeza dos dados (data cleaning). A função replace() é muito útil para essa finalidade, pois há alguns problemas muito comuns que ela pode sanar:

  1. substituir dois espaços por um espaço apenas: replace('  ',' ');
  2. apagar determinados textos (como o título 'Min. '), substituindo-os por uma string vazia: "";
  3. retirar quebras de linha (\n) ou tabulações (\t) de textos maiores, como a legislação impugnada ou a ementa;
  4. substituir textos por siglas.
campo = campo.replace('  ','')
campo = campo.replace('/n','')
campo = campo.replace('/t','')
campo = campo.replace('Min.','')
campo = campo.replace('DISTRITO FEDERAL','DF')
Exemplos úteis de replace

6.2 Função strip()

A função strip() é muito interessante porque, com um argumento vazio, ela retorna um texto excluindo espaços vazios do início ou do final do campo.

campo = campo.strip()

Além disso, você pode incluir como argumento uma cadeia de caracteres, que serão excluídos da primeira ou da última posição. Por exemplo, você pode usar os comandos abaixo para excluir espaços vazios, quebras de linha e hifens, que costumam 'poluir' o início e o fim de dados como ementas, decisões e legislação impugnada.

campo = campo.strip(' ')
campo = campo.strip('\n')
campo = campo.lstrip('-')
Atenção! Para retirar apenas o texto existente no início, use lstrip() (l para left) no lugar de strip(). Para retirar apenas o  texto existente no final, use rstrip().

6.3 Função upper()

A função upper() modifica uma string, transformando todas as letras em maiúsculas.

campo = campo.upper()

O fato de que as buscas do Python são sensíveis à diferença entre maiúsculas e minúsculas faz com que, muitas vezes, seja útil homogeneizar as letras de ementas, andamentos ou legislação, para evitar que a variação entre maiúsculas e minúsculas seja empecilho para uma busca adequada.

Porém, essa sensibilidade também faz com que você tenha de ter o cuidado de não alterar textos que você já tinha analisado antes, visto que o uso do upper() pode modificar marcadores de início ou de fim que você tenha utilizado antes.

6.4 Função dsd.limpar()

Essa função faz repetidos strip() para arrumar o começo do texto, retirando espaços extra e alguns caracteres que identificamos que por vezes aparecem no início de algumas informações.

Além disso, esta função exclui as marcas de parágrafo, operação que reduz a legibilidade dos textos, mas que evita que os parágrafos separem trechos que você deseja buscar dentro da string, o que pode comprometer certas análises.

6.5 Função dsd.limpar_numero()

Essa função foi feita para regularizar os números, visto que identificamos que a extração do campo número traz duas dificuldades que essa limpeza corrige:

  1. em alguns casos, os algarismos vêm junto com certos marcadores de formatação em HTML, que a função retira;
  2. os números da base do controle concentrado não têm os zeros no início, que usamos para homogeneizar o nome dos processos.

7. Completando a extração dos dados

Agora que você tem um extrator funcional, capaz de extrair dados e gerar tabelas, vamos ampliar a seleção para incluir as informações que estão nos nossos arquivos, mas que não extraímos ainda. Comece pela inclusão dos dados que já analisamos antes:

  1. requerente,
  2. requerido,
  3. entrada_data,
  4. distribuicao_data,
  5. relator
  6. requerente
  7. requerido
  8. dispositivoquestionado
  9. resultadoliminar
  10. resultadofinal
  11. decisaomonofinal
  12. fundamento
  13. indexacao

8. Código final

O seguinte código extrai todos os dados definidos no modelo de dados e os grava para um arquivo .csv, com um cabeçalho contendo os campos.

import os, dsd

path = 'ADIhtml\\'
lista = os.listdir(path)

for nomedoarquivo in lista:

    html = dsd.carregar_arquivo(path+nomedoarquivo)
    
    # extrai campo incidente
    incidente = dsd.extrair (html, 
                             'verProcessoAndamento.asp?incidente=',
                             '">')   
    
    # extrai campos classe + liminar + numero
    cln = dsd.extrair(html, 
                      '<div><h3><strong>', 
                      '</strong>')
    
    # extrai numero
    numero = dsd.extrair (cln, ' - ', '')
    numero = dsd.limpar_numero(numero)
    
    # extrai liminar e classe    
    if 'Liminar' in cln:
        liminar = 'sim'
        classe = dsd.extrair(cln, '', ' (Med') 
    else:
        liminar = 'não'
        classe = dsd.extrair(cln, '', ' - ') 
    
    # define dados a gravar
    dados = (incidente, classe, liminar, numero)
    campos = 'incidente, classe, liminar, numero'
    
    # definição de campo: origem     
    origem = dsd.extrair(html,'Origem:</td><td><strong>','</strong>')
    
           
    ## definição de campo: entrada
    entrada = dsd.extrair(html,'Entrada no STF:</td><td><strong>','</strong>')
    
    ## definição de campo: relator
    relator = dsd.extrair(html,'Relator:</td><td><strong>','</strong>')
    relator = relator.replace('MINISTRO','')
    relator = relator.replace('MINISTRA','')
    
    
    ## definição de campo: distribuição
    distribuicao = dsd.extrair(html,'Distribuído:</td><td><strong>','</strong>')
    
    
    ## definição de campo: requerente
    requerente = dsd.extrair(html,'Requerente: <strong>','</strong>')
    if '(CF 103, ' in requerente:
        requerentesplit = requerente.split('(CF 103, ')
        requerente = requerentesplit[0]
        requerente = requerente.strip()
        requerentetipo = requerentesplit[1]
        requerentetipo = requerentetipo.replace(')','')
        requerentetipo = requerentetipo.replace('0','')
    else:
        requerentetipo = 'NA'
    
    ## definição de campo: requerido
    requerido = dsd.extrair(html,
                        'Requerido :<strong>',
                        '</strong>')
    
    ## definição de campo: dispositivo questionado
    dispositivoquestionado = dsd.extrair(html,
                                     'Dispositivo Legal Questionado</b></strong><br /><pre>',
                                     '</pre>')
    dispositivoquestionado = dsd.limpar(dispositivoquestionado)
    
    ## definição de campo: resultado da liminar
    resultadoliminar = dsd.extrair(html,
                                   'Resultado da Liminar</b></strong><br /><br />',
                                   '<br />')
    
    ## definição de campo: resultado final
    resultadofinal = dsd.extrair(html,
                                 'Resultado Final</b></strong><br /><br />',
                                 '<br />')
    
    ## definição de campo: decisão monocrática final
    if 'Decisão Monocrática Final</b></strong><br /><pre>' in html:
        decisaomonofinal = dsd.extrair(html,
                                       'Decisão Monocrática Final</b></strong><br /><pre>',
                                       '</pre>')
        decisaomonofinal = dsd.limpar(decisaomonofinal)
    else: 
        decisaomonofinal = 'NA'
         
    ## definição de campo: fundamento    
    if 'Fundamentação Constitucional</b></strong><br /><pre>' in html:
        fundamento = dsd.extrair(html,
                             'Fundamentação Constitucional</b></strong><br /><pre>',
                             '</pre>')
        fundamento = dsd.limpar(fundamento)
    else:
        fundamento = 'NA'
    
    ## definição de campo: fundamento
    if 'Indexação</b></strong><br /><pre>' in html:
        indexacao = dsd.extrair(html,
                            'Indexação</b></strong><br /><pre>',
                            '</pre>')
        indexacao = dsd.limpar(indexacao)        
    else:
        indexacao = 'NA'
    
    ### criação da variável dados extraídos, com uma lista de dados
    dados = [classe, numero, incidente, liminar, origem, entrada, relator,
             distribuicao, requerente, requerentetipo, requerido, 
             dispositivoquestionado, resultadoliminar, resultadofinal, 
             decisaomonofinal, fundamento, indexacao]
    #inserir aqui o conteúdo da lista acima, trocando [] por ''
    campos = '''classe, numero, incidente, liminar, origem, entrada, relator, 
                distribuicao, requerente, requerentetipo, requerido, 
                dispositivoquestionado, resultadoliminar, resultadofinal, 
                decisaomonofinal, fundamento, indexacao'''

    
    # grava dados
    dsd.write_csv_header('ADI2.csv', campos)
    dsd.write_csv_line('ADI2.csv', dados)
            
    print (dados)  

9. Para seguir adiante

Neste ponto, espera-se que você seja capaz de elaborar bancos de dados contidos em uma única página e cujos campos são formados por strings. Nossa próxima etapa é extrair dados de múltiplas fontes simultâneas e oferecer treinamento para extrair dados complexos (especialmente conjuntos de partes e de andamentos), que precisam ser gravados como listas, e não como strings.