Catalog Scraper
Lê catálogos em PDF e em páginas web, extrai os produtos e converte os preços para uma unidade comum, deixando a comparação simples. Reconhece a estrutura do documento por regras fixas e só recorre a um modelo de IA quando o formato é irregular.
O Catalog Scraper resolve um problema simples de enunciar e trabalhoso de fazer na mão: comparar preços de produtos que chegam em catálogos de formatos diferentes.
Um fornecedor manda um PDF, outro tem uma página web, um terceiro envia uma lista de preços. Cada um descreve os mesmos produtos do seu jeito, então abrir tudo e conferir item a item é lento e fácil de errar.
O sistema automatiza esse trabalho. Você registra cada fonte, ele lê o documento, extrai os produtos e converte todos os preços para uma mesma unidade de medida (R$/kg, R$/L, R$/un). Com tudo na mesma medida, o melhor preço aparece no topo.
Para ler os documentos, o sistema aplica primeiro regras fixas que reconhecem a estrutura. Só recorre a um modelo de IA quando o formato é irregular.
Conceitos fundamentais
| Conceito | O que é |
|---|---|
| Scraping | Extração automática de dados de documentos e páginas web. Aqui, lê catálogos em PDF e em HTML para coletar produtos e preços. |
| Pipeline de mineração | Sequência de etapas que transforma um catálogo bruto em produtos estruturados: captura → extração → normalização → ranqueamento. |
| Extração determinística | Leitura por regras fixas que reconhecem tabelas e blocos de dados (JSON, lista de preços) sem adivinhação. Roda primeiro porque é previsível e gratuita. |
| Fallback de IA | Recurso secundário acionado só quando as regras fixas não acham estrutura. Usa o nível gratuito da Groq (provedor de inferência de modelos de linguagem); sem chave, ele apenas não opera e nada é cobrado. |
| Unidade base | Unidade comum para comparar preços de embalagens diferentes. Uma caixa de 25 kg e uma de 5 kg viram ambas R$/kg, então o melhor valor fica visível. |
| Faixa de desconto | Tabela de preço por degraus de quantidade. Informada uma quantidade alvo, o ranking recalcula o valor contra a faixa aplicável. |
| Edge Deploy (implantação em borda) | Código executado em servidores próximos do usuário. Aqui, dois Cloudflare Workers (um para a interface, um para a API). |
| build (construção) | Etapa que monta os arquivos finais a partir do código-fonte, antes de publicar. |
| token (credencial) | Texto assinado que o usuário recebe ao entrar e envia de volta a cada chamada, provando quem é. |
Stack
| Tecnologia | Papel |
|---|---|
| Hono | Framework web minimalista; roteamento da API e middleware de autenticação |
| Node.js 24 & TypeScript | Runtime local e tipagem de ponta a ponta |
| Drizzle ORM + Postgres 16 | Camada de dados tipada; ORM (mapeamento objeto-relacional) sobre Postgres |
| unpdf + Playwright | Ingestão: leitura de texto do PDF e captura de páginas web |
| Groq SDK | Fallback de extração no nível gratuito, acionado só quando falta estrutura |
| jose (JWT) | Sessão por token assinado; credenciais de fonte cifradas em repouso |
| Tecnologia | Papel |
|---|---|
| React 19 + Vite | SPA (aplicação de página única) da interface de compras |
| TanStack Router & Table | Roteamento tipado e a tabela de produtos com filtro e ordenação |
| Tailwind CSS v4 + shadcn/ui | Estilização utilitária e componentes base de interface |
| Zustand | Estado local da sessão e dos filtros |
| exceljs + jspdf | Exportação da comparação para Excel e PDF |
Arquitetura e engenharia
O centro do projeto é o pipeline, a sequência de etapas que vai do documento bruto ao produto pronto para comparar.
O status de cada fonte registrada avança por três marcos (pendente → capturado → extraído), e os produtos só entram na comparação depois de convertidos para a unidade base.
| Padrão | Descrição | Detalhes |
|---|---|---|
| Pipeline em etapas | Captura → extração → normalização → ranqueamento | Cada fonte passa pelas mesmas etapas; o status avança pendente → capturado → extraído |
| Leitura por regras antes da IA | Heurísticas de tabela e JSON tentam primeiro; IA é o último recurso | deterministic-extractor lê estrutura conhecida; groq-extractor só atua no fallback, evitando custo e variação |
| Preço por unidade comum | Embalagens diferentes reduzidas a R$/kg, R$/L ou R$/un | base-unit + normalize-price convertem cada item para a mesma medida antes de comparar |
| Ranking por melhor valor | Produtos ordenados pelo menor preço na unidade base | rank-products ordena com a quantidade informada aplicada às faixas de desconto |
| Contratos compartilhados | Mesmos schemas Zod validam API e interface | packages/shared é importado pelos dois apps; o formato de produto, fonte e usuário tem uma fonte única |
| Dois Workers no mesmo domínio | Interface e API como Cloudflare Workers separados + Postgres na Neon | Mesmo domínio raiz mantém o cookie de sessão sem configuração extra; a API abre um pool de conexão por requisição |
Regras primeiro, IA por último
A ordem dessas duas estratégias importa, tanto pelo custo quanto pela confiança no resultado.
Regras fixas que reconhecem uma tabela de preços ou um bloco de dados são previsíveis, gratuitas e repetíveis. O mesmo PDF gera sempre a mesma saída, o que facilita testar e confiar no que sai.
O modelo de IA só entra quando o formato é irregular demais para as regras, e mesmo assim na camada gratuita da Groq.
Sem uma chave de acesso configurada, esse recurso apenas avisa que não conseguiu processar. O sistema continua funcionando, e sem chamada ao provedor não há cobrança.
Mesma unidade para poder comparar
Os dados brutos chegam em medidas que não se comparam diretamente: um item por caixa de 25 kg, outro por pacote de 5 kg, outro por litro.
Comparar o preço de etiqueta não ajuda, porque o tamanho da embalagem muda. Uma caixa mais cara pode sair mais barata por quilo.
A etapa de normalização (base-unit + normalize-price) converte cada produto para a unidade comum e, quando existe desconto por quantidade, aplica a faixa da quantidade que você pretende levar.
No fim, sobra uma única coluna de preço por unidade, e ordenar por ela já responde qual produto sai mais barato. A tabela na interface filtra por categoria e exporta a comparação para Excel ou PDF.
Um só contrato de dados para os dois lados
A interface e a API são programas separados dentro do mesmo repositório, mas precisam concordar sobre o formato dos dados que trocam.
Esse formato fica num pacote compartilhado (packages/shared), um conjunto de schemas Zod (regras de validação) importado pelos dois lados.
Produto, fonte e usuário têm uma definição única. A API valida o que entra e a interface valida o que recebe usando exatamente a mesma regra, então um campo só muda num lugar.
Isso evita o bug clássico em que o backend muda um campo e o frontend continua esperando o formato antigo, sem ninguém perceber.
Deploy na borda, segredo só no backend
A interface e a API rodam como dois Cloudflare Workers (programas distribuídos em servidores próximos do usuário), com o banco Postgres na Neon (serviço serverless, sem servidor para administrar).
A interface é estática e não carrega segredo nenhum. A conexão com o banco e as chaves de assinatura ficam só na API.
Como os dois Workers ficam sob o mesmo domínio, o cookie de sessão é enviado para os dois sem configuração extra de CORS (a regra que controla quais origens podem se comunicar).
Um git push na branch principal dispara o build e a publicação automática dos dois. A atualização do banco roda antes de publicar e é idempotente: rodar de novo não causa dano, porque o processo pula as migrações já aplicadas.