1. contexto

Em meio aos desafios cotidianos da pandemia do covid-19, dois nudges me fizeram operacionalizar a coleta desse dataset. O primeiro deles foi a abertura de uma issue no repositório do projeto py-lexml-acervo que menciona explicitamente a existência de uma "ficha catalográfica" de normas jurídicas federais disponibilizada pelo próprio projeto LexML. O segundo "empurrãozinho" trata-se da abertura de um curso de aprendizado de máquina ministrado pelo Erick Muzart e um dos pré-requisitos da inscrição é a submissão de um pré-projeto como evidenciado abaixo:

Na Justificativa de inscrição, sugere-se que o candidato apresente o esboço de um problema que enfrenta e possíveis caminhos que vislumbra para sua solução, em poucas linhas.

Esse conjunto de metadados disponibilizado no formato JSON-LD, adiciona novas informações aos que eu já havia coletado na api do acervo do projeto LexML, abaixo um exemplo dos campos disponíveis para um decreto.

Metadados do Decreto nº 10.217 de 30/01/2020

[
    {
        "legislationPassedBy": {
            "@type": "GovernmentOrganization",
            "name": "Congresso Nacional",
            "@id": "https://pt.wikipedia.org/wiki/Congresso_Nacional_do_Brasil"
        },
        "keywords": "ALTERAÇÃO, ORGANIZAÇÃO ADMINISTRATIVA, QUADRO DEMONSTRATIVO, REMANEJAMENTO, CARGO EM COMISSÃO, FUNÇÃO GRATIFICADA, FUNÇÃO COMISSIONADA DO PODER EXECUTIVO (FCPE), AMBITO, COMISSÃO DE VALORES MOBILIARIOS, MINISTERIO DA ECONOMIA",
        "@type": "Legislation",
        "legislationIdentifier": "urn:lex:br:federal:decreto:2020-01-30;10217",
        "about": [
            {
                "@type": "Thing",
                "name": "EXECUTIVO"
            },
            {
                "@type": "Thing",
                "name": "ORGANIZAÇÃO ADMINISTRATIVA"
            },
            {
                "@type": "Thing",
                "name": "PESSOAL"
            }
        ],
        "inLanguage": "pt-BR",
        "description": "Altera o Decreto nº 6.382, de 27 de fevereiro de 2008, que aprova a Estrutura Regimental e o Quadro Demonstrativo dos Cargos em Comissão e das Funções Gratificadas da Comissão de Valores Mobiliários, e remaneja cargos em comissão e funções de confiança.",
        "legislationChanges": [
            {
                "@type": "Legislation",
                "description": "Decreto nº 6.382 de 27/02/2008 (alteração permanente)",
                "@id": "http://www.lexml.gov.br/urn/urn:lex:br:federal:decreto:2008-02-27;6382"
            },
            {
                "@type": "Legislation",
                "description": "Decreto nº 8.965 de 19/01/2017 (alteração permanente)",
                "@id": "http://www.lexml.gov.br/urn/urn:lex:br:federal:decreto:2017-01-19;8965"
            },
            {
                "@type": "Legislation",
                "description": "Decreto nº 9.436 de 03/07/2018 (alteração permanente)",
                "@id": "http://www.lexml.gov.br/urn/urn:lex:br:federal:decreto:2018-07-03;9436"
            }
        ],
        "alternateName": [
            "DEC-10217-2020-01-30"
        ],
        "spatialCoverage": {
            "address": {
                "addressCountry": "BR",
                "@type": "PostalAddress"
            },
            "@type": "Country",
            "name": "Brasil",
            "@id": "https://pt.wikipedia.org/wiki/Brasil"
        },
        "encoding": [
            {
                "contentUrl": "http://legis.senado.leg.br/legislacao/ListaTextoSigen.action?norma=31912090&id=31912097&idBinario=31912101&mime=application/rtf",
                "@type": "LegislationObject",
                "name": "Publicação Original [Decreto nº 10.217 de 30/01/2020]  [Diário Oficial da União  de 31/01/2020] (p. 34, col. 2)",
                "legislationLegalValue": "UnofficialLegalValue",
                "@id": "http://legis.senado.leg.br/legislacao/ListaTextoSigen.action?norma=31912090&id=31912097&idBinario=31912101&mime=application/rtf",
                "fileFormat": "http://www.iana.org/assignments/media-types/text/html"
            }
        ],
        "sdDatePublished": "2020-02-02",
        "sdLicense": "http://creativecommons.org/licenses/by/4.0/",
        "@context": "http://schema.org/",
        "legislationJurisdiction": {
            "address": {
                "addressCountry": "BR",
                "@type": "PostalAddress"
            },
            "@type": "Country",
            "name": "Brasil",
            "@id": "https://pt.wikipedia.org/wiki/Brasil"
        },
        "sdPublisher": {
            "@type": "GovernmentOrganization",
            "name": "Rede de Informação Legislativa e Jurídica Brasileira",
            "@id": "https://www.lexml.gov.br/"
        },
        "datePublished": "2020-01-31",
        "license": "http://creativecommons.org/licenses/by/4.0/",
        "dateCreated": "2020-01-30",
        "legislationDate": "2020-01-30",
        "name": "Decreto nº 10.217 de 30/01/2020",
        "legislationType": "https://pt.wikipedia.org/wiki/Decreto_legislativo",
        "@id": "http://www.lexml.gov.br/urn/urn:lex:br:federal:decreto:2020-01-30;10217"
    }
]

