PlaybookMarkUDown
MarkUDownPythonRSSPipelineIntermediário

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.

14 de abril de 202614 min de leituraPor Scrape Technology

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

O MarkUDown tem três camadas de extração internas. Se as primeiras camadas forem bloqueadas, ele escala automaticamente para o Abrasio — um browser service com Chromium corrigido e IPs residenciais brasileiros. Você não configura nada.

Tutorial

Você vai precisar de uma API key do MarkUDown. Crie a sua gratuitamente no dashboard.

1

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.

2

Instale as dependências

Só duas bibliotecas Python:

terminal
pip install requests python-dotenv

Crie um .env na raiz:

.env
# .env
MARKUDOWN_API_KEY=sua_chave_aqui

Nunca versione sua API key

Adicione .env ao .gitignore. A chave dá acesso à sua conta e ao seu saldo de requisições.
3

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.

schema.py
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",
}
4

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.

rss_discovery.py
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

Se o portal tem RSS, use o /rss. É a forma mais rápida e estável de descobrir matérias. Reserve o /map para portais que não disponibilizam feed.
5

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).

map_discovery.py
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")
6

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.

extractor.py
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 None
7

Pipeline completo

Juntando as duas fases em um script que roda de ponta a ponta:

news_pipeline.py
"""
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.py
O 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:

noticias.json
[
  {
    "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.