22 KiB
PRD — Conversão de artigo JSON para Markdown
1. Visão geral
O TextNLPClassifierApp já possui scripts para extrair artigos com Trafilatura, Newspaper4k e Readability e para selecionar deterministicamente o melhor extrator de conteúdo.
Esta feature adicionará a etapa seguinte do pipeline: receber o JSON de um único artigo, já contendo selected_extractor, selecionar deterministicamente os metadados disponíveis e gerar um arquivo Markdown com o conteúdo do extrator escolhido.
O arquivo river_plate_extracted_selected(2).json foi usado como referência de estrutura. Embora esse arquivo contenha uma coleção em articles, a entrada operacional desta feature será somente um objeto individual dessa coleção.
2. Problema
Cada artigo possui três resultados de extração com campos, formatos e níveis de preenchimento diferentes. O selected_extractor define qual corpo tem o maior peso e deve ser utilizado, mas metadados úteis podem estar ausentes nesse extrator e disponíveis em outro.
É necessário produzir um Markdown único e previsível sem escolher novamente o melhor conteúdo, sem usar LLM e sem depender de interpretação manual.
3. Objetivo
Criar um CLI Python que:
- receba um arquivo JSON contendo exatamente um artigo;
- valide os campos obrigatórios;
- use exclusivamente o
selected_extractorpara obter o corpo do artigo; - selecione título, URL original e metadados opcionais por regras determinísticas;
- converta o corpo HTML para Markdown quando necessário;
- grave um arquivo
.mdlegível, consistente e pronto para as etapas posteriores do pipeline.
4. História do usuário
Como operador do pipeline de conteúdo, quero converter o JSON selecionado de um artigo em um arquivo Markdown para que o conteúdo e os melhores metadados disponíveis possam ser consumidos pelas etapas seguintes do sistema.
5. Escopo
5.1 Incluído
- CLI Python.
- Leitura de um arquivo JSON com um único artigo.
- Suporte a
trafilatura,newspaper4kereadabilitycomo valores deselected_extractor. - Seleção determinística de metadados.
- Conversão de HTML para Markdown.
- Uso direto do Markdown já gerado pela Trafilatura quando disponível.
- Geração de um único arquivo
.mdpor execução. - Validação de entrada, saída e erros.
- Gravação atômica do arquivo de saída.
- Testes unitários, testes do CLI e arquivos de resultado esperado.
5.2 Fora do escopo
- Receber o objeto raiz com o array
articles. - Processar vários artigos em uma execução.
- Executar novamente Trafilatura, Newspaper4k ou Readability.
- Calcular ou alterar
selected_extractor. - Comparar, combinar ou complementar o corpo com conteúdo de outro extrator.
- Usar LLM, embeddings ou qualquer seleção probabilística.
- Fazer novas requisições HTTP.
- Baixar ou armazenar imagens.
- Limpar semanticamente publicidade, recomendações, overlays ou outros blocos editoriais presentes no corpo selecionado.
- Criar API, banco de dados, fila, interface gráfica ou integração externa.
- Alterar o JSON recebido.
6. Contrato de entrada
6.1 Formato
A entrada será um arquivo JSON UTF-8 cujo objeto raiz representa um único item do array articles observado no arquivo de referência.
Campos esperados no objeto:
| Campo | Tipo esperado | Obrigatoriedade | Uso |
|---|---|---|---|
selected_extractor |
string | Obrigatório | Define a única fonte permitida para o corpo. |
input_meta |
object | Opcional | Fornece principalmente URL original, título e data de fallback. |
crawled_url |
string | Opcional | URL de fallback. |
page_title |
string | Opcional | Título de fallback. |
trafilatura |
object | Condicional | Obrigatório quando selecionado; opcional nos demais casos. |
newspaper4k |
object | Condicional | Obrigatório quando selecionado; opcional nos demais casos. |
readability |
object | Condicional | Obrigatório quando selecionado; opcional nos demais casos. |
6.2 Valores aceitos para selected_extractor
trafilaturanewspaper4kreadability
Qualquer outro valor deve invalidar a entrada.
6.3 Campos obrigatórios após a resolução
O processamento somente será bem-sucedido se for possível resolver:
- título não vazio;
- URL original absoluta com protocolo
httpouhttps; - corpo não vazio pertencente ao
selected_extractor.
Os demais campos são opcionais e nunca devem impedir a geração do Markdown.
7. Contrato de saída
7.1 Arquivo
- Formato: Markdown UTF-8.
- Quantidade: um arquivo por execução.
- Nome padrão: mesmo nome-base do JSON de entrada, substituindo
.jsonpor.md. - Caminho alternativo: informado por
-oou--output. - Escrita: arquivo temporário seguido de substituição atômica do destino.
7.2 Estrutura do Markdown
O Markdown deve seguir esta ordem:
# Título do artigo
Subtítulo ou descrição, quando disponível.
**Autor:** Nome do autor
**Publicado em:** 2026-08-20T00:36:33-03:00
**Site:** Nome do site
**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)

