feat(extractor): implement multi-engine article content extractor

- Added scripts/extract_article_contents.py for batch scraping with stealth Foxcape and triple extraction (Trafilatura, Newspaper4k, Readability)
- Created unit, integration, and E2E test suite in tests/test_extract_article_contents.py (90/90 passing)
- Updated specs/003-article-content-extractor and README.md with usage documentation and CLI contracts
- Passed ruff linting/formatting and mypy type checking cleanly
This commit is contained in:
2026-08-20 19:22:20 -03:00
parent 6e3d57619b
commit 6a45368cb0
85 changed files with 18345 additions and 3897 deletions
+255
View File
@@ -0,0 +1,255 @@
# PRD — Extrator e Parser de Artigos Multimotor (Foxcape + Trafilatura + Newspaper4k + Readability)
> **Status:** Proposto / Planejamento
> **Versão:** 1.0.0
> **Data:** 2026-08-20
> **Autor:** Antigravity AI / DunaMedia
> **Localização:** `docs/prd_extrator_artigos_nlp.md`
---
## 1. Visão Geral e Contexto
### 1.1 Objetivo do Produto
Construir um pipeline/script autônomo e resiliente em Python para **extração profunda e enriquecimento de conteúdo de artigos de notícias** a partir de listagens JSON previamente geradas (ex.: pelo extrator do Google News).
O pipeline utiliza o **Foxcape** em modo *headless* para acessar furtivamente as URLs finais, aguardar a renderização completa da árvore DOM e coletar o HTML íntegro. Em seguida, processa esse HTML simultaneamente através de **três motores consagrados de extração de conteúdo (NLP / Web Content Extraction)**:
1. **Trafilatura**
2. **Newspaper4k**
3. **Readability (`readability-lxml`)**
O resultado consolidado com o máximo de informações extraídas por cada motor é exportado em formato JSON estruturado na pasta `out/`.
---
## 2. Personas e Casos de Uso
| Persona | Necessidade | Benefício |
|---|---|---|
| **Engenheiro de Dados / ETL** | Ingerir em lote o conteúdo completo de notícias a partir de arquivos JSON de busca. | Automação resiliente sem necessidade de criar parsers manuais para cada portal de notícias. |
| **Cientista de Dados / NLP** | Comparar ou combinar diferentes abordagens de extração de texto, resumos e metadados. | Obter em um único JSON os outputs estruturados de 3 extratores líderes da indústria. |
| **Operador de Terminal / Analista** | Executar o script via CLI informando o arquivo de entrada e acompanhando o progresso em tempo real. | Visibilidade clara via `stderr` sem poluir a saída JSON no `stdout`. |
---
## 3. Arquitetura e Fluxo do Sistema
```mermaid
flowchart TD
A[Arquivo JSON de Entrada\n out/river_plate.json] --> B[Script CLI / Use Case\n Carrega lista de notícias]
B --> C[Iterador de Artigos]
C --> D[Motor Foxcape Headless\n Navega até a URL final]
D --> E[Aguarda DOM carregar\n domcontentloaded / load]
E --> F[Coleta HTML Renderizado Completo]
F --> G1[Motor 1: Trafilatura\n Texto limpo, autor, data, tags, meta]
F --> G2[Motor 2: Newspaper4k\n Artigo, autores, resumo NLP, keywords, top image]
F --> G3[Motor 3: Readability\n Miolo HTML limpo, texto limpo, título]
G1 --> H[Agregador de Extrações]
G2 --> H
G3 --> H
H --> I[JSON Consolidado de Saída\n out/extracted_articles_*.json]
```
---
## 4. Requisitos Funcionais (FR)
### RF01 — Ingestão de Arquivo JSON de Notícias
- O sistema deve aceitar como entrada um arquivo JSON localizado em `out/` (ou caminho customizado via flag CLI `--input` / `-i`).
- Deve validar a presença da lista `items` e extrair as propriedades fundamentais de cada item (`url`, `titulo`, `subtitulo`, `quando_publicado`, `pagina`).
- Deve suportar opções para limitar o processamento a N itens (ex.: `--limit 5` para testes rápidos).
### RF02 — Navegação e Coleta com Foxcape (Obrigatório)
- O acesso às páginas **deve obrigatoriamente** ser realizado com o pacote `foxcape`, aproveitando seu motor anti-bot (Camoufox + evasão de fingerprinting).
- Deve executar em modo `headless=True` por padrão (`FoxcapeConfig(headless=True, humanize=False)`).
- Deve navegar até a URL de cada notícia, aguardar o evento de carregamento do DOM (`wait_until="domcontentloaded"` com fallback para timeout) e extrair a string HTML completa e íntegra da página.
- Deve reaproveitar a mesma instância/sessão do navegador entre as requisições do lote para otimizar velocidade e consumo de memória.
### RF03 — Extração Máxima com Trafilatura
- Para cada HTML coletado, executar o motor `trafilatura`.
- Extrair todos os campos disponíveis:
- Texto principal limpo (`raw_text` / `text`).
- Título (`title`).
- Autor(es) (`author`).
- Data de publicação (`date`).
- Descrição / Subtítulo (`description`).
- Categorias e Tags (`categories`, `tags`).
- URL canônica (`canonical_url`).
- Comentários estruturados (se disponíveis).
- Extração em formato JSON estruturado nativo do Trafilatura.
### RF04 — Extração Máxima com Newspaper4k
- Para cada HTML coletado, instanciar `Article(url, input_html=html)`.
- Executar `parse()` e os métodos de NLP:
- Título do artigo (`title`).
- Autores (`authors`).
- Data de publicação (`publish_date`).
- Texto completo limpo (`text`).
- Resumo gerado por NLP (`summary`).
- Palavras-chave extraídas por NLP (`keywords`).
- Imagem de destaque (`top_image`) e lista de todas as imagens (`images`).
- Metadados brutos OpenGraph e Schema.org (`meta_data`).
### RF05 — Extração com Readability (`readability-lxml`)
- Para cada HTML coletado, processar via `Document(html)`.
- Extrair:
- Título limpo (`title()` e `short_title()`).
- Conteúdo limpo em HTML sem boilerplates/anúncios (`summary()`).
- Texto limpo puro derivado do conteúdo principal.
### RF06 — Consolidação e Exportação de Resultados
- O sistema deve agregar o resultado dos 3 extratores em um único objeto por notícia.
- Deve salvar o resultado final em formato JSON na pasta `out/` (ex.: `out/extracted_<nome_do_arquivo_origem>.json` ou caminho fornecido via `--output` / `-o`).
- Deve incluir metadados de execução global:
- `source_file`: caminho do JSON de entrada.
- `processed_at`: timestamp ISO 8601 da execução.
- `total_articles`: quantidade de artigos no arquivo de entrada.
- `successful_articles`: quantidade processada com sucesso.
- `failed_articles`: quantidade com falha.
- `articles`: array com os objetos consolidados.
### RF07 — Interface de Linha de Comando (CLI) e Logs em Tempo Real
- Disponibilizar interface CLI:
```bash
python scripts/extract_article_contents.py -i out/river_plate.json -o out/river_plate_extracted.json
```
- Flags suportadas:
- `-i, --input`: Caminho do arquivo JSON de entrada (obrigatório).
- `-o, --output`: Caminho do arquivo JSON de saída (opcional, padrão: `out/extracted_<basename>.json`).
- `-l, --limit`: Limitar quantidade de artigos processados (opcional).
- `-t, --timeout`: Timeout em segundos por página no Foxcape (padrão: 30s).
- `-s, --silent`: Suprime logs visuais no `stderr`.
- Logs informativos enviados para `sys.stderr` com emojis e timestamps:
- `[INFO] 🚀 Iniciando extração de N artigos a partir de ...`
- `[INFO] 🌐 [1/10] Foxcape navegando: https://...`
- `[INFO] ⚙️ [1/10] Processando extratores (Trafilatura, Newspaper4k, Readability)...`
- `[INFO] ✅ [1/10] Concluído com sucesso!`
- `[INFO] 💾 Salvo com sucesso em: out/...`
### RF08 — Tolerância a Falhas e Resiliência
- Se o acesso a uma URL falhar (ex.: timeout, 404, bloqueio severo), registrar o erro no item individual (`status: "failed"`, `error_message: "..."`) e prosseguir imediatamente para a próxima notícia do lote.
- Se um dos 3 extratores falhar em um HTML específico, os outros 2 extratores devem continuar operando normalmente, registrando o erro no campo correspondente ao motor que falhou.
---
## 5. Requisitos Não Funcionais (NFR)
| ID | Requisito | Critério |
|---|---|---|
| **RNF01** | **Furtividade Anti-Bot** | Utilizar Foxcape/Camoufox para evitar bloqueios de TLS/JA3 e Cloudflare. |
| **RNF02** | **Eficiência de Recursos** | Manter instância persistente do navegador aberta em lote em vez de instanciar/fechar o browser a cada URL. |
| **RNF03** | **Segregação de Streams** | `sys.stderr` exclusivo para logs; `sys.stdout` reservado para output JSON caso não seja especificado arquivo. |
| **RNF04** | **Modularidade** | Código estruturado em classes/módulos independentes para cada extrator (`TrafilaturaExtractor`, `NewspaperExtractor`, `ReadabilityExtractor`). |
| **RNF05** | **Compatibilidade de Plataforma** | Totalmente funcional em Windows, Linux e macOS (Python 3.10+). |
---
## 6. Esquema de Dados (Contratos de Interface)
### 6.1 Esquema do JSON de Entrada (`Input`)
```json
{
"query": "River Plate",
"language": "es",
"locale": "AR",
"total_paginas": 1,
"total_itens": 2,
"scraped_at": "2026-08-20T14:37:38.233557+00:00",
"items": [
{
"titulo": "Los puntajes de River vs. Independiente Santa Fe",
"subtitulo": "River empató sem gols...",
"quando_publicado": "Thu, 20 Aug 2026 03:27:26 GMT",
"url": "https://www.tycsports.com/river-plate/los-puntajes-id755914.html",
"pagina": 1
}
]
}
```
### 6.2 Esquema do JSON Consolidado de Saída (`Output`)
```json
{
"source_file": "out/river_plate.json",
"processed_at": "2026-08-20T15:00:00.000000+00:00",
"total_articles": 1,
"successful_articles": 1,
"failed_articles": 0,
"articles": [
{
"input_meta": {
"titulo_original": "Los puntajes de River vs. Independiente Santa Fe",
"subtitulo_original": "River empató sem gols...",
"quando_publicado": "Thu, 20 Aug 2026 03:27:26 GMT",
"url_original": "https://www.tycsports.com/river-plate/los-puntajes-id755914.html",
"pagina": 1
},
"extraction_status": "success",
"error_message": null,
"crawled_url": "https://www.tycsports.com/river-plate/los-puntajes-id755914.html",
"page_title": "Los puntajes de River...",
"http_status": 200,
"trafilatura": {
"title": "Los puntajes de River vs. Independiente Santa Fe...",
"author": "Ernesto Provitilo",
"date": "2026-08-20",
"description": "El análisis uno por uno...",
"categories": ["River Plate", "Copa Sudamericana"],
"tags": ["River", "Santa Fe"],
"canonical_url": "https://www.tycsports.com/...",
"text": "Franco Armani (6): Seguro en las pocas llegadas...",
"raw_json": {}
},
"newspaper4k": {
"title": "Los puntajes de River vs. Independiente Santa Fe",
"authors": ["Ernesto Provitilo"],
"publish_date": "2026-08-20T03:27:26",
"text": "Franco Armani (6): Seguro en las pocas llegadas...",
"summary": "Resumo gerado por NLP do newspaper...",
"keywords": ["river", "santa fe", "puntajes", "armani"],
"top_image": "https://media.tycsports.com/...",
"images": ["https://media.tycsports.com/..."],
"meta_data": {}
},
"readability": {
"title": "Los puntajes de River vs. Independiente Santa Fe",
"short_title": "Los puntajes de River",
"cleaned_html": "<div><p>Franco Armani (6)...</p></div>",
"cleaned_text": "Franco Armani (6): Seguro en las pocas llegadas..."
}
}
]
}
```
---
## 7. Dependências do Projeto
As seguintes bibliotecas Python são necessárias para viabilizar este PRD:
```toml
[dependencies]
foxcape = ">=0.1.1"
trafilatura = ">=1.8.0"
newspaper4k = ">=0.9.3"
readability-lxml = ">=0.8.1"
beautifulsoup4 = ">=4.12.0"
lxml = ">=4.9.0"
```
---
## 8. Critérios de Aceite
1. **Execução de ponta a ponta**: Executar o script apontando para `out/river_plate.json` e gerar com sucesso `out/river_plate_extracted.json`.
2. **Uso Mandatório do Foxcape**: Todas as páginas HTML devem ser renderizadas e baixadas via Foxcape headless.
3. **Completude das Extrações**:
- O objeto `trafilatura` deve conter título, autor, data e texto limpo.
- O objeto `newspaper4k` deve conter NLP summary, keywords, top_image e texto limpo.
- O objeto `readability` deve conter HTML limpo e texto limpo.
4. **Resiliência a Falhas**: Artigos com falha de conexão não devem quebrar o script e devem vir identificados como `extraction_status: "failed"`.
5. **Logs Informativos**: O operador visualiza no terminal o progresso item a item através do `stderr`.