# Raspar o Google Maps é fácil; o dado útil não é

> Um teste real com o gosom/google-maps-scraper mostra que o gargalo não está em extrair o Maps, e sim na segunda camada de coleta (sites, e-mails) e na limpeza. Arquitetura, flags, telemetria e limites legais.

- Author: Elzo Brito
- Language: pt-BR
- Published: 2026-10-26
- Canonical URL: https://elzobrito.com/blog/raspar-o-google-maps-e-facil-o-dado-util-nao-e/
- Translation: https://elzobrito.com/en/blog/scraping-google-maps-is-easy-useful-data-is-not/

---
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.
