Como construir uma API de notíciascom MarkUDown
Três endpoints, duas dependências Python. Um pipeline que descobre matérias via RSS e mapeamento de sites, extrai o conteúdo de cada uma e devolve um JSON estruturado com fonte, título, subtítulo, data, autor e texto — de qualquer portal, sem manter seletores CSS.
O problema que todo analista de dados já enfrentou
Você precisa saber o que a imprensa está dizendo sobre seu setor. Pode ser um fundo de investimento monitorando cobertura de empresas do portfólio. Uma startup acompanhando menções a concorrentes. Uma equipe de marketing medindo o impacto de um lançamento. Um pesquisador construindo um corpus de jornalismo econômico.
Parece um problema simples. Você tem as URLs. Você tem Python. Você abre o terminal e começa com requests.get(url).text.
E aí começa a realidade:
- O G1 retorna HTML com o artigo vazio — o conteúdo é carregado via JavaScript
- A Folha retorna 403 a partir de IPs de datacenter
- O InfoMoney tem estrutura diferente a cada redesign
- Você extrai o texto mas junto vêm menus, anúncios e rodapé
- Cada portal usa um formato diferente para data, autor e subtítulo
O que parecia um script de uma tarde vira um projeto de manutenção constante. Você conserta o seletor do G1, o Valor muda o layout, você conserta o Valor, a Folha atualiza o anti-bot, e o ciclo recomeça.
Três endpoints. Problema resolvido.
O MarkUDown tem três endpoints que, juntos, cobrem todo o pipeline de notícias:
/rss — Descoberta a partir de feeds
Passe a URL de qualquer feed RSS. Receba de volta um array de {url, title, summary} para cada matéria. Sem biblioteca, sem parsing manual de XML.
/map — Descoberta em portais sem RSS
Para sites que não disponibilizam feed, o /map rastreia a página de seção e devolve as URLs das matérias. Um filtro por padrão de URL isola só os artigos.
/extract — Extração estruturada em uma chamada
Passe a URL da matéria e o schema de campos. O endpoint acessa a página, extrai o conteúdo limpo e mapeia os campos com IA — retorna JSON pronto.
Portais com anti-bot incluídos
Tutorial
Você vai precisar de uma API key do MarkUDown. Crie a sua gratuitamente no dashboard.
Crie sua conta e obtenha a API key
Acesse o dashboard do MarkUDown, crie uma conta gratuita e copie sua API key. Ela vai como header X-API-KEY em todas as chamadas.
Instale as dependências
Só duas bibliotecas Python:
pip install requests python-dotenvCrie um .env na raiz:
# .env
MARKUDOWN_API_KEY=sua_chave_aquiNunca versione sua API key
.env ao .gitignore. A chave dá acesso à sua conta e ao seu saldo de requisições.Defina o schema da notícia
O schema é um dicionário que descreve os campos que você quer extrair. A IA usa as descrições para entender o que procurar em cada página — independente do layout do portal.
NEWS_SCHEMA = {
"fonte": "Nome do portal ou veículo de comunicação",
"titulo": "Título principal da matéria",
"subtitulo": "Subtítulo ou linha de apoio, se existir",
"data_publicacao": "Data e hora de publicação no formato ISO 8601",
"autor": "Nome do autor ou repórter",
"texto": "Corpo completo da matéria, sem anúncios ou menus",
}Fase 1 — Descoberta via /rss
Para portais com feed RSS, uma chamada ao /rss já devolve todas as matérias recentes com URL, título e resumo. Sem biblioteca de parsing, sem lidar com XML.
import requests
import os
MARKUDOWN_API_KEY = os.getenv("MARKUDOWN_API_KEY")
BASE_URL = "https://api.scrapetechnology.com"
def descobrir_via_rss(feed_url: str) -> list[dict]:
"""Retorna lista de {url, title, summary} para cada item do feed."""
resp = requests.post(
f"{BASE_URL}/rss",
headers={"X-API-KEY": MARKUDOWN_API_KEY},
json={"url": feed_url},
timeout=30,
)
resp.raise_for_status()
return resp.json().get("items", [])
feeds = [
"https://g1.globo.com/rss/g1/economia/",
"https://feeds.folha.uol.com.br/mercado/rss091.xml",
"https://www.infomoney.com.br/feed/",
"https://www.valor.com.br/rss",
]
itens = []
for feed_url in feeds:
novos = descobrir_via_rss(feed_url)
itens.extend(novos)
print(f" {feed_url}: {len(novos)} matérias")RSS primeiro, sempre
/rss. É a forma mais rápida e estável de descobrir matérias. Reserve o /map para portais que não disponibilizam feed.Fase 1b — Descoberta via /map
Para portais sem feed RSS, o /map rastreia a página de seção e devolve as URLs encontradas. O filter_pattern filtra só URLs que seguem o padrão de matérias (com o ano no caminho).
def descobrir_via_map(site_url: str, max_urls: int = 50) -> list[str]:
resp = requests.post(
f"{BASE_URL}/map",
headers={"X-API-KEY": MARKUDOWN_API_KEY},
json={
"url": site_url,
"limit": max_urls,
"filter_pattern": "/[0-9]{4}/", # filtra URLs com ano no caminho
},
timeout=30,
)
resp.raise_for_status()
return resp.json().get("urls", [])
# Portais sem feed RSS
portais_sem_rss = [
"https://www.cnnbrasil.com.br/economia/",
]
for portal in portais_sem_rss:
urls = descobrir_via_map(portal)
itens.extend([{"url": u} for u in urls])
print(f" {portal}: {len(urls)} URLs")Fase 2 — Extração com /extract
Para cada URL descoberta, uma única chamada ao /extract faz tudo: acessa a página, renderiza o JavaScript, remove ruído (anúncios, menus, rodapé) e mapeia o conteúdo aos campos do seu schema.
def extrair_materia(url: str) -> dict | None:
try:
resp = requests.post(
f"{BASE_URL}/extract",
headers={"X-API-KEY": MARKUDOWN_API_KEY},
json={
"url": url,
"schema_fields": NEWS_SCHEMA,
"extract_query": "Extraia os detalhes completos desta matéria jornalística",
},
timeout=60,
)
resp.raise_for_status()
body = resp.json()
if not body.get("success") or not body.get("data"):
return None
result = body["data"]
result["url"] = url
return result
except requests.RequestException as e:
print(f" ✗ {url}: {e}")
return NonePipeline completo
Juntando as duas fases em um script que roda de ponta a ponta:
"""
Pipeline de coleta e extração de notícias com MarkUDown.
Uso:
python news_pipeline.py
Saída:
noticias.json
"""
import json, time, os, requests
from datetime import datetime
from dotenv import load_dotenv
load_dotenv()
MARKUDOWN_API_KEY = os.getenv("MARKUDOWN_API_KEY")
BASE_URL = "https://api.scrapetechnology.com"
NEWS_SCHEMA = {
"fonte": "Nome do portal ou veículo de comunicação",
"titulo": "Título principal da matéria",
"subtitulo": "Subtítulo ou linha de apoio, se existir",
"data_publicacao": "Data e hora de publicação no formato ISO 8601",
"autor": "Nome do autor ou repórter",
"texto": "Corpo completo da matéria, sem anúncios ou menus",
}
RSS_FEEDS = [
"https://g1.globo.com/rss/g1/economia/",
"https://feeds.folha.uol.com.br/mercado/rss091.xml",
"https://www.infomoney.com.br/feed/",
"https://www.valor.com.br/rss",
]
SITES_SEM_RSS = [
"https://www.cnnbrasil.com.br/economia/",
]
# ── Fase 1: Descoberta ────────────────────────────────────────────────────────
def descobrir_urls() -> list[str]:
urls = []
print("→ Fase 1: Descoberta de URLs")
for feed_url in RSS_FEEDS:
try:
resp = requests.post(
f"{BASE_URL}/rss",
headers={"X-API-KEY": MARKUDOWN_API_KEY},
json={"url": feed_url},
timeout=30,
)
resp.raise_for_status()
itens = resp.json().get("items", [])
novos = [item["url"] for item in itens if item.get("url")]
urls.extend(novos)
print(f" RSS {feed_url[:55]}: {len(novos)} matérias")
except requests.RequestException as e:
print(f" ✗ RSS {feed_url}: {e}")
for site in SITES_SEM_RSS:
try:
resp = requests.post(
f"{BASE_URL}/map",
headers={"X-API-KEY": MARKUDOWN_API_KEY},
json={"url": site, "limit": 30, "filter_pattern": "/[0-9]{4}/"},
timeout=30,
)
resp.raise_for_status()
novos = resp.json().get("urls", [])
urls.extend(novos)
print(f" Map {site[:55]}: {len(novos)} URLs")
except requests.RequestException as e:
print(f" ✗ Map {site}: {e}")
seen, unique = set(), []
for url in urls:
if url not in seen:
seen.add(url)
unique.append(url)
print(f"\n Total: {len(unique)} URLs únicas\n")
return unique
# ── Fase 2: Extração ──────────────────────────────────────────────────────────
def extrair_materia(url: str) -> dict | None:
try:
resp = requests.post(
f"{BASE_URL}/extract",
headers={"X-API-KEY": MARKUDOWN_API_KEY},
json={
"url": url,
"schema_fields": NEWS_SCHEMA,
"extract_query": "Extraia os detalhes completos desta matéria jornalística",
},
timeout=60,
)
resp.raise_for_status()
body = resp.json()
if not body.get("success") or not body.get("data"):
return None
result = body["data"]
result["url"] = url
return result
except requests.RequestException:
return None
def extrair_todas(urls: list[str], delay: float = 1.0) -> list[dict]:
print("→ Fase 2: Extração das matérias")
resultados = []
for i, url in enumerate(urls, 1):
print(f" [{i:02d}/{len(urls)}] {url[:70]}...", end=" ", flush=True)
materia = extrair_materia(url)
if materia:
resultados.append(materia)
print(f"✓ {materia.get('titulo', '')[:45]}...")
else:
print("✗ skipped")
time.sleep(delay)
print(f"\n Extraídas com sucesso: {len(resultados)}/{len(urls)}\n")
return resultados
# ── Main ──────────────────────────────────────────────────────────────────────
def main():
start = datetime.now()
urls = descobrir_urls()
noticias = extrair_todas(urls[:20]) # remova [:20] para processar tudo
output_path = "noticias.json"
with open(output_path, "w", encoding="utf-8") as f:
json.dump(noticias, f, ensure_ascii=False, indent=2)
elapsed = (datetime.now() - start).total_seconds()
print(f"✓ {len(noticias)} notícias salvas em '{output_path}' ({elapsed:.1f}s)")
if __name__ == "__main__":
main()Execute
python news_pipeline.pyO script imprime o progresso em tempo real e salva
noticias.json ao final.Resultado
Cada matéria vira um objeto JSON limpo — pronto para indexar, processar com LLM, alimentar um dashboard ou publicar via API própria:
[
{
"fonte": "InfoMoney",
"titulo": "Ibovespa sobe 1,2% e fecha a 128.450 pontos com alívio externo",
"subtitulo": "Bolsa acompanhou recuperação dos mercados internacionais após dados de inflação nos EUA",
"data_publicacao": "2026-04-14T18:32:00-03:00",
"autor": "Redação InfoMoney",
"texto": "O Ibovespa, principal índice da bolsa brasileira, encerrou a sessão desta segunda-feira em alta de 1,2%, aos 128.450 pontos. O movimento seguiu a recuperação dos mercados internacionais...",
"url": "https://www.infomoney.com.br/mercados/ibovespa-sobe-128450-pontos/"
},
{
"fonte": "Folha de S.Paulo",
"titulo": "Banco Central mantém Selic em 10,5% ao ano pela segunda reunião seguida",
"subtitulo": "Decisão unânime do Copom surpreendeu parte do mercado que esperava corte de 0,25 ponto",
"data_publicacao": "2026-04-13T21:00:00-03:00",
"autor": "Eduardo Cucolo",
"texto": "O Comitê de Política Monetária (Copom) do Banco Central manteve a taxa Selic em 10,5% ao ano...",
"url": "https://www1.folha.uol.com.br/mercado/2026/04/banco-central-mantem-selic.shtml"
}
]O que você pode construir com isso
Monitor de menções
Detecte quando sua empresa, produto ou concorrente é citado na imprensa. Rode o pipeline a cada hora.
Feed temático personalizado
Agregue notícias de múltiplas fontes filtradas por setor em um endpoint único — sem abrir cada portal manualmente.
Análise de sentimento
Passe o campo texto para um LLM classificar como positivo, neutro ou negativo. Mede percepção de marca em escala.
Dashboard editorial
Alimente um painel interno com as matérias do dia já normalizadas. Sem copiar e colar.
Alerta de tendências
Compare volume e sentimento de cobertura ao longo do tempo. Identifique quando um tema começa a escalar.
Corpus para IA
Construa uma base de jornalismo real para treinar modelos de linguagem, embeddings ou sistemas de RAG.
Comece agora com o MarkUDown
Crie sua conta gratuita e execute o pipeline deste tutorial em menos de 10 minutos.