- Em dezembro de 2022, em meio à inflação alta, foi criado o pipeline pricewatcher.gr para acompanhar diariamente as variações de preços dos três maiores supermercados da Grécia
- Como as listas de produtos ficavam por trás de renderização JavaScript e rolagem infinita, em vez de
curlourequests.get(), foi usado o Playwright para controlar o navegador e extrair dados do DOM - Como o ambiente de execução era lento usando apenas um notebook antigo e AWS EC2 era caro, a abordagem mudou para criar servidores Hetzner somente quando necessário, executar três scrapers em paralelo e depois encerrá-los
- Para um supermercado em que a Akamai bloqueava IPs não residenciais, foi usado um exit node do Tailscale para rotear pelo IP de casa; falhas eram detectadas com alertas por e-mail, transformações rigorosas e heurísticas de contagem de produtos
- Com servidor de 8 vCPUs e bloqueio de requisições de imagens, o tempo de execução foi reduzido; nas 31 execuções mais recentes, os custos ficaram em cerca de €4,94 pelos servidores Hetzner, €0,09 por IPv4 e €0,00 de armazenamento no Cloudflare R2
Raspando sites de supermercados baseados em JavaScript
- Em dezembro de 2022, em um cenário de inflação alta, foi criado o pricewatcher.gr, que acompanha as variações de preços dos três maiores supermercados da Grécia
- Todos os três e-shops dos supermercados usavam renderização JavaScript, e algumas áreas carregavam mais produtos ao rolar a página, como a rolagem infinita das redes sociais
- Não era possível obter os dados dos produtos com um simples
curlourequests.get(), então foi usado o Playwright - O Playwright controla o navegador programaticamente e trata as seguintes tarefas por API
- Abrir uma nova aba e navegar para uma URL
- Inspecionar o DOM
- Consultar detalhes de elementos
- Interceptar e inspecionar requisições
- O Playwright oferece suporte a Chromium, Safari e Firefox, e pode ser usado em Node, Java, .NET e Python
- O scraper encontra o elemento
load moreda rolagem infinita e continua rolando; depois, na listali.product-item, exclui produtos marcados como fora de estoque - Por fim, faz o parsing de nome do produto, preço, foto, link etc. de cada produto `` e repete o mesmo processo para a próxima categoria de produtos
Escolhendo o ambiente de execução para rodar diariamente
- Em um M1 MacBook Pro, processar um supermercado inteiro levava de 50 minutos a 2 horas e 30 minutos, e executar os três scrapers em paralelo não fazia diferença perceptível
- O notebook era suficiente para desenvolvimento e testes, mas era necessário um ambiente permanente para execução diária
-
Execução apenas em um notebook antigo
- A primeira tentativa foi com um notebook antigo de 2013
- As especificações eram um processador série M dual-core de 2,20 GHz e 4 GB de RAM, depois ampliados para 12 GB de RAM
- Porém, mesmo o supermercado “rápido” levava mais de 2 horas para ser processado, ficando aquém do desempenho esperado
-
Execução na nuvem
- Foram avaliadas instâncias EC2 na AWS com cerca de 4 cores e 8 GB de RAM, mas o custo pesava para um projeto paralelo
- No momento da escrita, a
c5a.xlargeemeu-north-1custava US$ 0,1640 por hora, o equivalente a US$ 118,08 por mês ou US$ 1.416,96 por ano - O servidor equivalente da Hetzner,
cpx31, custava US$ 17,22 (€15,72) por mês, ou US$ 206,64 por ano, cerca de 7 vezes mais barato que a AWS - O ambiente final de execução escolhido foi a Hetzner
Pipeline diário configurado com Concourse
- O notebook antigo não executa a raspagem diretamente; ele atua como um servidor de CI que delega o trabalho a servidores Hetzner
- A ferramenta de CI usada é o Concourse
- O Concourse se apresenta como “a continuous thing-doer”
- Com um modelo declarativo de pipelines e versionamento de entradas, ele busca builds reproduzíveis
- Todas as noites, o pipeline roda na seguinte ordem
- Criar o servidor de scraping
- Executar em paralelo as tarefas de scraping dos 3 supermercados
- Após a conclusão de todas as tarefas, encerrar o servidor para reduzir custos
- Enviar a saída bruta de cada scraper para uma tarefa de transformação
- Carregar os dados transformados no pricewatcher.gr
- Se qualquer etapa falhar, enviar alerta por e-mail
Contornando restrições de IP e exit node do Tailscale
- O supermercado usado nos testes funcionava normalmente, mas outro supermercado ficava atrás da Akamai e tinha uma regra de firewall ativada que bloqueava requisições de IPs não residenciais
- A estrutura necessária era o inverso de uma VPN comum: fazer com que as requisições parecessem sair do IP real de casa
- Com o Tailscale, vários dispositivos foram conectados como se estivessem na mesma rede
- Ao designar um dispositivo como exit node no Tailscale, é possível configurar as requisições de outros dispositivos para saírem por esse nó
- O notebook antigo também passou a atuar como exit node para o tráfego de scraping
- O fato de o ISP usado empregar CGNAT, de modo que o IP público não fica vinculado apenas a uma pessoa específica e é compartilhado com outros clientes do ISP, continua sendo uma variável
Tipos de falha e formas de detecção
- Essa configuração operou por um ano e meio e, no geral, funcionou de forma confiável
- Projetos de scraping inevitavelmente ficam sujeitos a mudanças feitas pelos desenvolvedores dos sites-alvo
- As falhas se dividem em dois tipos principais
-
Mudanças que quebram tudo
- São casos em que uma mudança no site faz o scraper falhar imediatamente
- Alguns exemplos são
- Uma pesquisa ser adicionada, exigindo clicar em mais um botão
- O layout mudar completamente, exigindo uma grande refatoração do scraper
-
Mudanças que não quebram tudo
- Casos em que o scraper continua rodando normalmente, mas a interpretação dos dados fica errada, são mais complicados
- Por exemplo, se o formato de exibição de preços mudar e separar a parte decimal com ``, uma batata chips de €1,99 pode ser parseada como €199
- Para capturar esse tipo de mudança, a etapa de transformação foi configurada para validar a entrada da forma mais rigorosa possível
- Como a execução é diária, há tempo para verificar o problema, mas se algo quebrar durante férias, isso pode virar uma fonte de preocupação
Otimizando tempo de execução e estabilidade
- A arquitetura geral se manteve quase igual desde o início, mas várias partes foram alteradas para aumentar a confiabilidade e reduzir trabalho manual
- As melhorias aplicadas incluíram
- Alertas por e-mail em caso de falha
- Heurísticas que enviam alerta quando a contagem de produtos de um supermercado específico fica alta ou baixa demais
- Timeouts
- Retentativas que não recomeçam tudo do zero
- O maior gargalo era o tempo de execução do scraping
- Quanto mais demorado, maior o custo
- Quando era preciso tentar de novo após uma falha, o incômodo aumentava
-
Usar um servidor maior
- O servidor foi trocado de 4 vCPUs e 16 GB de RAM para 8 vCPUs e 16 GB de RAM
- O tempo de execução caiu cerca de 20%, chegando a um nível parecido com o desempenho obtido no MBP
- Como o servidor de scraping é usado por cerca de apenas 2 horas, a diferença de preço era desprezível
-
Buscar menos coisas
- Foi usado
page.routedo Playwright para bloquear requisições de imagens - Requisições
.pnge.jpgsão abortadas, de modo que imagens não sejam baixadas durante o carregamento dos produtos - Essa abordagem torna o scraping mais rápido e também pode reduzir um pouco a largura de banda e os custos do site-alvo
- Foi usado
Custos em 31 execuções
- Segundo a fatura recente da Hetzner, os custos foram os seguintes
- 31 servidores criados: €4,94
- 31 endereços IPv4 recebidos por esses servidores: €0,09
- Os dados de scraping são armazenados no Cloudflare R2
- Como ainda não passou do nível gratuito de 10 GB do Cloudflare R2, o custo de armazenamento é €0,00
- O pipeline completo funciona combinando Playwright, Hetzner, Concourse, Tailscale e Cloudflare R2 para acompanhar diariamente as variações de preços de supermercados
1 comentários
Opiniões no Hacker News
Estou fazendo algo parecido para a New Zealand desde o começo deste ano. Uso Playwright/Typescript para raspar os dados e salvá-los como arquivos Parquet em armazenamento na nuvem, mas ainda não os exibi em uma interface.
A maior parte do trabalho vai para contornar serviços de proxy reverso como Akamai e Cloudflare. Quando comecei, achei que ninguém estivesse fazendo isso, mas agora sei de pelo menos 3 startups na NZ fazendo a mesma coisa. Parece que a inflação desencadeou bastante inovação por aqui.
O padrão é o esperado. Os supermercados usam suas táticas habituais para tornar os preços o mais complexos possível e, com variações de preço em “dente de serra”, separam quem tem pouco tempo de quem tem pouco dinheiro. Muitas vezes também separam clientes fiéis à marca de clientes sensíveis a preço; se há 3 marcas populares de chocolate, a cada semana só uma delas fica a um preço razoável.
Fiquei especialmente curioso com a parte “separam clientes fiéis à marca de clientes sensíveis a preço. Se há 3 marcas populares de chocolate, a cada semana só uma delas é vendida a um preço razoável”.
Na AU, muitos desses scrapers aparecem e desaparecem, mas normalmente os grandes supermercados acabam bloqueando. É um ciclo recorrente de utilidade e de “por que isso não existe?”, quando na verdade já existiu várias vezes.
Levei menos de 20 horas para criar o crawler e a infraestrutura de suporte.
Além disso, é ainda mais útil porque há apenas um marketplace que reúne os preços de várias lojas.
Ótimo texto. Passei por problemas parecidos no site de comparação de preços de lentes de contato que opero, https://lenspricer.com/, que funciona em cerca de 30 países. Concordo que sites mudando o HTML são uma enorme dor de cabeça.
Um dos maiores obstáculos no começo foi associar o mesmo produto em mais de 100 sites. Você imaginaria que o nome do produto seria único, mas todo mundo escreve com pequenas variações próprias. A maior parte é tratada com regex, mas muitos precisaram ser mapeados manualmente; em alguns casos usei IA, mas validei tudo pessoalmente.
Criar scrapers e a infraestrutura é relativamente fácil. O difícil é manter todos os scrapers e, quando um produto desaparece de um site, descobrir se foi erro do scraper, bloqueio, mudança no site ou se o site estava em manutenção no momento do crawling.
É um projeto divertido, mas às vezes difícil, com muitos problemas irritantes de corrigir.
Os preços variam muito, e a cobertura normalmente é algo como 30% via reembolso pelo correio.
O número de lojas também parece muito pequeno; talvez todos os links sejam patrocinados. Encontrei preços mais baixos no idealo.de.
A Costco é bem famosa por isso: quase todos os eletrônicos vendidos nas lojas, e muitos outros produtos, têm SKUs personalizados. Muitas vezes a composição do produto também é um pouco diferente.
Criei um site parecido que recebeu bastante atenção na minha cidade. Ele raspa dados de apps e sites, e roda em um único servidor da Linode com 2 GB de RAM, 5 IPv4 e 1000 IPv6 gratuitos.
Todos os produtos são coletados em intervalos de no máximo 40 minutos, com média de cerca de 25 minutos. Uso curl-impersonate e raspo JSON sempre que possível. 90% do mercado fornece preços por chamadas Ajax, e os 10% restantes são fáceis de parsear em HTML com regex.
Dá para ver em https://www.economizafloripa.com.br
https://www.economizafloripa.com.br/?q=parceria-comercial
Ao ver essa página, o projeto passou de “uma ferramenta útil para pessoas retomarem o controle de empresas que vendem itens essenciais” para “mais uma tentativa de ganhar dinheiro”. Claro, isso é escolha deles, mas ao ler a página inicial eu esperava uma motivação mais ética.
Ótimo texto.
Acho melhor separar scraping e parsing em processos distintos. Se você guardar o JSON ou HTML original, sempre pode voltar, corrigir o parser e reaplicar.
Como parte do meu projeto de mestrado, criei um sistema e um site semelhantes para os Países Baixos: https://www.superprijsvergelijker.nl/
A maior parte do scraping do meu projeto é feita enviando chamadas HTTP simples para APIs JSON. Alguns sites usam uma instância do Playwright para obter um cookie de sessão válido e contornar proteções contra bots e CAPTCHA. O restante dos crawlers/scrapers, parsers e da API foi feito em Haskell e roda no AWS ECS. O site é em NextJS.
O principal desafio que continuo tentando resolver é vincular produtos de diferentes supermercados para listar os preços em uma única tela. Há um exemplo aqui: https://www.superprijsvergelijker.nl/supermarkt-aanbieding/6...
Se o produto fornecer pelo menos um número de código de barras correto, geralmente funciona bem.
Acho que os dois grandes supermercados da Austrália poderiam criar uma estrutura de duopólio com precificação anticompetitiva só operando algoritmos de IA de análise de preços em ambos os lados. No fim, é bem provável que os algoritmos caminhem para cooperar a fim de maximizar o lucro.
Isso pode ser feito legalmente usando apenas preços obtidos publicamente, e também ilegalmente se compartilharem custos de fornecimento ou dados de lucro por produto. O resultado provavelmente seria parecido.
Duas IAs treinadas fariam uma análise de regressão multidimensional, talvez hiperdimensional, para maximizar o lucro de formas estranhas, e os consumidores acabariam bancando os lucros maximizados de empresas que, na aparência, são concorrentes. Se for possível obter dados de preços desse jeito, não é preciso muita coisa para rodar duas implementações de machine learning focadas em um duopólio.
A lógica é que, se todos os preços forem públicos, os consumidores acabam pagando preços mais altos. Os supermercados passariam a ajustar os preços para o ponto em que todos obtêm lucro máximo.
Por aqui, os supermercados há anos empregam “caçadores de preços”, pessoas que vão às lojas concorrentes e registram o preço de todos os itens.
Na Noruega, é comum ver o supermercado A dar desconto em um item específico em uma semana, e então o supermercado B oferecer um desconto parecido na semana seguinte ou na outra para atrair clientes.
Para detectar esse tipo de mudança, acho que daria para adicionar verificações automáticas. Por exemplo, não sincronizar mudanças de preço/produto se uma verificação de bom senso falhar.
Poderia haver critérios como: cada preço não pode variar mais de 100%, e o número de produtos ativos não pode variar mais de 20%.
Em vez disso, usei esse tipo de heurística para confirmar se o scraping foi bem-sucedido. Por exemplo, verificar se o número de produtos coletados hoje está dentro de cerca de 10% da média dos últimos 7 dias.
O difícil não é o scraping em si, mas contornar mecanismos de bloqueio cada vez mais sofisticados.
É preciso ficar alternando proxies residenciais, usar os bem avaliados e evitar expor padrões de scraping de dados. Alguns supermercados não mostram as requisições de rede na aba Network, então não dá para simplesmente pegar as respostas da API.
Mesmo tentando fazer um ataque man-in-the-middle no app móvel para ver as requisições de rede e os dados, você é bloqueado se não se disfarçar corretamente.
Tentei fazer isso, mas concluí que não valia a pena por causa do custo e do trabalho contínuo de desenvolvimento. Na prática, alguns serviços de comparação de preços de supermercados simplesmente usam mão de obra barata para coletar os dados.
Como isto é um projeto paralelo, se a demanda de tempo ficar frequente demais, pretendo simplesmente encerrar e publicar todo o código e os dados.
Mas fico curioso para saber como é possível que as requisições de rede não apareçam na aba Network.
A parte mais difícil para mim é vincular e comparar produtos entre os supermercados.
Seria bom haver transparência de preços dos produtos. Isso tornaria muito mais fácil acompanhar esse processo por loja e por região.
Por exemplo, daria para comparar o preço do leite de aveia, um substituto do leite, por CEP e por supermercado. Também daria para acompanhar a “reduflação”, quando o preço fica igual, mas a quantidade diminui.
Nesse sentido, parece que vocês acompanham preços, mas fico curioso se também verificam o custo por grama ou por onça. Fabricantes ou lojas podem manter o preço igual e dar menos quantidade ao consumidor; fico curioso se a ferramenta conseguiria detectar isso.
Ainda assim, é um recurso que daria para adicionar sem problema e, se tivesse havido reduflação, seria possível mostrar o momento em que ela aconteceu.
No mercado sueco, fazemos isso há mais de 8 anos. Existe um site chamado https://www.matspar.se/, onde os clientes podem navegar por todos os produtos das principais lojas online, comparar preços e colocar os itens desejados no carrinho
No fim, eles comparam o valor total do carrinho, incluindo o frete, e podem exportar o carrinho para a loja desejada para fazer o pedido
Sou cofundador e CTO atual, então fiz muito scraping e manutenção ao longo desse tempo. Estamos raspando mais de 30 milhões de preços por dia
Segundo dados públicos, a receita da empresa é de cerca de US$ 400 mil e há 6 funcionários: https://www.allabolag.se/5590076351/matspar-i-sverige-ab
Quando me mudei para uma região nova, eu fazia rastreamento de preços, mas hoje acho muito mais fácil comprar em mercados que mantêm preços sempre baixos ou em duas grandes lojas
Na Europa, provavelmente seriam Aldi/Lidl
Nos EUA, poderiam ser Costco/Trader Joe's
Online, há CamelCamelCamel/Amazon. Não para alimentos, mas para saúde/beleza/alguns eletrônicos
Quando é possível comprar diretamente do fabricante, às vezes isso é melhor. Por exemplo, comprei em grande quantidade uma marca específica de sabonete de que gosto em um site de atacado, e saiu por menos da metade do preço de varejo. Shampoo comprado diretamente em galões também era muito mais barato do que em qualquer varejista
Trader Joe's tem qualidade mais alta, mas em geral é mais caro