Esses dados são disponibilizados ao acessar a urn de um determinado normativo no domínio do site LexML, como exemplificado aqui. Como eu já havia coletado os dados em xml do acervo do LexML eu já tinha os registros das urns de normas federais que seriam necessárias para iniciar a coleta desses dados adicionais.

2. script

A publicação dos dados do acervo foram realizadas em JSON por ser um formato mais amplamente utilizado nos dias de hoje e, portanto, o escolhemos para coletar todas as urns que precisamos.

# importamos todas as bibliotecas necessárias
import bs4
import requests
from requests.exceptions import ConnectionError
from pathlib import Path
import json
from json.decoder import JSONDecodeError
from time import sleep
from loguru import logger
from tinydb import TinyDB, Query
from typing import Iterable
import re
import gc

Definimos a raiz do domínio do portal LexML onde serão feitas as requisições e armazenamos todos os arquivos json que serão utilizados para coletar as urns que serão utilizadas nas requisições.

urns = Path("./data/federal/")
urns = urns.rglob("*.json")
LOG_FILE = "./log/coleta_metadados_normativos.log"
logger.add(
    LOG_FILE, format="{time:YYYY-MM-DD HH:mm:ss} | {file} {level} | {line} | {message}"
)

Em seguida podemos iterar nos arquivos, coletar as urns de cada objeto e assim fazer a chamada da função get_metadata que será responsável pela coleta dos dados.

urn_retry = {} #container para armazenar uma lógica para retry de requisições 
for file_ in urns:
    with open(file_, encoding="utf8") as f:
        dados_acervo = json.load(f)
        lista_urns = [u.get("urn") for u in dados_acervo]
        for index, urn in enumerate(lista_urns):
            get_metadata(urn=urn, **{"foldername": file_, "index": index})

A função get_metadata será responsável por realização uma requisição get a url formada pela BASE_URL concatenada a urn dos normativos, o objeto alvo da nossa coleta é um json que encontra-se encapsulado em uma tag <script>. Abaixo, apresentamos o código da função.

def get_metadata(urn: str, **kwargs):
    BASE_URL = "https://www.lexml.gov.br/urn"
    global urn_retry
    if urn not in urn_retry:
        urn_retry[urn] = 0
    foldername = kwargs.get("foldername")
    index = kwargs.get("index")
    sleep(0.5)
    try:
        urn_retry[urn] += 1
        r = requests.get(f"{BASE_URL}/{urn}")
    except ConnectionError:
        sleep(30)
        if urn_retry[urn] > 5:
            logger.critical(
                f"O programa será encerrrado por excesso de requests para a mesma urn."
            )
            exit()
        else:
            get_metadata(urn=urn, **{"foldername": file_, "index": index})
    else:
        if r.status_code == 200:
            soup = bs4.BeautifulSoup(r.text, features="html.parser")
            scripts = soup.find_all("script")
            tag_script = [
                str(s) for s in scripts if re.search("application\/ld\+json", str(s))
            ]
            if tag_script:
                tag_script = tag_script[0]
                first_slice = tag_script.find("[")
                last_slice = tag_script.find("</script>")
                try:
                    metadata = json.loads(tag_script[first_slice:last_slice])
                except JSONDecodeError:
                    try:
                        first_slice = tag_script.find("{")
                        last_slice = tag_script.find("</script>")
                        metadata = json.loads(
                            "[" + tag_script[first_slice:last_slice] + "]"
                        )
                    except JSONDecodeError:
                        logger.error(f"Erro na conversão em JSON da urn {urn}.")
                        pass
                    else:
                        save_file(
                            data=metadata,
                            filename=index,
                            foldername=foldername,
                            **{"urn": urn},
                        )
                else:
                    save_file(
                        data=metadata,
                        filename=index,
                        foldername=foldername,
                        **{"urn": urn},
                    )

            else:
                logger.info(f"A urn {urn} não possui metadados.")
        else:
            logger.error(f"Requisição para urn {urn} com status code {r.status_code}.")

Por último, mas não menos importante, implementamos a função save_file que será responsável por persistir em disco nossos dados.

