Files
TextNLPClassifierApp/README.md
T

28 KiB

🧠 TextNLPClassifierApp

Python Version License Code Style Type Checked Tests

Plataforma modular em Python para Classificação Multilíngue de Inerência de Entidades (NLP/LLM/ECP), Extração Inteligente de Manchetes (Google News RSS & Foxcape), Extração Multimotor de Artigos (Trafilatura, Newspaper4k, Readability) e Seleção Determinística de Conteúdo por Consenso Textual (F_1 Shingles).


📑 Tabela de Conteúdos


🌟 Visão Geral

O TextNLPClassifierApp reúne um ecossistema completo de ferramentas de engenharia de dados e processamento de linguagem natural:

  1. classify.py: Motor de classificação semântica e contextual que determina o grau de aderência e inerência de um documento Markdown em relação a uma entidade alvo descrita em um ECP Snapshot (Entity Context Profile).
  2. scripts/extract_google_news.py: Extrator de notícias por palavra-chave, idioma e região geográfica utilizando navegação stealth Foxcape (headless), decodificação paralela de URLs para links reais e feedback em tempo real.
  3. scripts/extract_article_contents.py: Extrator e parser de artigos multimotor com navegação stealth Foxcape headless e extração simultânea via Trafilatura, Newspaper4k (NLP) e Readability, consolidando texto higienizado, autores, datas, imagens e resumos.
  4. scripts/select_article_extractor.py: Motor determinístico de seleção de extratores que avalia as saídas dos três motores, aplica normalização em memória, calcula métricas de consenso de shingles (5-tokens) com pontuação F_1, desempata tecnicamente (\le 0.03) favorecendo menor concisão/ruído e enriquece os dados de forma não-destrutiva e atômica.
  5. scripts/convert_article_to_markdown.py: Conversor determinístico que recebe o JSON de um único artigo selecionado, isola estritamente o corpo do extrator vencedor (trafilatura, newspaper4k ou readability), resolve metadados editoriais por prioridade estrita, higieniza links/imagens/cabeçalhos e gera um documento Markdown (.md) padronizado com gravação atômica transacional.

⚙️ Instalação e Setup

1. Clonar o Repositório e Criar Ambiente Virtual

git clone <URL_DO_REPOSITORIO>
cd TextNLPClassifierApp

python -m venv .venv
# Windows (PowerShell)
.venv\Scripts\Activate.ps1
# Linux/macOS
source .venv/bin/activate

2. Instalar Dependências

pip install -r requirements.txt

3. Baixar Binários do Navegador Stealth (Camoufox)

O motor Foxcape utiliza o navegador customizado Camoufox para evasão avançada de fingerprinting:

python -m camoufox fetch

1. 🧠 Classificador de Conteúdo e Inerência (NLP / LLM / ECP)

O que é e Como Funciona

O classificador avalia se um texto em Markdown trata centralmente, contextualmente ou apenas de forma superficial de uma determinada entidade alvo (ex: empresa, figura pública, clube, conceito). A entidade é descrita através de um ECP Snapshot (Entity Context Profile) contendo nomes canônicos, apelidos (aliases), âncoras temáticas, âncoras negativas e entidades de contexto relacional.

O sistema suporta nativamente 6 idiomas: Português (pt), Inglês (en), Espanhol (es), Francês (fr), Alemão (de) e Italiano (it).

Arquitetura de Classificação em 3 Tiers

flowchart TD
    MD[Markdown Content] --> Pre[Pré-processamento & Detecção de Idioma]
    ECP[ECP Snapshot JSON] --> Pre
    Pre --> T1[Tier 1: Determinístico & Regras NLP]
    T1 -- Alta Confiança --> Decision[Decisão Final JSON]
    T1 -- Score Intermediário / Ambíguo --> T2{Tier 2 Habilitado?}
    T2 -- Sim --> Emb[Tier 2: Similaridade Semântica / Embeddings]
    T2 -- Não --> Decision
    Emb -- Inconclusivo --> T3{Tier 3 Habilitado?}
    T3 -- Sim --> LLM[Tier 3: LLM Inherence Adapter]
    T3 -- Não --> Decision
    LLM --> Decision
  • Tier 1 (Determinístico / NLP Leve): Análise de frequência de termos, detecção de âncoras temáticas no primeiro terço do documento, contagem de aliases e penalização por âncoras negativas.
  • Tier 2 (Vetorial / Embeddings) (Opcional: --enable-embeddings): Projeção vetorial e cálculo de cosseno entre o perfil da entidade e os parágrafos do documento.
  • Tier 3 (LLM Fallback) (Opcional: --enable-llm): Adaptador de desambiguação inteligente (src/adapters/llm.py) acionado exclusivamente para casos limiares e ambíguos (ex: menção isolada TANGENTIAL ou confiança < 0.60). Casos claros não chamam o LLM para economizar custos e latência; em caso de falha de conexão com a API, degrada graciosamente mantendo o resultado do Tier 1 com aviso registrado em warnings.

