Eu queria saber quantas escolas particulares existem em Mairiporã (SP) com telefone, site e e-mail, e queria saber isso sem abrir o Maps e copiar ficha por ficha. Pedi ao Grok Bot, que delegou a execução ao agy, o agente de linha de comando do Google Antigravity, e o caminho escolhido foi o gosom/google-maps-scraper, um projeto em Go bastante usado para esse tipo de levantamento. O comando que funcionou foi este: docker run --rm --name gmaps-cli -e DISABLE_TELEMETRY=1 -v /workspace/gmaps-scraper:/work gosom/google-maps-scraper -input /work/query.txt -results /work/out.csv -lang pt -depth 2 -email -exit-on-inactivity 3m -c 4. Em 131 segundos ele devolveu 23 linhas; 19 tinham telefone, 15 tinham site e apenas 1 tinha e-mail. Quando limpei o resultado à mão, sobraram 8 escolas particulares reais. A ferramenta funcionou. O que o número revela é que extrair o Maps é a parte barata do problema, e que o dado útil mora em outro lugar.
Antes de rodar qualquer coisa, li o código. Não por paranoia, mas porque um scraper é, por definição, um programa que abre um navegador, visita páginas arbitrárias e escreve em disco, e eu prefiro saber o que ele faz além disso. A arquitetura é limpa: o binário em Go usa o scrapemate, um framework de jobs do mesmo autor, sobre Playwright, de modo que a coleta é uma fila de jobs encadeados. Um job de busca rola a lista de resultados, cada resultado gera um job de ficha (place), e, se -email estiver ligado, cada ficha com site gera um job de extração de e-mail. O mesmo núcleo é exposto por vários runners: o de arquivo (CLI, uma query por linha em -input), o web (-web, com UI e API REST, persistência em SQLite dentro de -data-folder), o de banco (-dsn com PostgreSQL, que permite separar produtores de consumidores com -produce e escalar horizontalmente) e até um runner para AWS Lambda. Para mim essa é a decisão de engenharia mais acertada do projeto: a semântica da coleta é uma só, e o modo de execução é só a borda que decide de onde vêm as queries e para onde vão os resultados.
Encontrei também uma coisa que não estava na propaganda. Em runner/runner.go, a função de telemetria envia eventos anônimos de execução para o PostHog (eu.i.posthog.com) a não ser que a variável DISABLE_TELEMETRY=1 esteja definida. Não é código malicioso, e o README é honesto sobre isso, mas é opt-out, e opt-out numa ferramenta que você vai rodar na sua máquina contra um serviço de terceiro é o tipo de padrão que eu prefiro desligar explicitamente. Por isso o -e DISABLE_TELEMETRY=1 está no comando. O servidor web, por sua vez, define cabeçalhos como Content-Security-Policy e X-Frame-Options: DENY; mesmo assim publiquei a porta só em 127.0.0.1:8080, porque uma UI sem autenticação não tem nada que fazer exposta na rede.
As flags parecem cosméticas, mas cada uma é um contrato sobre custo. -depth não é profundidade de link, é quantas vezes o scraper rola a lista de resultados da busca; o padrão é 10, e eu usei 2 porque, para um município pequeno, a lista se esgota cedo e cada rolagem extra é tempo de navegador para nada. -c define a concorrência do scrapemate, isto é, quantos jobs correm em paralelo. Aqui há um detalhe curioso: o texto de ajuda diz que o padrão é metade dos núcleos, mas o código é min(runtime.NumCPU()/2, 1), o que na prática resulta em 1. Quem confia no texto da ajuda roda serialmente sem perceber, e por isso passei -c 4 explicitamente. Já -exit-on-inactivity 3m resolve um problema que o modo CLI tem por natureza: uma fila de jobs não sabe quando acabou, porque qualquer job pode gerar outros. Sem essa flag (o padrão é 0, desligado) o processo pode ficar esperando indefinidamente; com ela, três minutos sem atividade encerram a execução. É um critério de parada empírico, não uma garantia de completude, e convém tratá-lo assim.
O e-mail é o ponto em que a expectativa e o código mais se distanciam. A flag se chama -email e sugere algo como “encontre o e-mail da empresa”, mas o que gmaps/emailjob.go faz é muito mais estreito: busca a URL de site que consta na ficha, procura links mailto: no HTML e, na falta deles, aplica uma regex ao corpo da resposta. O job é criado com MaxRetries igual a zero. Não há rastreamento da página de contato, não há espera por JavaScript que monte o rodapé, não há segunda tentativa quando o site de uma escola, hospedado num plano barato, demora a responder. Isso explica o 1 em 23 melhor do que qualquer teoria sobre o Google esconder dados: o Maps quase nunca guarda e-mail, e a segunda camada de coleta depende de sites lentos, montados no cliente, ou que simplesmente publicam o contato num formulário. A falha não é um bug, é a consequência direta de uma decisão de custo. Uma extração ingênua e de tentativa única é barata e escala; uma extração cuidadosa seria outro programa.
O modo REST deixou essa fronteira ainda mais visível. Subi o container com -data-folder montado, enviei o mesmo job via POST /api/v1/jobs com depth: 2, email: true e max_time: 300, e ele parou em 315 segundos com 18 linhas, zero e-mails. Os mesmos 23 locais foram encontrados; o que faltou foram os cinco que ainda esperavam na fila de e-mail quando o tempo estourou. A diferença entre os modos, portanto, não é de qualidade de extração, é de critério de parada: o CLI encerra por inatividade e escreve tudo que terminou, o job web encerra por relógio e descarta o que estava em voo. Se eu precisasse de previsibilidade operacional, escolheria o web; se precisasse de completude, o CLI com inatividade generosa.
A limpeza foi a parte mais cara, e é justamente a que nenhuma flag resolve. A busca “escola particular em Mairiporã SP” trouxe escolinhas de esporte, escola de informática, escola de idiomas, faculdades, um polo de universidade aberta, aulas particulares e até uma repartição pública, tudo dentro de Mairiporã, porque a busca do Maps é por relevância geográfica e semântica, não por categoria jurídica. Das 23 linhas, 8 eram escolas particulares de fato. As outras quinze linhas eram corretas como dado (endereço, telefone e nota batiam com a ficha) e erradas como resposta. Esse é o problema conceitual que eu acho que muita gente ignora quando fala de scraping: o scraper garante fidelidade à ficha, não pertinência à pergunta. A pertinência exige classificação, e classificação exige critério humano ou um segundo passo explícito.
Há ainda a fronteira que não é técnica, e o próprio README a reconhece ao trazer um aviso legal dizendo que o uso não autorizado pode violar termos. Os dados que eu coletei são de pessoas jurídicas, públicos na ficha, e o uso foi um levantamento pontual; ainda assim, telefone e e-mail podem identificar pessoas físicas (o dono de uma escolinha muitas vezes atende no celular pessoal), e aí a LGPD entra com finalidade, necessidade e base legal. Uma lista dessas não autoriza disparo em massa só porque foi fácil de montar. Para mim a regra é simples de enunciar e difícil de seguir: coletar o mínimo, guardar pelo mínimo, e nunca transformar a facilidade técnica em justificativa de uso.
O que fico do experimento é uma inversão de prioridade. Eu esperava que a dificuldade estivesse em contornar o Maps, e o gosom/google-maps-scraper resolve isso com uma arquitetura de filas bem separada dos modos de execução, num container de um comando. A dificuldade real estava depois: numa extração de e-mail que, por desenho, desiste no primeiro site lento, num critério de parada que é heurística, e numa busca que devolve o que é próximo, não o que é verdadeiro. Raspar é infraestrutura. Saber o que se raspou continua sendo trabalho.