← Voltar Null Comma Jogos, protótipos e insights de desenvolvimento

Uma biblioteca offline de leis federais, com busca semântica no aparelho

Imagem de capa de Uma biblioteca offline de leis federais, com busca semântica no aparelho

Eu queria consultar leis federais no celular do mesmo jeito que consulto na web: abrir o texto, buscar com palavras comuns, ler. Não um banco jurídico com conta e sincronização, nem um chat que resume a lei: o texto literal, offline, e um campo de busca que entende a pergunta.

O Planalto publica o texto integral de toda lei federal brasileira em HTML, de graça, sem API. Então o app monta a própria biblioteca a partir dessas páginas, no aparelho: baixa, faz o parse, guarda e indexa. O resultado é o Lista Leis, um app Android com 121 leis em 17 áreas, no Google Play. Sem backend, sem conta, sem analytics; as únicas chamadas de rede vão para as páginas do Planalto e para o Google Play, na cobrança.

A tela inicial A tela inicial: as quatro leis gratuitas prontas para ler, o resto marcado como Premium.


A fonte é uma página feita para leitura, não para parsing

O catálogo inteiro vem de um único tipo de fonte: uma página HTML por lei, no planalto.gov.br. Não existe feed, nem JSON, nem changelog. Existe uma página escrita para humanos, e o app lê exatamente isso. Três características dessas páginas moldam todo o trabalho de parsing.

A marcação é antiga e irregular. A maioria das páginas é ISO-8859-1; uma delas (Maria da Penha) é servida como UTF-16LE com o último byte cortado no servidor, então o decodificador tem que olhar o BOM antes de assumir uma codificação. A fonte também tem erros de digitação próprios, como um cabeçalho de capítulo corrompido na Lei das Contravenções Penais.

O mesmo artigo pode aparecer duas vezes. Quando uma alteração reescreve um artigo, a página mantém o texto antigo e anexa o novo, marcado “Redação dada pela Lei X”. A ordem de leitura engana, então o parser fica com a última ocorrência de cada número de artigo.

Citações parecem a própria lei. Quando uma lei cita artigos de outra norma, a citação fica dentro de um <blockquote> e contém cabeçalhos como “Art. 1º”. Sozinha, cada citação viraria um artigo fantasma da lei que está sendo lida. O parser remove os blockquotes recursivamente, dos mais internos para fora:

// Citações de outras leis parecem artigos: fora.
// Remove os blocos mais internos primeiro, até a página parar de mudar.
val inner = Regex("""(?is)<blockquote\b[^>]*>(?:(?!</?blockquote\b).)*</blockquote>""")
while (true) {
    val next = inner.replace(out, "")
    if (next == out) return out
    out = next
}

A partir daí, o parser percorre elementos p, h1, h2 e h3, quebra cada um em linhas nos <br> e reconhece três coisas: cabeçalhos de artigo, divisões (PARTE, LIVRO, TÍTULO, CAPÍTULO, SEÇÃO) e epígrafes, a rubrica curta em negrito que nomeia o artigo. O número precisa sobreviver ao que a fonte faz com ele: Art. 42, Art. 1.595 com ponto de milhar, Art. 9º-A de uma alteração, e Art. 1o com o ordinal em <sup>. A base mantém um padrão conservador, e cada lei pode optar por uma exceção quando a própria página faz diferente: o Código Eleitoral escreve Ar. e At., a Lei das Eleições escreve Art sem ponto, e o Código de Processo Penal Militar (CPPM) tem uma linha em negrito (“Parte de deserção”) que parece uma divisão mas é uma nota dentro do artigo. Nenhuma exceção muda como qualquer outra lei é lida.

Existe ainda um marcador de fim de documento: o aviso “Este texto não substitui o publicado no DOU”. Só que nem sempre no fim. A Lei 8.112 tem dois deles, um dentro de uma nota de republicação; só a última ocorrência encerra o documento.

Por isso o app carrega um parser por lei quando precisa. É o mesmo problema repetido em 121 páginas que discordam entre si.

O índice de uma lei O índice da lei: as divisões aparecem como títulos e capítulos, na mesma estrutura da fonte.

O hash decide se algo mudou

Uma biblioteca de 121 páginas só vale a pena conferir se a conferência é barata. Cada download calcula um SHA-256 da página depois de remover os <script>. O Planalto injeta um script anti-bot com payload que muda a cada carregamento; calcular o hash do HTML cru marcaria toda lei como alterada todas as vezes.

O hash guardado também carrega a versão do parser: sha256(stableHtml(html)) + ":p" + parseVersion. Quando a lógica de parsing melhora, o hash da versão anterior deixa de casar e a próxima passada reconstrói a lei, sem migração de banco.