Categorias de Decisão

Categoria Descrição
DIRECT_INHERENT O conteúdo é centrado e focado diretamente na entidade alvo.
CONTEXTUAL_INHERENT A entidade é relevante no contexto da discussão, mesmo dividindo foco com outros temas.
TANGENTIAL A entidade é mencionada apenas de passagem ou em listas ilustrativas.
NOT_RELATED O conteúdo não possui relação substantiva com a entidade alvo.

Formato do ECP Snapshot e Markdown

Exemplo de ECP Snapshot Real (examples/ecp_club_atletico_river_plate.json):

{
  "target_entity_id": "ecp_river_plate",
  "target_name": "Club Atlético River Plate",
  "canonical_name": "Club Atlético River Plate",
  "domain": "Futebol / Esportes",
  "aliases": ["Club Atlético River Plate", "River Plate", "River", "El Millonario", "La Banda", "CARP"],
  "anchors": ["Monumental", "Copa Libertadores", "Copa Sudamericana", "Eduardo Coudet", "Nicolás Otamendi"],
  "negative_anchors": ["River Plate de Montevideo", "River Plate de Asunción", "Rio da Prata"],
  "graph_version": "1.0.0",
  "related_entities": [
    {
      "entity_id": "boca_juniors",
      "name": "Club Atlético Boca Juniors",
      "relation_type": "RIVAL_OF",
      "weight": 0.9,
      "aliases": ["Boca Juniors", "Boca", "Xeneize"]
    },
    {
      "entity_id": "estadio_monumental",
      "name": "Estadio Mâs Monumental",
      "relation_type": "HOME_VENUE_OF",
      "weight": 0.95,
      "aliases": ["Monumental", "El Monumental"]
    }
  ]
}

Perfis de exemplo prontos para uso em examples/:

Exemplos de Uso CLI

# Classificação Determinística padrão (Tier 1)
python classify.py \
  --ecp examples/ecp_club_atletico_river_plate.json \
  --content out/markdown/meu_artigo_001.md

# Salvar resultado em arquivo JSON formatado
python classify.py \
  --ecp examples/ecp_club_atletico_river_plate.json \
  --content out/markdown/meu_artigo_001.md \
  --output out/classification_001.json

# Habilitar camadas adicionais (Embeddings e LLM)
python classify.py --ecp ecp.json --content artigo.md --enable-embeddings --enable-llm

2. 📰 Extrator de Manchetes do Google News

O que é e Como Funciona

O script scripts/extract_google_news.py é um extrator CLI autônomo projetado para consultar o feed RSS do Google News com máxima velocidade, resiliência e integridade de dados.

Diferenciais Técnicos

  • 🛡️ Foxcape Headless Anti-Bot: Utiliza o motor foxcape com FoxcapeConfig(headless=True) e Camoufox para evitar bloqueios 429/403 e captchas em segundo plano sem abrir navegadores visuais.
  • 🔗 Resolução Automática de URLs Reais: Decodifica em paralelo (ThreadPoolExecutor) as URLs intermediárias do Google News (news.google.com/rss/articles/...) entregando diretamente o link final do portal de notícia (Olé, TyC Sports, ge, ESPN, BBC, etc.).
  • 🧹 Higienização de HTML: Sanitiza resumos e descrições, eliminando tags HTML residuais (<ol>, <li>, <a>, <span>).
  • 📊 Logging Informativo no Terminal: Emite o status passo a passo no canal stderr sem poluir a saída JSON em stdout (100% compatível com pipes e jq).
  • 🌎 Mapeamento de Idioma & Região: Mapeia automaticamente códigos regionais (pt → BR:pt-BR, es → AR:es-419, en → US:en-US) permitindo customização com --locale.

Argumentos e Flags de Linha de Comando

