Files
TextNLPClassifierApp/specs/004-deterministic-content-selection/research.md
T

6.8 KiB

Research & Architectural Decisions: Deterministic Content Selection

Branch: 004-deterministic-content-selection | Date: 2026-08-20 | Spec: spec.md


1. Text Normalization Pipeline

Context

Cada extrator (Trafilatura, Newspaper4k e Readability) gera textos com diferentes resíduos de formatação (entidades HTML como &amp; ou &nbsp;, links no formato markdown [texto](url) ou tags <a href="...">texto</a>, variações de quebras de linha e pontuações). A comparação textual para consenso exige uma normalização uniforme, determinística e de alta performance.

Decisions

  1. Decodificação de entidades HTML: Utilizar html.unescape() da biblioteca padrão do Python.
  2. Remoção de imagens Markdown: Expressão regular re.compile(r'!\s*\[[^\]]*\]\([^)]*\)') substituindo imagens Markdown por espaços para descartar marcação de mídia não-textual e evitar falsos consensos com legendas.
  3. Preservação de texto de links Markdown: Expressão regular re.compile(r'\[([^\]]+)\]\([^)]+\)') substituindo links Markdown pelo texto âncora \1.
  4. Remoção de tags HTML: Expressão regular re.compile(r'<[^>]+>') substituindo tags por espaços para evitar fusão acidental de palavras vizinhas.
  5. Normalização Unicode: unicodedata.normalize('NFKC', text) para uniformizar caracteres compostos, ligaduras e variantes tipográficas.
  6. Conversão para minúsculas: .lower() após NFKC.
  7. Colapso de espaços em branco: re.sub(r'\s+', ' ', text).strip().
  8. Tokenização: Extração de sequências alfanuméricas com re.findall(r'[\w]+', text, flags=re.UNICODE). Pontuações são descartadas naturalmente sem remoção semântica de palavras.

Rationale

  • 100% implementável com módulos padrão do Python (re, unicodedata, html), garantindo portabilidade em qualquer ambiente sem novas dependências externas.
  • Complexidade linear O(N) no tamanho do texto, com execução em frações de milissegundo por artigo.

Alternatives Considered

  • BeautifulSoup para strip de tags: Rejeitado por ser mais lento e desnecessário para textos já extraídos.
  • nltk ou spacy: Rejeitados por adicionarem dependências pesadas, download de modelos e lentidão desnecessária para uma tarefa de tokenização alfanumérica pura.

2. 5-Token Shingles & Consensus Metrics

Context

O algoritmo compara a sobreposição textual entre os candidatos ativos através de janelas deslizantes consecutivas de 5 tokens (shingles).

Decisions

  1. Geração de Shingles:
    • Para um candidato com T tokens ordenados [t_0, t_1, \dots, t_{T-1}]:
      • Se T \ge 5: conjunto de tuplas de 5 tokens \{ (t_i, t_{i+1}, t_{i+2}, t_{i+3}, t_{i+4}) \mid 0 \le i \le T-5 \}.
      • Se 1 \le T \le 4: conjunto contendo uma única tupla com todos os tokens \{ (t_0, \dots, t_{T-1}) \}.
      • Se T = 0: conjunto vazio \emptyset.
  2. Construção do Consenso:
    • Para cada shingle único observado nos candidatos ativos, conta-se em quantos candidatos distintos ele aparece.
    • \text{Consenso} = \{ s \mid \text{contagem}(s) \ge 2 \}.
  3. Métricas por Candidato Ativo $C$:
    • \text{cobertura}(C) = \frac{|C_{\text{shingles}} \cap \text{Consenso}|}{|\text{Consenso}|}
    • \text{suporte}(C) = \frac{|C_{\text{shingles}} \cap \text{Consenso}|}{|C_{\text{shingles}}|}
    • \text{score}(C) = \frac{2 \times \text{cobertura}(C) \times \text{suporte}(C)}{\text{cobertura}(C) + \text{suporte}(C)} (se denominador for zero, \text{score} = 0.0).

Rationale

  • A métrica de pontuação F_1 penaliza tanto extratores que perderam conteúdo essencial (baixa cobertura) quanto extratores que trouxeram excesso de lixo/boilerplate do site (baixo suporte).
  • A representação por set de tuplas em Python permite operações de intersecção (&) com complexidade ótima de tempo O(|C|).

3. Regras de Decisão, Empate Técnico e Desempate Hierárquico

Context

O sistema precisa garantir uma escolha única e determinística em todas as variações possíveis de entrada.

Decisions

  1. Formação do Conjunto Ativo:
    • Classificação:
      • Usável: text é string não vazia após normalização e error é None/vazio.
      • Degradado: text é string não vazia após normalização, mas error não é None.
      • Indisponível: text é nulo, ausente, não-string ou vazio.
    • Se houver \ge 1 Usável \to Ativos = Usáveis.
    • Senão, se houver \ge 1 Degradado \to Ativos = Degradados.
    • Senão \to Seleciona newspaper4k diretamente (Fallback Final).
    • Se |\text{Ativos}| = 1 \to Seleciona o único candidato ativo imediatamente.
  2. Seleção Com Consenso (|\text{Consenso}| > 0):
    • Maior score S_{\max} = \max_{C \in \text{Ativos}} \text{score}(C).
    • Grupo de empate técnico: \{ C \in \text{Ativos} \mid S_{\max} - \text{score}(C) \le 0.03 + 10^{-9} \}.
    • Se grupo tiver 1 candidato \to Seleciona ele.
    • Se grupo tiver \ge 2 candidatos \to Seleciona o candidato com menor |C_{\text{shingles}}| (menor conteúdo excedente).
    • Se ainda houver empate no número de shingles \to Desempate por prioridade fixa: newspaper4k > readability > trafilatura.
  3. Seleção Sem Consenso (|\text{Consenso}| = 0):
    • Se |\text{Ativos}| = 3 \to Seleciona candidato com quantidade mediana de shingles.
    • Se |\text{Ativos}| = 2 \to Seleciona candidato com maior quantidade de shingles.
    • Se |\text{Ativos}| = 1 \to Seleciona o único candidato.
    • Empates na quantidade de shingles \to Prioridade fixa: newspaper4k > readability > trafilatura.

Rationale

  • Total aderência às seções 7.1 a 7.6 do PRD. A tolerância de 10^{-9} evita imprecisões de ponto flutuante em comparações <= 0.03.

4. Estratégia de I/O Não Destrutiva e Escrita Atômica

Context

O processamento em lote deve preservar a ordem dos artigos e todos os campos originais do JSON, gravando o resultado sem risco de corrupção de arquivos em caso de interrupção.

Decisions

  1. Entrada e Saída:
    • Nome padrão de saída: <nome_original_sem_extensão>_selected.json.
    • Suporte a argumento opcional de saída --output / -o.
  2. Gravação Atômica:
    • Gravar os dados em um arquivo temporário no mesmo diretório (<saida>.tmp.<pid>).
    • Executar substituição atômica via os.replace(temp_path, target_path).
  3. Preservação de Conteúdo:
    • Carregar o JSON original em estruturas nativas de dicionário/lista.
    • Inserir a chave selected_extractor diretamente em cada dicionário de artigo.
    • Se selected_extractor já existir na entrada, sobrescrever com o novo valor recalculado.

Rationale

  • Garante integridade absoluta dos dados contra falhas de disco ou encerramentos abruptos.