---
Conteúdo do artigo em Markdown.
Regras de apresentação:
- título, URL original e corpo sempre devem aparecer;
- cada linha opcional deve ser completamente omitida quando não houver valor válido;
- nenhum placeholder como
null,None,N/A,unknownou[no-author]deve aparecer; - o subtítulo deve ser omitido quando for igual ao título após normalização;
- a imagem principal deve ser omitida quando não possuir URL absoluta
httpouhttps; - o nome do
selected_extractornão deve ser exibido no documento; - os metadados técnicos internos do JSON não devem ser exibidos.
8. Regras funcionais
RF-001 — Receber um único artigo
O CLI deve aceitar somente um objeto individual de artigo. Um objeto contendo articles deve ser rejeitado, pois o processamento em lote não pertence a esta feature.
RF-002 — Respeitar o extrator selecionado
O corpo deve vir exclusivamente do extrator indicado em selected_extractor. A ausência de corpo utilizável nesse extrator deve encerrar o processamento com erro. O sistema não pode trocar silenciosamente para outro extrator.
RF-003 — Resolver o corpo dentro do extrator selecionado
selected_extractor |
Fonte principal | Fallback do mesmo extrator | Tratamento |
|---|---|---|---|
trafilatura |
trafilatura.markdown |
trafilatura.text |
Usar o Markdown diretamente; o texto puro já é Markdown válido. |
newspaper4k |
newspaper4k.article_html |
newspaper4k.text |
Converter o HTML; usar texto puro somente quando o HTML estiver vazio. |
readability |
readability.cleaned_html |
readability.cleaned_text |
Converter o HTML; usar texto puro somente quando o HTML estiver vazio. |
O fallback ocorre somente entre representações do mesmo extrator selecionado.
RF-004 — Selecionar metadados deterministicamente
Para cada campo, o sistema deve:
- percorrer as fontes na ordem definida neste PRD;
- normalizar e validar cada candidato;
- selecionar o primeiro candidato válido;
- não consultar as fontes restantes após a seleção;
- omitir o campo se nenhum candidato opcional for válido.
A mesma entrada deve sempre gerar a mesma seleção e o mesmo arquivo.
RF-005 — Priorizar metadados por campo
SELECIONADO representa o campo equivalente dentro do objeto indicado por selected_extractor. Quando o extrator não possuir o campo, essa posição é ignorada.
| Campo de saída | Ordem de prioridade |
|---|---|
| Título | SELECIONADO.title → input_meta.titulo → page_title → newspaper4k.title → trafilatura.title → readability.title |
| URL original | input_meta.url → crawled_url → URL canônica do selecionado → trafilatura.canonical_url → newspaper4k.canonical_link |
| Subtítulo/descrição | descrição do selecionado → trafilatura.description → newspaper4k.meta_description → input_meta.subtitulo |
| Autores | autor(es) do selecionado → newspaper4k.authors → trafilatura.author → readability.author |
| Data de publicação | data do selecionado → newspaper4k.publish_date → trafilatura.date → input_meta.quando_publicado |
| Site | site do selecionado → trafilatura.sitename → newspaper4k.meta_site_name → trafilatura.hostname → hostname da URL original |
| Categorias | categorias do selecionado → trafilatura.categories |
| Tags | tags do selecionado → trafilatura.tags → newspaper4k.tags → newspaper4k.meta_keywords |
| Palavras-chave | newspaper4k.keywords → newspaper4k.meta_keywords |
| Idioma | idioma do selecionado → trafilatura.language → newspaper4k.meta_lang |
| Imagem principal | imagem do selecionado → newspaper4k.top_image → trafilatura.image |
Mapeamento dos campos equivalentes do extrator selecionado:
| Informação | Trafilatura | Newspaper4k | Readability |
|---|---|---|---|
| Título | title |
title |
title |
| Descrição | description |
meta_description |
Não disponível |
| Autores | author |
authors |
author |
| Data | date |
publish_date |
Não disponível |
| Site | sitename |
meta_site_name |
Não disponível |
| Categorias | categories |
Não disponível | Não disponível |
| Tags | tags |
tags |
Não disponível |
| Idioma | language |
meta_lang |
Não disponível |
| Imagem principal | image |
top_image |
Não disponível |
| URL canônica | canonical_url |
canonical_link |
Não disponível |
RF-006 — Normalizar valores escalares
Antes da validação, toda string candidata deve:
- ter entidades HTML decodificadas;
- remover espaços no início e no fim;
- colapsar sequências internas de espaços em um único espaço;
- ser considerada ausente quando vazia ou quando corresponder, sem diferença entre maiúsculas e minúsculas, a um placeholder conhecido:
null,none,n/a,unknown,[no-author]ouno-author.
RF-007 — Normalizar listas
Autores, categorias, tags e palavras-chave podem chegar como lista ou string. O sistema deve:
- aceitar lista de strings;
- aceitar string única;
- separar strings com múltiplos valores apenas por ponto e vírgula;
- normalizar cada item conforme RF-006;
- descartar autor iniciado por
http://,https://ouwww.; - eliminar duplicatas sem diferenciar maiúsculas de minúsculas, preservando a primeira grafia e a ordem original;
- considerar a fonte inválida quando nenhum item válido restar;
- usar somente a primeira fonte da tabela de prioridade que resultar em lista válida, sem unir listas de fontes diferentes.
RF-008 — Normalizar datas
- Aceitar datas ISO 8601 e RFC 2822 observadas na entrada de referência.
- Emitir ISO 8601.
- Preservar o fuso horário informado.
- Emitir somente
YYYY-MM-DDquando a fonte fornecer apenas a data. - Considerar inválida uma data que não possa ser interpretada e continuar para a próxima fonte de prioridade.
RF-009 — Validar URLs
- Aceitar somente URLs absolutas com protocolo
httpouhttps. - Não fazer requisições para validar existência ou disponibilidade.
- Não aceitar
data:,javascript:, caminhos relativos ou strings sem hostname.
RF-010 — Converter HTML para Markdown
A conversão deve usar a biblioteca Python markdownify, configurada para produzir títulos no padrão ATX (#, ##, ###).
Devem ser preservados, quando presentes no HTML selecionado:
- parágrafos;
- títulos e subtítulos estruturais;
- listas ordenadas e não ordenadas;
- negrito e itálico;
- links;
- citações;
- blocos de código;
- tabelas suportadas pela biblioteca;
- imagens válidas do próprio corpo.
A escolha de markdownify é intencional: a necessidade é exclusivamente converter HTML para Markdown. O Microsoft MarkItDown suporta HTML, mas atende vários outros formatos e acrescentaria uma abstração mais ampla do que a requerida por esta feature.
RF-011 — Tratar imagens do corpo
- Preservar imagens convertidas do corpo somente quando o destino for uma URL absoluta
httpouhttps. - Remover imagens com URL vazia, relativa ou
data:. - Eliminar repetições exatas da mesma URL de imagem, preservando a primeira ocorrência.
- Não adicionar ao corpo a coleção
newspaper4k.images. - Não baixar, redimensionar ou validar remotamente imagens.
RF-012 — Evitar título duplicado
Após a conversão do corpo, o sistema deve remover o primeiro título H1 do corpo somente quando ele for igual ao título resolvido após decodificação de HTML, normalização de espaços e comparação sem diferença entre maiúsculas e minúsculas.
Outros títulos do conteúdo devem ser preservados.
RF-013 — Normalizar o Markdown final
O arquivo final deve:
- usar quebra de linha
LF; - terminar com exatamente uma quebra de linha;
- eliminar espaços no final das linhas;
- limitar sequências de linhas vazias a no máximo duas;
- não conter tags HTML remanescentes geradas apenas pela estrutura do documento;
- preservar o texto, a pontuação e os caracteres Unicode do conteúdo selecionado.
RF-014 — Não gerar saída parcial
Se ocorrer qualquer erro antes da conclusão, o arquivo de destino existente deve permanecer intacto e nenhum arquivo temporário deve permanecer no diretório de saída.
9. Interface CLI
9.1 Script
scripts/convert_article_to_markdown.py
9.2 Argumentos
| Parâmetro | Tipo | Obrigatoriedade | Padrão | Descrição |
|---|---|---|---|---|
-i, --input |
caminho | Obrigatório | — | JSON contendo um único artigo. |
-o, --output |
caminho | Opcional | <input_stem>.md |
Arquivo Markdown de destino. |
Não devem ser adicionadas flags sem requisito funcional neste PRD.
9.3 Exemplos
python scripts/convert_article_to_markdown.py -i out/article_001.json
Resultado: out/article_001.md.
python scripts/convert_article_to_markdown.py \
-i out/article_001.json \
-o out/markdown/article_001.md
9.4 Saída do processo
- Código
0: arquivo gerado com sucesso. - Código
2: argumentos inválidos, conforme comportamento doargparse. - Código
1: erro de leitura, validação, conversão ou gravação. - Mensagens de erro e confirmação devem ir para
stderr. - O conteúdo Markdown não deve ser impresso no terminal quando houver arquivo de saída.
10. Tratamento de erros
O processamento deve falhar de forma clara nos seguintes casos:
| Situação | Comportamento esperado |
|---|---|
| Arquivo não encontrado ou ilegível | Encerrar com código 1 e informar o caminho. |
| JSON inválido | Encerrar com código 1 e informar que a entrada não é JSON válido. |
| Raiz diferente de objeto | Encerrar com código 1. |
Entrada contém articles |
Encerrar com código 1 e informar que o CLI aceita um único artigo. |
selected_extractor ausente ou desconhecido |
Encerrar com código 1. |
| Objeto do extrator selecionado ausente | Encerrar com código 1. |
| Corpo do extrator selecionado vazio | Encerrar com código 1; não usar outro extrator. |
| Título não resolvido | Encerrar com código 1. |
| URL original não resolvida ou inválida | Encerrar com código 1. |
| Metadado opcional inválido | Ignorar o candidato e tentar o próximo; omitir se todos falharem. |
| Falha na conversão | Encerrar com código 1. |
| Falha na gravação | Encerrar com código 1 sem alterar o destino anterior. |
As mensagens não devem imprimir o conteúdo integral do artigo.
11. Requisitos não funcionais
RNF-001 — Determinismo
A mesma entrada e a mesma versão das dependências devem produzir exatamente o mesmo arquivo Markdown.
RNF-002 — Compatibilidade
A feature deve manter as versões de Python declaradas como suportadas pelo projeto e funcionar nos sistemas operacionais já suportados pelo repositório.
RNF-003 — Execução local
O processamento deve ocorrer inteiramente em memória local, sem rede, browser, LLM ou serviço externo.
RNF-004 — Integridade
A entrada não deve ser modificada. A gravação de saída deve ser atômica.
RNF-005 — Manutenibilidade
As regras de resolução de campos e as regras de conversão devem ser isoladas em funções testáveis, sem duplicação entre o CLI e o uso interno.
RNF-006 — Qualidade
O código deve atender aos gates já adotados pelo projeto: Ruff, Mypy, Pytest e SonarQube.
12. Critérios de aceite
CA-001 — Trafilatura selecionada
Dado um artigo com selected_extractor igual a trafilatura e trafilatura.markdown preenchido
Quando o CLI for executado
Então o corpo do arquivo deve vir de trafilatura.markdown e nenhum corpo dos demais extratores deve ser incorporado.
CA-002 — Newspaper4k selecionado
Dado um artigo com selected_extractor igual a newspaper4k e newspaper4k.article_html preenchido
Quando o CLI for executado
Então esse HTML deve ser convertido para Markdown preservando sua estrutura editorial.
CA-003 — Readability selecionado
Dado um artigo com selected_extractor igual a readability e readability.cleaned_html preenchido
Quando o CLI for executado
Então esse HTML deve ser convertido para Markdown preservando sua estrutura editorial.
CA-004 — Fallback dentro do extrator
Dado um extrator selecionado cujo campo estruturado esteja vazio, mas cujo campo de texto puro esteja preenchido
Quando o CLI for executado
Então o texto puro do mesmo extrator deve ser usado.
CA-005 — Proibição de fallback de corpo entre extratores
Dado um extrator selecionado sem HTML, Markdown ou texto utilizável e outro extrator com conteúdo
Quando o CLI for executado
Então o processamento deve falhar sem utilizar o outro extrator.
CA-006 — Metadado vindo de outro extrator
Dado um artigo cujo extrator selecionado não possua autor e outro extrator possua autor válido
Quando o CLI for executado
Então o primeiro autor válido conforme a prioridade deve aparecer no Markdown.
CA-007 — Obrigatórios presentes
Dado um artigo válido
Quando o Markdown for gerado
Então ele deve conter título, URL original e corpo não vazio.
CA-008 — Opcionais ausentes
Dado um artigo sem metadados opcionais válidos
Quando o Markdown for gerado
Então nenhuma linha vazia de metadado ou placeholder deve ser exibida.
CA-009 — Imagens inválidas
Dado um corpo com imagens data:, vazias ou relativas
Quando o conteúdo for convertido
Então essas imagens devem ser removidas do Markdown.
CA-010 — Título repetido no corpo
Dado um corpo que começa com H1 igual ao título resolvido
Quando o Markdown for montado
Então deve existir somente um H1 com esse título no arquivo final.
CA-011 — Entrada em lote rejeitada
Dado o arquivo completo de referência contendo articles
Quando ele for passado diretamente ao CLI
Então o processamento deve falhar informando que a entrada esperada é um único artigo.
CA-012 — Determinismo
Dado o mesmo JSON processado duas vezes com as mesmas dependências
Quando os arquivos forem comparados byte a byte
Então eles devem ser idênticos.
CA-013 — Gravação segura
Dado um arquivo de destino preexistente e uma falha durante o processamento
Quando o CLI encerrar
Então o arquivo preexistente deve continuar inalterado.
13. Estratégia de testes
13.1 Testes unitários
- resolução de cada campo conforme a ordem de prioridade;
- normalização de strings e placeholders;
- normalização, deduplicação e seleção de listas;
- parsing e padronização de datas ISO 8601 e RFC 2822;
- validação de URLs;
- seleção do corpo para cada extrator;
- fallback de representação dentro do mesmo extrator;
- bloqueio do fallback de corpo para outro extrator;
- conversão dos principais elementos HTML;
- remoção de imagens inválidas e duplicadas;
- remoção somente do H1 inicial duplicado;
- normalização final de espaços e quebras de linha.
13.2 Testes de integração do CLI
- geração com caminho padrão;
- geração com
--output; - códigos de saída
0,1e2; - mensagens em
stderr; - rejeição do JSON com coleção
articles; - preservação do destino em caso de falha;
- codificação UTF-8 com caracteres acentuados.
13.3 Casos de resultado esperado
Devem existir pelo menos três fixtures válidas, uma para cada valor de selected_extractor, acompanhadas dos respectivos arquivos Markdown esperados. A comparação deve ser exata.
Também devem existir fixtures inválidas cobrindo:
- JSON corrompido;
selected_extractorausente;- extrator desconhecido;
- corpo selecionado vazio;
- título ausente em todas as fontes;
- URL ausente ou inválida em todas as fontes;
- objeto raiz contendo
articles.
14. Definition of Done
A feature será considerada concluída quando:
- o script
scripts/convert_article_to_markdown.pyestiver implementado; markdownifyestiver declarada nas dependências do projeto;- todos os requisitos funcionais e critérios de aceite estiverem cobertos;
- os testes unitários e de integração estiverem passando;
- as três fixtures de extratores produzirem exatamente os Markdown esperados;
- Ruff não apontar erros;
- Mypy não apontar erros;
- o conjunto completo de testes do projeto permanecer aprovado;
- o Quality Gate do SonarQube estiver aprovado;
- o README documentar a nova etapa, os argumentos e exemplos do CLI;
- nenhuma funcionalidade fora do escopo tiver sido adicionada.
15. Dependência técnica escolhida
- Biblioteca:
markdownify - Finalidade: conversão direta de HTML para Markdown em Python.
- Alternativa avaliada:
Microsoft MarkItDown. - Decisão: não usar MarkItDown nesta feature porque seu escopo de conversão multiformato excede a necessidade de HTML para Markdown.