Hash igual significa conteúdo igual, e o app só atualiza a data de “verificado em”. Hash diferente significa refazer o parse: seções e artigos são substituídos dentro de uma transação única, o hash é gravado por último (um crash no meio nunca deixa uma lei marcada como ingerida com zero artigos) e o cache de busca é invalidado. Um parse vazio é tratado como página de erro transitória da fonte, não como uma lei sem artigos: o conteúdo anterior fica e o app tenta de novo depois.

A busca com as palavras que você tem

Um campo de busca que exige a palavra-chave certa não serve para uma biblioteca de leis. Se alguém digita “cobraram duas vezes o mesmo item”, o artigo que responde provavelmente diz “cobrança indevida”. Então cada artigo recebe um embedding no aparelho com um Universal Sentence Encoder, via MediaPipe. O modelo tem cerca de 6 MB e vem dentro do APK, sem download na primeira execução; os vetores ficam como blobs no mesmo banco local do texto, e a consulta também vira um embedding na hora da busca, comparada ao corpus por similaridade de cosseno.

O ramo semântico sozinho seria uma caixa-preta, então o ranking junta quatro sinais:

sinalo que épeso
semânticacosseno entre a consulta e cada embedding de artigoo score base
palavra-chaveLIKE sobre o texto do artigo0,7, com bônus quando acerta a epígrafe
aproximadasimilaridade de Levenshtein entre termos da consulta e tokens do artigoentra a partir de 0,62, soma 0,3
número do artigo“art. 42” reconhecido na consulta2,0, lidera a lista

O caminho do número existe porque as pessoas buscam artigo por número, e um artigo exato nunca deve perder para um parecido por significado. O caminho aproximado é a tolerância a erro de digitação: é o que faz “sussego” encontrar “sossego”.

O resultado sai em duas passadas. O ranking de palavra-chave é emitido primeiro, em dezenas de milissegundos, e o ranking final mesclado o substitui quando o embedding e a passada aproximada terminam. Falha no embedding não quebra a busca: aquele ramo contribui vazio e o resultado sai dos demais. Consultas repetidas caem num cache local com validade de sete dias, guardado por escopo, então uma busca dentro de uma lei é cacheada separada da busca global.

Resultados de busca Busca por “defeito” no Código de Defesa do Consumidor: o caminho das seções aparece acima de cada artigo, e o grupo principal fica separado de “Outros artigos possíveis”.

Ler primeiro, indexar depois

Fazer o parse de uma lei é rápido. Gerar os embeddings dela não é. O app mantém duas filas seriais: baixar e parsear, e indexar. A lei fica legível assim que é parseada, ainda sem vetores; os embeddings chegam em segundo plano. Abrir uma lei a move para a frente das duas filas, e a indexação é interrompida entre artigos, gravando cada vetor conforme calcula, então ela retoma do primeiro artigo que ainda está sem vetor.

Numa instalação nova, as quatro leis gratuitas baixam de imediato; as pagas entram na mesma fila depois que a compra é restaurada.

A tela de artigo O leitor, com o texto literal e o link para a fonte. A nota de rodapé repete o que o texto é: literal, da fonte oficial, e não substitui o Diário Oficial.

Texto público, ferramenta paga

O texto da lei federal brasileira é público: pode ser copiado livremente, e o app o pega da fonte oficial e mostra literalmente. O que o app vende é a ferramenta em volta: a busca no aparelho, a biblioteca offline, o catálogo além das quatro primeiras leis. Essas quatro são gratuitas para todo mundo: Código de Defesa do Consumidor (CDC), Lei das Contravenções Penais (LCP), CLT e Lei Maria da Penha. As outras 117 são liberadas com assinatura mensal ou anual, ou com uma compra vitalícia de fundador. A cobrança passa pelo Google Play, então a compra acompanha a conta Google e se restaura num aparelho novo sem conta dentro do app.

Limites honestos

Três coisas que o app não é:

  • É um leitor, não um parecer. Ele mostra o texto literal com link para a fonte e não resume nem interpreta nada; a publicação no Diário Oficial é o que vale.
  • A fonte pode mudar por baixo dele. Os parsers são testados contra trechos das páginas reais, e uma página que para de parsear mantém o conteúdo anterior no lugar, mas um redesenho no Planalto quebra o parser, e isso não é algo que o celular conserte sozinho.
  • A busca semântica ordena, não responde. Ela pode deixar passar um artigo relevante ou ordenar diferente do que uma pessoa ordenaria; a busca exata por número e o piso de palavra-chave são as garantias, o resto é ranking.

O app é um repositório independente de textos legais, sem vínculo com qualquer órgão do governo.


Se você quiser conferir qualquer coisa que o app mostra, o caminho é o mesmo do código acima: planalto.gov.br mais a página da lei. Qualquer navegador e um view-source resolvem o resto. O mesmo texto está no app, byte a byte, porque é o único tipo de fidelidade que vale prometer.

Projetos relacionados