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:
@@ -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`.
|
||||
Reference in New Issue
Block a user