def save_file(data: dict, filename: str, foldername: str, **kwargs):
    urn = kwargs.get("urn")
    get_year_folder = str(foldername).split("_")[-1].split(".")[0]
    BASE_PATH = Path(f"./data/metadados/{get_year_folder}")
    BASE_PATH.mkdir(parents=True, exist_ok=True)
    BASE_PATH = BASE_PATH / f"{get_year_folder}_{filename}.json"
    try:
        with open(BASE_PATH, mode="w", encoding="utf8") as _:
            json.dump(data, _, ensure_ascii=False)
    except ValueError:
        logger.error(f"Erro no dump para urn {urn}.")
    else:
        logger.success(f"Metadados da urn {urn} salvo com sucesso.")

Agora, podemos agregar os dados em uma solução de banco de dados leve orientada a documentos como o Tinydb de forma que podemos consultar os dados de maneira estruturada. Primeramente, vamos estruturar uma função que irá gravar os dados no banco.

def insert_into_db(list_of_files: Iterable, db: TinyDB, key: str) -> None:
    container = []
    for f in list_of_files:
        if len(container) > 500000:
            db.insert_multiple(container)
            container = []
            gc.collect()
        with open(f, "r", encoding="utf8") as _:
            data = json.load(_)
            for d in data:
                if key == "metadados":
                    try:
                        d["keywords"] = d["keywords"].split(",")
                    except KeyError:
                        print(
                            f"Não foi possível encontrar o campo keywords no arquivo {f}."
                        )

                container.append(d)

    db.insert_multiple(container)

Por fim, podemos agora listar os arquivos a serem inseridos no banco e fazer uma chamada a função recém criada. Além disso, os metadados do acervo do portal LexML haviam sido publicados no kaggle de forma desagregada, isto é, foi disponibilizados vários arquivos json. Portanto, vamos organizá-los em um único arquivo de banco de dados para facilitar o seu uso e consulta.

db_metadados = TinyDB("./data/db/metadados.json") #instância do tinydb para os metadados de normas federais
files_metadados = Path("./data/metadados/")
files_metadados = files_metadados.rglob("*.json") #arquivos json dos metadados das normas federais

db_acervo = TinyDB("./data/db/acervo.json") #instância do tinydb para os dados do acervo LexML.
files_acervo = Path("./data/json/")
files_acervo = files_acervo.rglob("*.json") #arquivos json do acervo LexML

params = {
    "metadados": (db_metadados, files_metadados),
    "acervo": (db_acervo, files_acervo)
}

for key, data in params.items():
    insert_into_db(list_of_files=data[1], db=data[0], key=key)

Uma vez carregado os dados, podemos conferir quantos normativos federais tiveram seus metadados coletados, e assim chegamos a informação do número de 34.352 normas.

total_normativos = db_metadados.all()

Para exemplificar o uso da api de query do TinyDB podemos determinar quantas normas foram publicadas no ano de 2020 e concluir que foram ao todo 472.

norma = Query()
normas_2020 = db_metadados.search(norma['datePublished'].matches('(2020)-[0-9]+-[0-9]+'))
num_normas_2020_pubs = len(normas_2020)

Abaixo, apresentamos o resultado dos metadados referentes ao Decreto nº 10.197 de 02/01/2020 que é o primeiro registro do resultado da query acima.