Flag Tipo Obrigatório Padrão Descrição
-q, --query, --keyword str Sim — Termo de pesquisa (ex: "River Plate").
-l, --lang, --language str Não "pt" Código do idioma (pt, es, en, de, fr, it).
--locale, --country str Não None País/região editorial (BR, AR, MX, US, GB, ES).
-p, --max-pages int Não 1 Quantidade de páginas (1 a 10; cada página traz 10 itens).
-o, --output str Não None Caminho para salvar o arquivo JSON.
--pretty flag Não False Formata o JSON no terminal com indentação de 2 espaços.
--no-resolve-urls flag Não False Desativa a resolução e mantém as URLs brutas do Google News.
-s, --silent, --quiet flag Não False Suprime os logs de progresso no stderr.

Exemplos Práticos de Uso

# River Plate (Argentina / Espanhol / 2 Páginas / Salvar em Arquivo)
python scripts/extract_google_news.py -q "River Plate" -l es --locale AR -p 2 -o out/river_plate.json

# Cruzeiro (Brasil / Português / Formatado no Terminal)
python scripts/extract_google_news.py --query "Cruzeiro" --lang pt --locale BR --pretty

# Filtragem com jq em modo silencioso
python scripts/extract_google_news.py -q "inteligência artificial" -s | jq '.items[].url'

3. 📄 Extrator e Parser Multimotor de Artigos

Visão Geral e Tríplice Extração

O script scripts/extract_article_contents.py lê os arquivos JSON gerados pelo extrator do Google News (ou qualquer lista contendo items com url), acessa cada página via Foxcape em modo stealth headless (reutilizando uma única sessão de navegador ativa com espera do evento domcontentloaded), e executa simultaneamente 3 motores especializados de extração:

  1. Trafilatura: Texto principal higienizado, autores, data de publicação, categorias, tags, URL canônica e payload estruturado nativo.
  2. Newspaper4k: Artigo completo, autores, imagens (top_image e galeria), resumo automático e palavras-chave (keywords) extraídas por NLP nativo.
  3. Readability (readability-lxml): Miolo limpo em HTML sem anúncios ou scripts supérfluos, títulos e texto puro formatado.

O JSON final consolidado é salvo em out/ com descarte de strings HTML brutas para manter o arquivo leve e veloz.

Argumentos e Flags CLI

Parâmetro Tipo Padrão Descrição
-i, --input Caminho (obrigatório) — Arquivo JSON de busca de notícias (ex: out/river_plate.json).
-o, --output Caminho (opcional) <input_stem>_extracted.json Arquivo JSON de destino consolidado.
-l, --limit Inteiro (opcional) Todos Limita a quantidade máxima de notícias processadas.
--lang, --language String (opcional) Do JSON / en Sobrescreve o código de idioma para o NLP do Newspaper4k (ex: pt, es, en).
-t, --timeout Inteiro (opcional) 30 Timeout em segundos por página no Foxcape.
-s, --silent Flag booleana False Suprime logs informativos de progresso no stderr.

Exemplos de Uso

# Extração Completa Automática (gera out/river_plate_extracted.json)
python scripts/extract_article_contents.py -i out/river_plate.json

# Amostragem Rápida (Limit 2 Notícias)
python scripts/extract_article_contents.py -i out/river_plate.json --limit 2

# Destino Customizado e Timeout Ajustado
python scripts/extract_article_contents.py -i out/petrobras_result.json -o out/petrobras_full.json --timeout 45

4. 🎯 Seletor Determinístico de Conteúdo de Artigos

Visão Geral e Algoritmo de Consenso (F_1)

O script scripts/select_article_extractor.py é uma ferramenta autônoma, leve e 100% determinística (baseada exclusivamente na biblioteca padrão do Python) que resolve o problema de divergência entre múltiplos extratores de texto.

O motor analisa as saídas dos candidatos ativos (trafilatura, newspaper4k e readability), compara a sobreposição de conteúdo através de shingles de 5 tokens e calcula o índice de concordância mútua via pontuação F_1:

