
Construímos um Pipeline de Renderização Personalizado para Nosso Servidor MCP
Construímos um Pipeline de Renderização Personalizado com Playwright para Nosso Servidor MCP — Aqui Está o Que Aprendemos
No Haunt API, construímos ferramentas de extração da web para agentes de IA. Nosso servidor MCP permite que Claude e outros assistentes de IA extraiam dados estruturados de qualquer URL. Simples o suficiente no papel — buscar uma página, analisar o HTML, retornar JSON.
O problema? Metade da internet não quer ser acessada.
O Problema com "Apenas Use o Playwright"
A maioria dos tutoriais de web scraping segue mais ou menos assim:
from playwright.async_api import async_playwright
async with async_playwright() as p:
browser = await p.chromium.launch()
page = await browser.new_page()
await page.goto(url)
html = await page.content()
E isso funciona! Para uma demonstração. Para um produto do qual usuários reais dependem, isso se desmorona rapidamente:
- Sites detectam navegadores headless e servem captchas ou páginas vazias
- Páginas SPA precisam de tempo para renderizar — quanto tempo você espera? 2 segundos? 5? 10?
- Você está queimando recursos carregando imagens, fontes e CSS quando você só precisa de texto
- Cada renderização custa o mesmo — sem cache, sem inteligência
Passamos por todos esses problemas. Aqui está como resolvemos cada um deles.
Lição 1: Não Use Uma Ferramenta Para Tudo
Nosso pipeline tem três camadas, e a maioria das requisições nunca chega ao Playwright:
- HTTP Direto — Funciona para ~80% da web. Rápido, barato, sem navegador necessário.
- FlareSolverr — Lida com desafios do Cloudflare e renderização básica de JS.
- Playwright — Renderização completa do navegador para SPAs pesadas em JS que retornam esqueletos vazios.
A chave da percepção: detectamos páginas esqueleto — HTML que tem um <div id="root"></div> mas sem conteúdo real — e só ativamos o navegador quando precisamos. A maioria das páginas não precisa disso.
def is_skeleton_html(html: str) -> bool:
"""Detecta se o HTML é um esqueleto JS não renderizado."""
if len(html) < 500:
return True
# Remove scripts/styles e verifica por texto visível
text = strip_tags(html)
if len(text) < 100:
return True
# Marcadores comuns de SPA
skeleton_markers = [
'<div id="root"></div>',
'<div id="__next"></div>',
'Você precisa habilitar o JavaScript',
]
return any(marker in html for marker in skeleton_markers)
Lição 2: Estratégias de Espera Inteligentes Superam Temporizadores Fixos
A pior coisa sobre a automação de navegadores é a espera. time.sleep(5) é ou muito curto (a página não carregou) ou muito longo (perdendo tempo em páginas que carregaram instantaneamente).
Construímos três estratégias de espera concorrentes. A primeira a ser acionada vence:
Estabilidade do Conteúdo — Verifica o texto visível da página a cada 200ms. Se não mudou por 1 segundo, o conteúdo foi carregado.
Rede Ociosa — Espera por nenhuma nova requisição de rede por 500ms. Bom para páginas que fazem chamadas de API após o carregamento inicial.
Conteúdo Significativo — Espera até que a página tenha pelo menos 500 caracteres de texto visível. Captura páginas que carregam algo, mas ainda não estão completas.
async def wait_for_content(page, timeout=10):
"""Espera inteligente — detecta quando o conteúdo foi realmente carregado."""
tasks = [
wait_for_content_stability(page),
wait_for_network_idle(page),
wait_for_meaningful_content(page),
]
done, pending = await asyncio.wait(
tasks, timeout=timeout, return_when=asyncio.FIRST_COMPLETED
)
for t in pending:
...
O artigo apresenta uma solução prática para a extração de dados da web, essencial para empresas que utilizam agentes de IA. A abordagem discutida pode ajudar a otimizar a coleta de dados, reduzindo custos e aumentando a eficiência. Isso é crucial para empresas brasileiras que buscam se adaptar à era digital.

