Automação e Web Scraping
Serviço de automação construído como desafio técnico. Desenvolvido integralmente por Paulo.
Serviço de automação construído como desafio técnico: navegar em um portal público via Playwright, extrair dados estruturados e servir os resultados via API REST. O desafio não está no scraping em si — está em fazê-lo de forma confiável em produção, onde o ambiente é diferente do local e os timeouts são reais.
Portais públicos têm comportamentos não documentados: scripts de analytics que nunca terminam de carregar, WAFs que adicionam latência variável, textos que existem como substrings uns dos outros, cliques que são interceptados por elementos sobrepostos. Cada um desses casos se manifestou de forma diferente em produção do que no ambiente local.
O aspecto mais valioso deste projeto não é o código que funcionou — é o registro dos seis problemas que apareceram só em produção e a forma como cada um foi isolado e corrigido.
FastAPI recebe requisições e valida entrada/saída via Pydantic. Playwright assíncrono executa a navegação e extração de dados — async porque a operação é I/O-bound (espera de rede/DOM), não CPU-bound. Docker containeriza o serviço com Chromium. Nginx fica na frente como proxy reverso com proxy_read_timeout ajustado. EC2 hospeda o serviço. Swagger UI gerado automaticamente pelo FastAPI documenta todos os endpoints. Tratamento de erros em 3 camadas: validação de entrada (Pydantic), execução com timeout explícito (Playwright), e limpeza garantida no finally (fechar o browser mesmo que a extração falhe).
Playwright assíncrono — async porque I/O-bound, não CPU-bound
- Problema
- Scraping web é uma operação de espera: aguardar carregamento de rede, renderização de DOM, execução de JavaScript. Usar threads síncronas bloquearia o event loop do FastAPI durante todo o tempo de espera do navegador — podendo chegar a 3 minutos por requisição.
- Alternativas
- Multiprocessing: adequado para operações CPU-bound (compressão, parsing pesado). Adiciona overhead de IPC e memória. Threads síncronas: bloqueia o event loop. Para I/O-bound, async é a escolha certa — libera o event loop enquanto o navegador espera pela rede.
- Escolha
- playwright.async_api com async/await. O FastAPI mantém o event loop livre durante a espera do Playwright. Múltiplas requisições podem ser processadas concorrentemente enquanto um scraping aguarda carregamento de DOM.
- Resultado
- Serviço responsivo mesmo com operações longas em andamento. A abordagem async cobre naturalmente os casos de timeout longo sem bloquear o servidor.
Pydantic separando schema de entrada do schema de saída
- Problema
- Sem separação explícita entre o que a API recebe e o que retorna, validação de entrada e formatação de saída ficam misturadas no mesmo modelo — ou pior, no código de scraping. Difícil de testar e de documentar.
- Escolha
- Dois schemas Pydantic distintos: SearchRequest (valida e normaliza a entrada antes do scraping começar) e SearchResult (define a estrutura exata da resposta). FastAPI usa esses schemas para validação automática e para gerar o Swagger UI sem anotação manual.
- Resultado
- Erros de entrada são rejeitados antes de abrir o navegador. A resposta tem estrutura garantida por schema. Swagger UI documenta a API automaticamente.
Tratamento de erros em 3 camadas com finally garantindo limpeza
- Problema
- Se o scraping falha no meio da execução — timeout, elemento não encontrado, exceção inesperada — o browser fica aberto consumindo memória na EC2. Em produção, isso acumula e degrada o serviço ao longo do tempo.
- Escolha
- Camada 1 (Pydantic): valida a entrada antes de qualquer operação. Camada 2 (try/except no Playwright): captura erros de navegação e timeout com mensagem de erro estruturada. Camada 3 (finally): fecha o browser e o contexto Playwright independentemente do resultado — sucesso ou falha. O finally não é opcional: é a garantia de limpeza.
- Resultado
- Sem vazamento de processos de browser em produção. Falhas retornam erro estruturado. A EC2 não acumula processos órfãos ao longo do tempo.
networkidle nunca resolvia — scripts de analytics bloqueavam o wait
- Sintoma
- page.goto() com wait_until='networkidle' travava indefinidamente. O Playwright aguardava silêncio na rede por 500ms, mas scripts de analytics e trackers do portal mantinham requisições de rede em loop contínuo. O scraping nunca avançava.
- Causa
- networkidle espera que todas as conexões de rede estejam encerradas. Scripts de terceiros (analytics, beacons) nunca param de fazer requisições — esse estado nunca é atingido em portais com tracking.
- Correção
- Substituição de networkidle por domcontentloaded como wait_until. O DOM estar pronto é o sinal correto para scraping — não o silêncio da rede. Completamente independente de scripts de terceiros.
Timeout de 30s insuficiente na EC2 — WAF adicionava latência variável
- Sintoma
- Localmente, o scraping completava em 8–12 segundos. Na EC2, o mesmo fluxo ultrapassava 30 segundos consistentemente e retornava erro de timeout. O Playwright chegava à página mas a resposta demorava muito mais do que o esperado.
- Causa
- O WAF do portal inspeciona requisições de IP de datacenter de forma diferente de IPs residenciais. Esse overhead de inspeção adiciona latência variável que não existe no ambiente local.
- Correção
- Timeout do Playwright ajustado para 90 segundos para cobrir a latência real observada na EC2. O valor foi estabelecido empiricamente — não arbitrariamente — após medir o percentil 95 dos tempos de resposta em produção.
Nginx retornando 502 antes do scraping terminar
- Sintoma
- Mesmo com o Playwright configurado para 90s, requisições longas retornavam 502 Bad Gateway antes do scraping concluir. O Playwright completava, mas o Nginx já tinha encerrado a conexão.
- Causa
- O padrão do Nginx para proxy_read_timeout é 60s. Eu já havia configurado 120s no bloco location, mas o fluxo mais longo — com etapas extras de navegação — ainda ultrapassava esse limite, e o Nginx encerrava a conexão com o cliente antes do FastAPI retornar a resposta do Playwright.
- Correção
- proxy_read_timeout: padrão de 60s → 120s (ainda insuficiente) → 300s, no bloco location do Nginx. O timeout do Nginx precisa ser maior que o do Playwright — não igual. Bastou recarregar o Nginx, sem rebuild do container.
Falso positivo: '0 resultados' era substring de '10.000 resultados'
- Sintoma
- O código verificava se o texto da página continha '0 resultados' para detectar busca sem retorno. Buscas com 10.000 resultados eram incorretamente marcadas como vazias — '0 resultados' existe como substring dentro de '10.000 resultados'.
- Causa
- Verificação de substring sem ancoragem. Qualquer número terminado em 0 (10, 100, 1.000, 10.000) contém a sequência '0 resultado' dentro da string de resultado.
- Correção
- Substituição da verificação de substring por regex com ancoragem: padrão que verifica '0 resultados' como expressão completa, não como parte de outra string. Testado com os casos de borda que dispararam o bug.
Clique travado — label sobreposta interceptava o evento
- Sintoma
- page.click() em um elemento de formulário falhava com 'element is not clickable at point'. O Playwright localizava o elemento corretamente, mas o clique não era recebido pelo elemento esperado.
- Causa
- Uma tag label com CSS de posicionamento absoluto ficava sobreposta ao input. O clique atingia a label, não o input diretamente — comportamento diferente do esperado pela automação.
- Correção
- Substituição de page.click() por page.locator().dispatch_event('click'). O dispatch_event entrega o evento diretamente ao elemento no DOM, ignorando a sobreposição visual. Alternativa: clicar na label (que é o comportamento correto do usuário real), mas dispatch_event foi mais explícito sobre a intenção.
Extração de texto instável — page.evaluate() com TreeWalker
- Sintoma
- Extração de texto de células de tabela via innerText retornava conteúdo inconsistente dependendo de como o navegador havia renderizado o DOM. Valores de células adjacentes apareciam concatenados ou cortados.
- Causa
- innerText é sensível ao layout de renderização — o que é considerado 'texto visível' varia com o estado do CSS. Elementos com display:none ou visibility:hidden podem ser incluídos ou excluídos de forma imprevisível.
- Correção
- Substituição por page.evaluate() com TreeWalker percorrendo o DOM explicitamente: selecionar apenas nós de texto, filtrar por tipo de nó, concatenar com separador controlado. Extração determinística independente do estado de renderização.
Serviço de scraping assíncrono operando em produção 24/7 em AWS EC2. Seis problemas específicos de produção identificados, isolados e resolvidos — cada um com causa raiz documentada.
API documentada via Swagger UI gerado automaticamente pelo FastAPI. Tratamento de erros em 3 camadas com limpeza garantida de recursos. Nenhum vazamento de processos de browser em operação contínua.
Ambientes de datacenter se comportam diferente de ambientes locais. WAF adiciona latência. networkidle nunca termina em portais com analytics. Timeouts precisam ser medidos em produção, não estimados localmente. O valor de um scraper em produção está nos casos que quebraram — não nos que funcionaram na primeira tentativa.