{
    "legislationPassedBy": {
        "@type": "GovernmentOrganization",
        "name": "Congresso Nacional",
        "@id": "https://pt.wikipedia.org/wiki/Congresso_Nacional_do_Brasil"
    },
    "keywords": [
        "ALTERAÇÃO",
        "NORMAS",
        "SISTEMA",
        "INTERNET",
        "INTEGRAÇÃO",
        "ATUAÇÃO",
        "ORGÃO PUBLICO",
        "ADMINISTRAÇÃO DIRETA",
        "ENTIDADE",
        "ADMINISTRAÇÃO INDIRETA",
        "AMBITO",
        "UNIÃO FEDERAL",
        "OBJETIVO",
        "RESOLUÇÃO",
        "CONFLITO",
        "CONSUMIDOR"
    ],
    "@type": "Legislation",
    "legislationIdentifier": "urn:lex:br:federal:decreto:2020-01-02;10197",
    "about": [
        {
            "@type": "Thing",
            "name": "DEFESA DO CONSUMIDOR"
        }
    ],
    "inLanguage": "pt-BR",
    "description": "Altera o Decreto nº 8.573, de 19 de novembro de 2015, para estabelecer o Consumidor.gov.br como plataforma oficial da administração pública federal direta, autárquica e fundacional para a autocomposição nas controvérsias em relações de consumo.",
    "legislationChanges": [
        {
            "@type": "Legislation",
            "description": "Decreto nº 8.573 de 19/11/2015 (alteração permanente)",
            "@id": "http://www.lexml.gov.br/urn/urn:lex:br:federal:decreto:2015-11-19;8573"
        }
    ],
    "alternateName": [
        "DEC-10197-2020-01-02"
    ],
    "spatialCoverage": {
        "address": {
            "addressCountry": "BR",
            "@type": "PostalAddress"
        },
        "@type": "Country",
        "name": "Brasil",
        "@id": "https://pt.wikipedia.org/wiki/Brasil"
    },
    "encoding": [
        {
            "contentUrl": "http://legis.senado.leg.br/legislacao/ListaTextoSigen.action?norma=31900545&amp;amp;id=31900552&amp;amp;idBinario=31900556&amp;amp;mime=application/rtf",
            "@type": "LegislationObject",
            "name": "Publicação Original [Decreto nº 10.197 de 02/01/2020]  [Diário Oficial da União  de 03/01/2020] (p. 1, col. 1)",
            "legislationLegalValue": "UnofficialLegalValue",
            "@id": "http://legis.senado.leg.br/legislacao/ListaTextoSigen.action?norma=31900545&amp;amp;id=31900552&amp;amp;idBinario=31900556&amp;amp;mime=application/rtf",
            "fileFormat": "http://www.iana.org/assignments/media-types/text/html"
        }
    ],
    "sdDatePublished": "2020-01-05",
    "sdLicense": "http://creativecommons.org/licenses/by/4.0/",
    "@context": "http://schema.org/",
    "legislationJurisdiction": {
        "address": {
            "addressCountry": "BR",
            "@type": "PostalAddress"
        },
        "@type": "Country",
        "name": "Brasil",
        "@id": "https://pt.wikipedia.org/wiki/Brasil"
    },
    "sdPublisher": {
        "@type": "GovernmentOrganization",
        "name": "Rede de Informação Legislativa e Jurídica Brasileira",
        "@id": "https://www.lexml.gov.br/"
    },
    "datePublished": "2020-01-03",
    "license": "http://creativecommons.org/licenses/by/4.0/",
    "dateCreated": "2020-01-02",
    "legislationDate": "2020-01-02",
    "name": "Decreto nº 10.197 de 02/01/2020",
    "legislationType": "https://pt.wikipedia.org/wiki/Decreto_legislativo",
    "@id": "http://www.lexml.gov.br/urn/urn:lex:br:federal:decreto:2020-01-02;10197"
}

Para apresentar mais um exemplo de query podemos buscar todos os normativos que possuem a tag INTERNET no campo keywords. E chegamos ao resultado que há 40 normativos que tiveram a tag INTERNET atribuída ao seu domínio.

tag_internet = db_metadados.search(norma['keywords'].any(['INTERNET']))

3. publicação do dataset

Project image

O dataset está diposnível no repositório do py-lexml-acervo no kaggle com o nome de metadados.json com 93.75 MB.

4. aplicação em aprendizado de máquina

Ao meu ver o primeiro uso desse dataset para fins de machine learning é um problema de classificação multi-label. Para cada normativo, temos um campo keywords que recebe múltiplas tags que estão relacionadas ao assunto dessa norma jurídica. Além disso, temos o campo about.description que registra a referida ementa.

Sabido que esse processo de tagging é, geralmente, realizado por meio de anotação manual, isto é, um profissional do meio jurídico ou alguém atue com publicação de normativos legais irá decidir quais tags são mais apropriadas a esse documento legal. Essa sistemática de trabalho, possui algumas desvantagens, podendo-se citar algumas, dentre as quais:

  • Sujeito a falhas, como a maior parte dos procedimentos manuais;
  • Falta de uniformidade na classificação;
  • Necessidade de uma equipe multidisciplinar disponível para anotação;
  • Para o resultado da classificação ser acurado e satisfatório, exige-se um alto custo de homem-hora;

As características elencadas acima, já mostram razão suficiente para que a tecnologia, por meio de um modelo de aprendizado de máquina auxilie os profissionais no desempenho de suas funções.

O papel da tecnologia no mundo jurídico foi recentemente abordada no podcast, hipsters ponto tech 👾.

Assim, com o dataset apresentado acreditamos ter um potencial para o desenvolvimento de um data product que poderia ser acoplado a um sistema pré-existente de publicação de atos normativos. Assim, o usuário abdicaria da responsabilidade de inserir manualmente as tags mais apropriadas, e o sistema faria a consulta a API do modelo de aprendizado de máquina e o seu front-end já apresentaria a sugestão das tags mais apropriadas para a norma em análise. Assim, a responsabilidade do usuário seria apenas de validar a sugestão do modelo, de modo a desonerar parte do trabalho intelectual do profissional.

Portanto, a proposição é a criação de um modelo de classificação multi-label, treinando-o sobre as ementas de cada norma. Nas próximas postagens, esperamos continuar explorando esses dados e iniciar o processo de treinamento do modelo. Até mais! 😀