\text{Coverage} = \frac{|\text{Shingles do Candidato} \cap \text{Consenso}|}{|\text{Consenso}|} \text{Support} = \frac{|\text{Shingles do Candidato} \cap \text{Consenso}|}{|\text{Shingles do Candidato}|} F_1 = \frac{2 \times \text{Coverage} \times \text{Support}}{\text{Coverage} + \text{Support}}
flowchart TD
    Art[Artigo com Trafilatura, Newspaper4k e Readability] --> Class[Classificação de Viabilidade: Usable, Degraded, Unavailable]
    Class --> Set[Formação do Conjunto Ativo]
    Set --> Norm[Normalização NFKC & Shingles de 5 Tokens]
    Norm --> Metric[Cálculo de Consenso & Métricas F1]
    Metric --> Check{Existe Consenso >= 2?}
    Check -- Sim --> TopScore[Avaliação do Top Score]
    TopScore --> TiePool{Empate Técnico <= 0.03?}
    TiePool -- Sim --> SmallestShingle[Menor Quantidade de Shingles / Menos Ruído]
    SmallestShingle --> Winner[Extrator Selecionado]
    TiePool -- Não --> HighestScore[Maior Pontuação F1]
    HighestScore --> Winner
    Check -- Não --> ZeroCons[Desempate Sem Consenso: Mediana / Max / Prioridade]
    ZeroCons --> Winner

Pipeline de Normalização e Shingles

A normalização ocorre em memória exclusivamente para fins comparativos:

  1. Decodificação de entidades HTML (html.unescape).
  2. Descarte de imagens Markdown (![alt](url)) para evitar que descrições de imagens criem falsos consensos com legendas.
  3. Preservação de links Markdown ([texto](url) \to texto).
  4. Remoção de tags HTML preservando espaçamento entre palavras adjacentes.
  5. Normalização Unicode NFKC e conversão para minúsculas.
  6. Colapso de espaços em branco.
  7. Tokenização Unicode alfanumérica com descarte de pontuações.
  8. Geração de Shingles: Janela deslizante de 5 tokens (ou tupla única para textos curtos de 1 a 4 tokens).

Critérios de Desempate Técnico e Resiliência

  • Empate Técnico (\le 0.03): Quando dois ou mais extratores atingem pontuações com diferença \le 0.03, o algoritmo seleciona aquele com menor quantidade de shingles (penalizando boilerplate, cabeçalhos ou menus excedentes).
  • Desempate Hierárquico Estrito: Em caso de empate absoluto de pontuação e tamanho, aplica-se a hierarquia fixa: \text{newspaper4k} > \text{readability} > \text{trafilatura}
  • Cenários de 0 Consenso:
    • 3 candidatos ativos \to seleciona o de tamanho mediano de shingles.
    • 2 candidatos ativos \to seleciona o de maior tamanho de shingles.
    • 1 candidato ativo \to seleciona o único utilizável.
    • Todos indisponíveis \to fallback obrigatório em newspaper4k.
  • Gravação Atômica e Não-Destrutiva: Criação de arquivo temporário com substituição atômica (os.replace), preservando 100% dos dados pré-existentes, ordem de artigos e propriedades originais.

Argumentos e Flags CLI

Parâmetro Tipo Padrão Descrição
input_file Posicional (obrigatório) — Caminho para o arquivo JSON contendo a coleção articles.
-o, --output Caminho (opcional) <input_stem>_selected.json Caminho do arquivo JSON de destino.
--indent Inteiro (opcional) 2 Espaços de indentação do JSON (0 para compacto).
-v, --verbose Flag booleana False Emite no stderr os detalhes de pontuação, ativos e regra de escolha por artigo.

Exemplos Práticos de Uso

1. Execução Padrão Automática

python scripts/select_article_extractor.py out/river_plate_extracted.json
# Gera automaticamente out/river_plate_extracted_selected.json

2. Execução com Modo Verboso

python scripts/select_article_extractor.py out/river_plate_extracted.json --verbose
  • Saída no Terminal:
