web scraping em tempo real com puppeteer e socket.io
tive que resolver um problema em que os dados de máquinas de ressonância magnética estavam disponíveis apenas dentro de uma interface web fechada.
não havia api, documentação ou qualquer forma oficial de acesso.
a única alternativa era acessar a interface como um usuário e extrair os dados diretamente da página.
como eu resolvi
a solução foi baseada em três pilares principais:
- puppeteer para acessar e extrair os dados
- node.js para processar as informações
- socket.io para distribuir os dados em tempo real para o frontend
scraping com puppeteer (o core)
o puppeteer permite controlar um navegador via código, possibilitando interações com a página como se fosse um usuário.
foi assim que eu implementei a base do scraping:
const puppeteer = require('puppeteer');
async function getData() {
const browser = await puppeteer.launch({ headless: true });
const page = await browser.newPage();
await page.goto('URL_DA_PLATAFORMA');
await page.waitForSelector('.selector-que-tem-os-dados');
const data = await page.evaluate(() => {
// aqui você acessa o DOM como se fosse frontend
return Array.from(document.querySelectorAll('.item')).map((el) => ({
name: el.querySelector('.name')?.innerText,
status: el.querySelector('.status')?.innerText,
}));
});
await browser.close();
return data;
}
pontos importantes dessa abordagem:
evaluateexecuta dentro do contexto do navegador- os dados são extraídos do DOM já renderizado
- funciona bem com aplicações SPA e conteúdo carregado via ajax
loop em tempo real (sem websocket na origem)
como a fonte não fornecia dados em tempo real, foi necessário simular esse comportamento.
basicamente, um loop de execução:
setInterval(async () => {
const data = await getData();
// aqui entra o diff
processData(data);
}, 5000);
esse é o ponto central do sistema.
não basta apenas coletar os dados, é necessário:
- comparar com o estado anterior
- detectar mudanças
- decidir quando emitir atualizações
backend com socket.io
o backend foi responsável por orquestrar o fluxo de dados.
o socket.io foi utilizado para enviar atualizações ao frontend em tempo real, sem necessidade de refresh.
estrutura básica:
const express = require('express');
const http = require('http');
const socketIo = require('socket.io');
const app = express();
const server = http.createServer(app);
const io = socketIo(server);
io.on('connection', (socket) => {
console.log('client conectado');
socket.on('disconnect', () => {
console.log('client saiu');
});
});
quando há mudança nos dados:
io.emit('machines:update', data);
integrando scraping + socket
foi implementado um controle simples de estado para evitar emissões desnecessárias:
let lastState = [];
function processData(newData) {
if (JSON.stringify(newData) !== JSON.stringify(lastState)) {
lastState = newData;
io.emit('machines:update', newData);
}
}
é uma abordagem simples, mas funcional para esse tipo de cenário.
frontend (react)
no frontend, a aplicação apenas escuta os eventos do socket e atualiza o estado da interface:
import { useEffect, useState } from 'react';
import { io } from 'socket.io-client';
const socket = io('http://localhost:3000');
export default function Home() {
const [machines, setMachines] = useState([]);
useEffect(() => {
socket.on('machines:update', (data) => {
setMachines(data);
});
}, []);
return (
<div>
{machines.map((m, i) => (
<div key={i}>
<p>{m.name}</p>
<p>{m.status}</p>
</div>
))}
</div>
);
}
notificações (o detalhe que muda tudo)
um ponto importante foi evitar notificações excessivas.
foi necessário garantir que apenas mudanças reais de estado gerassem eventos:
if (machine.status !== prev.status) {
notify(machine);
}
problemas reais que enfrentei
alguns desafios comuns nesse tipo de abordagem:
- mudanças no layout da página podem quebrar o scraping
- delays de carregamento podem gerar inconsistências
- scraping muito frequente pode levar a bloqueios
- dados podem vir incompletos
scraping não é uma solução confiável por padrão — ele precisa ser tratado como tal.
como fazer direito
para tornar o sistema mais robusto:
- utilizar
waitForSelectorsempre que possível - evitar dependência de classes dinâmicas
- implementar mecanismos de retry
- validar os dados antes de emitir
- não assumir que o scraping é 100% confiável
no fim
essa abordagem permitiu criar uma camada de dados onde não existia:
- leitura de dados a partir de uma interface fechada
- transformação em uma estrutura consumível
- distribuição em tempo real para o frontend
repos
- backend: https://github.com/guswitch/digitalhealth-backend
- frontend: https://github.com/guswitch/digitalhealth-app