[Artigo #001] Extrator: newspaper4k  | Motivo: technical_tie_smallest_shingles  | Ativos: 3 | Consenso: 1048
[Artigo #002] Extrator: newspaper4k  | Motivo: highest_score                    | Ativos: 3 | Consenso: 333
[Artigo #003] Extrator: readability  | Motivo: highest_score                    | Ativos: 3 | Consenso: 778
...
{
  "status": "success",
  "input_file": "out/river_plate_extracted.json",
  "output_file": "out/river_plate_extracted_selected.json",
  "total_articles": 20,
  "processed_count": 20,
  "distribution": {
    "newspaper4k": 9,
    "readability": 9,
    "trafilatura": 2
  }
}

3. Uso Programático como Módulo Python

from scripts.select_article_extractor import select_article_extractor

article_data = {
    "trafilatura": {"text": "River Plate venceu ontem por 3-0.", "error": None},
    "newspaper4k": {"text": "River Plate venceu ontem por 3-0 no Monumental.", "error": None},
    "readability": {"cleaned_text": "River Plate venceu ontem por 3-0.", "error": None},
}

result = select_article_extractor(article_data)
print("Extrator Selecionado:", result.selected_extractor.value)
print("Motivo:", result.selection_reason)

5. 📝 Conversor de Artigo JSON para Markdown

Visão Geral e Estrutura do Documento

O script scripts/convert_article_to_markdown.py realiza a conversão de um arquivo JSON contendo exatamente um artigo (com selected_extractor) para um documento Markdown (.md) pronto para consumo editorial ou classificação downstream.

A estrutura do Markdown gerado segue estritamente o padrão:

# Título do Artigo

Subtítulo ou descrição editorial (omitido se ausente ou igual ao título).

**Autor:** Nome do Autor 1, Nome do Autor 2
**Publicado em:** 2026-08-20T00:36:33-03:00
**Site:** Nome do Veículo
**Categoria:** Categoria 1, Categoria 2
**Tags:** Tag 1, Tag 2
**Palavras-chave:** Palavra 1, Palavra 2
**Idioma:** es
**Fonte original:** [https://exemplo.com/artigo](https://exemplo.com/artigo)

![Imagem principal](https://exemplo.com/imagem_capa.jpg)

---

Conteúdo textual do artigo convertido em Markdown.

Isolamento Estrito de Extratores e Fallback

  • Isolamento Total do Corpo: O texto e HTML do corpo vêm exclusivamente do extrator indicado em selected_extractor. Caso o extrator selecionado não possua corpo válido, o processo encerra imediatamente com erro (código 1). Nunca ocorre fallback de corpo para outro extrator.
  • Fallback Interno:
    • trafilatura: usa trafilatura.markdown; se vazio, usa trafilatura.text.
    • newspaper4k: converte newspaper4k.article_html para Markdown; se vazio, usa newspaper4k.text.
    • readability: converte readability.cleaned_html para Markdown; se vazio, usa readability.cleaned_text.
  • Conversão HTML→Markdown: Utiliza a biblioteca markdownify configurada para títulos padrão ATX (#, ##, ###), preservando negrito, itálico, listas, tabelas, citações, links e blocos de código.

Matriz Determinística de Metadados

Os metadados editoriais são resolvidos deterministicamente consultando fontes na ordem de prioridade estrita:

  1. Título: SELECIONADO.title → input_meta.titulo → page_title → newspaper4k.title → trafilatura.title → readability.title
  2. URL Original: input_meta.url → crawled_url → URL canônica do selecionado → trafilatura.canonical_url → newspaper4k.canonical_link
  3. Subtítulo/Descrição: descrição do selecionado → trafilatura.description → newspaper4k.meta_description → input_meta.subtitulo
  4. Autores: autor(es) do selecionado → newspaper4k.authors → trafilatura.author → readability.author
  5. Data de Publicação: data do selecionado → newspaper4k.publish_date → trafilatura.date → input_meta.quando_publicado
  6. Site: site do selecionado → trafilatura.sitename → newspaper4k.meta_site_name → trafilatura.hostname → Hostname da URL original
  7. Categorias: categorias do selecionado → trafilatura.categories
  8. Tags: tags do selecionado → trafilatura.tags → newspaper4k.tags → newspaper4k.meta_keywords
  9. Palavras-chave: newspaper4k.keywords → newspaper4k.meta_keywords
  10. Idioma: idioma do selecionado → trafilatura.language → newspaper4k.meta_lang
  11. Imagem Principal: imagem do selecionado → newspaper4k.top_image → trafilatura.image

Sanitização Editorial e Deduplicação

  • Remoção de H1 Duplicado: Se o corpo iniciar com um título H1 idêntico ao título resolvido do artigo, esse H1 inicial é removido automaticamente.
  • Filtro de Imagens: Remove imagens com URLs relativas, vazias ou em formato data:. Deduplica URLs de imagem repetidas no corpo.
  • Filtro de Placeholders: Descarta valores como null, None, N/A, unknown, [no-author] ou rótulos vazios.
  • Gravação Atômica: Escrita transacional em arquivo temporário seguida de substituição com os.replace, garantindo integridade e nenhum resíduo em falhas.

Argumentos e Flags CLI

Parâmetro Tipo Obrigatoriedade Padrão Descrição
-i, --input Caminho Sim — Arquivo JSON contendo exatamente um único artigo.
-o, --output Caminho Não <input_stem>.md Caminho do arquivo Markdown de destino.

Exemplos Práticos de Uso

1. Conversão Padrão

python scripts/convert_article_to_markdown.py -i out/article_001.json
# Gera automaticamente out/article_001.md

2. Conversão com Caminho de Destino Personalizado

python scripts/convert_article_to_markdown.py \
  -i out/article_001.json \
  -o out/markdown/meu_artigo.md

3. Uso Programático em Python

from pathlib import Path
from scripts.convert_article_to_markdown import convert_article

out_file = convert_article(Path("out/article_001.json"), Path("out/artigo.md"))
print(f"Markdown gerado em: {out_file}")

📁 Estrutura do Projeto

TextNLPClassifierApp/
├── classify.py                         # CLI principal do Classificador de Inerência
├── scripts/
│   ├── __init__.py                     # Pacote utilitário de scripts
│   ├── extract_google_news.py          # CLI de Extração de Manchetes do Google News
│   ├── extract_article_contents.py     # CLI de Extração e Parsing Multimotor de Artigos
│   ├── select_article_extractor.py     # CLI de Seleção Determinística de Extrator
│   └── convert_article_to_markdown.py  # CLI de Conversão de Artigo JSON para Markdown
├── src/                                # Módulos centrais do classificador
│   ├── classifier.py                   # Orquestrador de classificação (Tier 1, 2, 3)
│   ├── models.py                       # Modelos de dados e esquemas (ECPSnapshot, Decision)
│   ├── preprocessor.py                 # Normalização de texto e detecção de idioma
│   └── adapters/                       # Adaptadores opcionais de Embeddings e LLM
├── specs/                              # Especificações e planos arquiteturais (Speckit)
│   ├── 001-multilingual-entity-classifier/
│   ├── 002-google-news-extractor/
│   ├── 003-article-content-extractor/
│   ├── 004-deterministic-content-selection/
│   └── 005-convert-json-markdown/      # Specs da conversão JSON para Markdown
├── tests/                              # Suíte de testes automatizados
│   ├── test_classifier.py
│   ├── test_extract_google_news.py
│   ├── test_extract_article_contents.py
│   ├── test_select_article_extractor.py
│   ├── test_convert_article_to_markdown.py # Testes da conversão para Markdown
│   ├── test_llm_fallback.py            # Testes do Tier 3 LLM Fallback
│   └── test_e2e_text_analysis_pipeline.py # Suíte E2E do Funil de Análise e Fallback
├── requirements.txt                    # Dependências do projeto
├── pyproject.toml                      # Configurações de ferramentas (pytest, ruff, mypy)
└── README.md                           # Documentação principal

🧪 Testes e Qualidade de Código

O repositório possui 209 testes automatizados com 100% de aprovação cobrindo testes unitários, de regressão, de integração, Golden Fixtures exatas, testes de sensibilidade de mutação, testes de fallback para LLM (Tier 3), validações de degradação graciosa e testes End-to-End (E2E) via CLI subprocess:

# Executar toda a suíte de testes do projeto (209 testes)
pytest -v

# Executar a Suíte E2E do Funil de Análise de Texto e Fallback para LLM
pytest tests/test_e2e_text_analysis_pipeline.py -v

# Executar os testes do Fallback para LLM (Tier 3)
pytest tests/test_llm_fallback.py -v

# Executar os testes de Conversão de Artigo para Markdown (67 testes)
pytest tests/test_convert_article_to_markdown.py -v

# Executar os testes do Seletor Determinístico
pytest tests/test_select_article_extractor.py -v

# Executar os testes do Extrator de Conteúdo Multimotor
pytest tests/test_extract_article_contents.py -v

# Executar os testes do Extrator do Google News
pytest tests/test_extract_google_news.py -v

# Validação com Ruff
ruff check .

# Verificação estática de tipos com Mypy
mypy src/ scripts/ tests/

📄 Licença

Este projeto está licenciado sob os termos da licença MIT. Consulte o arquivo LICENSE para mais detalhes.