Voltar ao blog
Guias e Tutoriais2026-03-15·12 min de leitura

Raspe Avaliações do Google Maps com Python: Guia 2025

Por Ibrahim DemolCEO IBLeadAtualizado em 12 de junho de 2026

Se você quer raspar avaliações do Google Maps com Python, está lidando com um dos alvos de scraping mais complicados da web. O Google carrega avaliações dinamicamente, altera sua estrutura HTML e detecta ativamente bots. Este guia cobre dois métodos funcionais — Playwright e Selenium — com código completo, técnicas de anti-detecção e notas honestas sobre o que quebra e por quê.

Sem enrolação. Apenas código que funciona.


O Que É Raspar Avaliações do Google?

Raspar avaliações do Google é a extração automatizada de dados de avaliações de clientes de listagens do Google Maps e Google Business. Em vez de copiar avaliações manualmente, um script visita páginas de negócios e puxa os dados para você.

Cada avaliação contém campos úteis:

  • Avaliação em estrelas (1–5)
  • Texto da avaliação
  • Nome do avaliador
  • Data da postagem
  • Resposta do negócio (se houver)

Esses dados têm valor real. Monitoramento de reputação, análise competitiva, rastreamento de sentimentos, qualificação de leads — tudo começa com dados brutos de avaliações.

Por Que Não Usar a API Oficial do Google?

A API Google Places fornece avaliações, mas com limites rigorosos. Você obtém no máximo 5 avaliações por negócio. Sem dados históricos. Sem avaliações de concorrentes. Os preços aumentam rapidamente uma vez que você excede o nível gratuito.

O web scraping dá acesso a todas as avaliações públicas, sem um limite artificial. O trade-off: você precisa lidar com os sistemas anti-bot do Google por conta própria.


Por Que Python Para Esta Tarefa?

Python tem o melhor ecossistema para automação de navegadores e extração de dados. Três bibliotecas fazem a maior parte do trabalho pesado:

  • Playwright — moderno, rápido, pronto para async, com recursos de stealth embutidos
  • Selenium — testado em batalha, enorme comunidade, máxima compatibilidade
  • BeautifulSoup — parsing leve de HTML uma vez que você tenha o conteúdo bruto

As avaliações do Google carregam via JavaScript. Scrapers estáticos (requests + BeautifulSoup sozinhos) não funcionarão aqui. Você precisa de um navegador real que execute JS, role a página e clique em botões — exatamente o que o Playwright e o Selenium fazem.


O Desafio Principal: Por Que o Google Reage

Antes de escrever uma única linha de código, entenda com o que você está lidando.

Carregamento Dinâmico de Conteúdo

O Google não serve todas as avaliações no HTML inicial. O primeiro carregamento da página mostra de 10 a 20 avaliações. Mais carregam à medida que você rola. Cada lote aciona solicitações de rede separadas. Seu scraper deve simular a rolagem para acionar esses carregamentos.

Camadas de Detecção de Bots

O Google executa vários sistemas de detecção simultaneamente:

  • Fingerprinting do navegador — resolução de tela, fontes, fuso horário, idioma
  • Análise comportamental — padrões de movimento do mouse, velocidade de rolagem, tempo de clique
  • Reconhecimento de padrão de solicitação — frequência de solicitações não humanas
  • Reputação de IP — sinalização de IPs que enviam muitas solicitações

Acertar qualquer um desses gatilhos e você verá CAPTCHAs, resultados vazios ou um bloqueio total.

Estrutura HTML em Constante Mudança

O Google atualiza seu frontend regularmente. Um seletor CSS que funciona hoje pode retornar zero resultados na próxima semana. Scrapers robustos usam múltiplos seletores de fallback para cada campo.


Método 1: Playwright (Recomendado para 2025)

Playwright é o melhor ponto de partida para novos projetos. É 2–3x mais rápido que o Selenium, tem suporte async embutido e lida com anti-detecção com menos configuração manual.

Configuração

python -m venv google_scraper_env
source google_scraper_env/bin/activate  # Windows: google_scraper_env\Scripts\activate
pip install playwright pandas emoji beautifulsoup4 lxml
playwright install chromium

Scraper Completo do Playwright

from playwright.sync_api import sync_playwright
import pandas as pd
import re
import emoji
import logging
import time
import random

logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)

class GoogleReviewsScraper:
    def __init__(self, headless=True):
        self.headless = headless
        self.reviews_data = []

    def clean_text(self, text):
        text = emoji.replace_emoji(text, replace='')
        text = re.sub(r'\s+', ' ', text).strip()
        return text

    def random_delay(self, min_delay=1, max_delay=3):
        time.sleep(random.uniform(min_delay, max_delay))

    def initialize_browser(self):
        playwright = sync_playwright().start()
        browser = playwright.chromium.launch(
            headless=self.headless,
            args=[
                '--disable-blink-features=AutomationControlled',
                '--disable-extensions',
                '--no-sandbox',
                '--disable-setuid-sandbox',
                '--disable-dev-shm-usage',
                '--disable-gpu'
            ]
        )
        context = browser.new_context(
            user_agent='Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
            viewport={'width': 1366, 'height': 768}
        )
        page = context.new_page()
        page.add_init_script("""
            Object.defineProperty(navigator, 'webdriver', {
                get: () => undefined,
            });
        """
        )
        return playwright, browser, page

    def search_business(self, page, business_name):
        try:
            page.goto("https://www.google.com/maps", wait_until="networkidle")
            self.random_delay(2, 4)
            search_box = page.locator("input[id='searchboxinput']")
            search_box.fill(business_name)
            search_box.press("Enter")
            page.wait_for_timeout(5000)
            logger.info(f"Searched for: {business_name}")
            return True
        except Exception as e:
            logger.error(f"Error searching: {e}")
            return False

    def navigate_to_reviews(self, page):
        try:
            reviews_tab = page.get_by_role("tab", name=re.compile("Reviews|reviews", re.IGNORECASE))
            if reviews_tab.is_visible():
                reviews_tab.click()
                page.wait_for_timeout(3000)
                logger.info("Navigated to reviews section")
                return True
            logger.warning("Reviews tab not found")
            return False
        except Exception as e:
            logger.error(f"Error navigating to reviews: {e}")
            return False

    def scroll_and_load_reviews(self, page, max_reviews=100):
        loaded_reviews = 0
        scroll_attempts = 0
        max_scroll_attempts = 20

        while loaded_reviews < max_reviews and scroll_attempts < max_scroll_attempts:
            try:
                page.evaluate("window.scrollTo(0, document.body.scrollHeight)")
                self.random_delay(2, 4)
                current_reviews = page.locator('[data-review-id]').count()
                if current_reviews > loaded_reviews:
                    loaded_reviews = current_reviews
                    logger.info(f"Loaded {loaded_reviews} reviews...")
                    scroll_attempts = 0
                else:
                    scroll_attempts += 1
                try:
                    more_button = page.locator("button", has_text=re.compile("more|More", re.IGNORECASE))
                    if more_button.is_visible():
                        more_button.click()
                        self.random_delay(2, 3)
                except:
                    pass
            except Exception as e:
                logger.error(f"Error during scrolling: {e}")
                break

        logger.info(f"Total reviews found: {loaded_reviews}")
        return loaded_reviews

    def extract_review_data(self, page):
        reviews = []
        try:
            review_elements = page.locator('[data-review-id]').all()
            for element in review_elements:
                try:
                    review_data = {}

                    name_element = element.locator('div[class*="name"] span, div[class*="Name"] span').first
                    review_data['reviewer_name'] = name_element.inner_text() if name_element.is_visible() else "Anonymous"

                    rating_element = element.locator('[role="img"][aria-label*="star"]').first
                    if rating_element.is_visible():
                        rating_text = rating_element.get_attribute('aria-label')
                        rating_match = re.search(r'(\d+)', rating_text)
                        review_data['rating'] = int(rating_match.group(1)) if rating_match else None

                    text_elements = element.locator('span[class*="review-text"], div[class*="review-text"]').all()
                    review_text = ""
                    for text_elem in text_elements:
                        if text_elem.is_visible():
                            review_text += text_elem.inner_text() + " "
                    review_data['review_text'] = self.clean_text(review_text.strip())

                    date_element = element.locator('span[class*="date"], div[class*="date"]').first
                    review_data['review_date'] = date_element.inner_text() if date_element.is_visible() else "Unknown"

                    if review_data['review_text']:
                        reviews.append(review_data)
                except Exception as e:
                    logger.warning(f"Error on individual review: {e}")
                    continue

            logger.info(f"Extracted {len(reviews)} reviews")
            return reviews
        except Exception as e:
            logger.error(f"Extraction error: {e}")
            return []

    def scrape_reviews(self, business_name, max_reviews=100):
        playwright, browser, page = self.initialize_browser()
        try:
            if not self.search_business(page, business_name):
                return []
            if not self.navigate_to_reviews(page):
                return []
            self.scroll_and_load_reviews(page, max_reviews)
            reviews = self.extract_review_data(page)
            self.reviews_data = reviews
            return reviews
        except Exception as e:
            logger.error(f"Scraping failed: {e}")
            return []
        finally:
            browser.close()
            playwright.stop()

    def save_to_csv(self, filename="google_reviews.csv"):
        if self.reviews_data:
            df = pd.DataFrame(self.reviews_data)
            df.to_csv(filename, index=False, encoding='utf-8')
            logger.info(f"Saved to {filename}")
        else:
            logger.warning("No reviews to save")

if __name__ == "__main__":
    scraper = GoogleReviewsScraper(headless=False)
    business_name = "Starbucks Times Square New York"
    reviews = scraper.scrape_reviews(business_name, max_reviews=50)
    if reviews:
        scraper.save_to_csv(f"reviews_{business_name.replace(' ', '_')}.csv")
        print(f"Scraped {len(reviews)} reviews.")
    else:
        print("No reviews scraped.")

O Que Este Código Faz

  • Bandeiras de stealth escondem a impressão digital de automação da camada de detecção do Google
  • Atrasos aleatórios entre 1–4 segundos imitam o ritmo de navegação humano
  • Loop de rolagem continua carregando até atingir max_reviews ou ficar sem conteúdo
  • Múltiplos seletores de fallback lidam com as frequentes mudanças de HTML do Google
  • Exportação CSV fornece um arquivo limpo pronto para análise ou importação em qualquer ferramenta

Método 2: Selenium (Alternativa Confiável)

Selenium tem sido o padrão para automação de navegadores por mais de uma década. É mais lento que o Playwright, mas tem uma comunidade maior e mais documentação.

Quando Escolher Selenium

  • Você está trabalhando com infraestrutura legada que já o utiliza
  • Você precisa de máxima compatibilidade entre versões de navegadores
  • Sua equipe já tem experiência com Selenium

Configuração

pip install selenium pandas

Você também precisará do ChromeDriver correspondente à sua versão do Chrome. O Selenium moderno (4.6+) gerencia automaticamente o driver.

Scraper Completo do Selenium

from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.common.action_chains import ActionChains
from selenium.common.exceptions import TimeoutException, NoSuchElementException
import pandas as pd
import time
import random
import re
import logging

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

class SeleniumGoogleReviewsScraper:
    def __init__(self, headless=True):
        self.headless = headless
        self.driver = None
        self.wait = None
        self.reviews_data = []

    def setup_driver(self):
        options = Options()
        if self.headless:
            options.add_argument("--headless")
        options.add_argument("--disable-blink-features=AutomationControlled")
        options.add_experimental_option("excludeSwitches", ["enable-automation"])
        options.add_experimental_option('useAutomationExtension', False)
        options.add_argument("--no-sandbox")
        options.add_argument("--disable-dev-shm-usage")
        options.add_argument("--window-size=1366,768")
        options.add_argument("--user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36")

        self.driver = webdriver.Chrome(options=options)
        self.driver.execute_script("Object.defineProperty(navigator, 'webdriver', {get: () => undefined,});")
        self.wait = WebDriverWait(self.driver, 20)
        logger.info("Driver initialized")

    def random_delay(self, min_s=1, max_s=3):
        time.sleep(random.uniform(min_s, max_s))

    def search_google_maps(self, business_name):
        try:
            self.driver.get("https://www.google.com/maps")
            self.random_delay(2, 4)
            search_box = self.wait.until(EC.presence_of_element_located((By.ID, "searchboxinput")))
            search_box.clear()
            for char in business_name:
                search_box.send_keys(char)
                time.sleep(random.uniform(0.05, 0.15))
            search_box.submit()
            self.wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, "[data-value='Reviews']")))
            logger.info(f"Searched for: {business_name}")
            return True
        except TimeoutException:
            logger.error("Timeout on search")
            return False

    def click_reviews_tab(self):
        try:
            reviews_tab = self.wait.until(EC.element_to_be_clickable((By.CSS_SELECTOR, "[data-value='Reviews']")))
            self.driver.execute_script("arguments[0].scrollIntoView(true);")
            self.random_delay(1, 2)
            reviews_tab.click()
            self.wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, "[data-review-id]")))
            logger.info("Reviews tab clicked")
            return True
        except TimeoutException:
            logger.error("Reviews tab not found")
            return False

    def scroll_to_load_reviews(self, target_reviews=100):
        last_height = self.driver.execute_script("return document.body.scrollHeight")
        reviews_loaded = 0
        scroll_attempts = 0

        while reviews_loaded < target_reviews and scroll_attempts < 30:
            self.driver.execute_script("window.scrollTo(0, document.body.scrollHeight);")
            self.random_delay(2, 4)
            try:
                show_more = self.driver.find_element(By.XPATH, "//button[contains(text(), 'more') or contains(text(), 'More')]\")
                if show_more.is_displayed():
                    ActionChains(self.driver).move_to_element(show_more).click().perform()
                    self.random_delay(2, 3)
            except NoSuchElementException:
                pass

            current_count = len(self.driver.find_elements(By.CSS_SELECTOR, "[data-review-id]"))
            if current_count > reviews_loaded:
                reviews_loaded = current_count
                logger.info(f"Loaded {reviews_loaded} reviews...")
                scroll_attempts = 0
            else:
                scroll_attempts += 1

            new_height = self.driver.execute_script("return document.body.scrollHeight")
            if new_height == last_height:
                scroll_attempts += 1
            last_height = new_height

        return reviews_loaded

    def extract_reviews(self):
        reviews = []
        review_elements = self.driver.find_elements(By.CSS_SELECTOR, "[data-review-id]")
        for element in review_elements:
            try:
                review_data = {}
                try:
                    review_data['reviewer_name'] = element.find_element(By.CSS_SELECTOR, "div[class*='name'] span").text.strip()
                except NoSuchElementException:
                    review_data['reviewer_name'] = "Anonymous"
                try:
                    aria_label = element.find_element(By.CSS_SELECTOR, "[role='img'][aria-label*='star']").get_attribute('aria-label')
                    match = re.search(r'(\d+)', aria_label)
                    review_data['rating'] = int(match.group(1)) if match else None
                except NoSuchElementException:
                    review_data['rating'] = None
                try:
                    text_elems = element.find_elements(By.CSS_SELECTOR, "span[class*='review-text']")
                    review_data['review_text'] = " ".join([e.text for e in text_elems if e.text]).strip()
                except NoSuchElementException:
                    review_data['review_text'] = ""
                try:
                    review_data['review_date'] = element.find_element(By.CSS_SELECTOR, "span[class*='date']").text.strip()
                except NoSuchElementException:
                    review_data['review_date'] = "Unknown"

                if review_data['review_text']:
                    reviews.append(review_data)
            except Exception as e:
                logger.warning(f"Review extraction error: {e}")
                continue

        logger.info(f"Extracted {len(reviews)} reviews")
        return reviews

    def scrape_business_reviews(self, business_name, max_reviews=100):
        try:
            self.setup_driver()
            if not self.search_google_maps(business_name):
                return []
            if not self.click_reviews_tab():
                return []
            self.scroll_to_load_reviews(max_reviews)
            reviews = self.extract_reviews()
            self.reviews_data = reviews
            return reviews
        except Exception as e:
            logger.error(f"Scraping failed: {e}")
            return []
        finally:
            if self.driver:
                self.driver.quit()

    def save_to_csv(self, filename="selenium_reviews.csv"):
        if self.reviews_data:
            pd.DataFrame(self.reviews_data).to_csv(filename, index=False, encoding='utf-8')
            logger.info(f"Saved to {filename}")

if __name__ == "__main__":
    scraper = SeleniumGoogleReviewsScraper(headless=False)
    reviews = scraper.scrape_business_reviews("McDonald's Times Square", max_reviews=75)
    if reviews:
        scraper.save_to_csv("mcdonalds_times_square_reviews.csv")
        print(f"Scraped {len(reviews)} reviews.")

Técnicas de Anti-Detecção Que Realmente Funcionam

Ambos os scrapers acima incluem stealth básico. Aqui está o que adicionar quando você precisa ir mais longe.

Rotação de Proxy

Raspar com um único IP é bloqueado rapidamente. Rode proxies para distribuir solicitações:

import random

PROXY_LIST = [
    "http://user:pass@proxy1:port",
    "http://user:pass@proxy2:port",
    "http://user:pass@proxy3:port",
]

def get_random_proxy():
    return random.choice(PROXY_LIST)

# No Playwright:
context = browser.new_context(proxy={"server": get_random_proxy()})

Proxies residenciais funcionam melhor do que proxies de datacenter para o Google especificamente. IPs de datacenter são sinalizados mais rapidamente.

Rotação de User Agent

USER_AGENTS = [
    'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
    'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
    'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
    'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/120.0',
]

def get_random_ua():
    return random.choice(USER_AGENTS)

Digitação Semelhante à Humana

def human_type(element, text):
    for char in text:
        element.send_keys(char)
        time.sleep(random.uniform(0.05, 0.2))

Digitar em velocidade uniforme é um sinal de bot. Atrasos variáveis por caractere parecem humanos.

Aquecimento de Sessão

Não vá direto para o Google Maps. Visite o Google Search primeiro, espere alguns segundos e depois navegue para o Maps. Sessões frias que pulam diretamente para alvos de scraping são sinalizadas com mais frequência.


Lidando com Conteúdo Dinâmico

As avaliações do Google usam rolagem infinita — sem números de página, sem botão "próximo". Seu scraper precisa continuar rolando até que:

  1. Ele atinja seu alvo de max_reviews, ou
  2. Nenhuma nova avaliação carregue após várias tentativas de rolagem

O código acima lida com isso com um contador de consecutive_failures. Após 5 rolagens sem novas avaliações, ele para. Essa é a abordagem certa — não loop para sempre.

Expandindo Avaliações Truncadas

Avaliações longas são cortadas com um link "Mais". Para obter o texto completo:

def expand_truncated_reviews(page):
    expand_buttons = page.locator("button:has-text('More'), span:has-text('...')")
    count = expand_buttons.count()
    for i in range(min(count, 100)):
        btn = expand_buttons.nth(i)
        if btn.is_visible():
            btn.click()
            page.wait_for_timeout(300)
    logger.info(f"Expanded {count} truncated reviews")

Execute isso após carregar todas as avaliações, antes da extração.


Considerações Legais e Éticas

Raspar dados visíveis publicamente é geralmente legal na maioria das jurisdições. A decisão HiQ v. LinkedIn de 2022 nos EUA confirmou que raspar dados públicos não viola a Lei de Fraude e Abuso de Computadores.

Dito isso, algumas regras se aplicam:

  • Não sobrecarregue servidores. Mantenha as solicitações abaixo de 10 por minuto para uso casual.
  • Respeite o robots.txt. O robots.txt do Google restringe alguns caminhos — verifique-o.
  • Não republifique conteúdo raspado literalmente. Agregue e analise, não copie e cole.
  • Evite dados pessoais. Nomes de avaliadores são públicos, mas não construa perfis sobre indivíduos.
  • Uso comercial precisa de revisão legal. Se você está vendendo dados raspados, consulte um advogado.

A abordagem mais segura: raspe para análise interna, não para redistribuição.


Quando Raspar com Python Não É a Ferramenta Certa

Escrever e manter um scraper do Google Maps exige um esforço real. O Google muda sua estrutura HTML regularmente. Seletores quebram. Medidas anti-bot evoluem. Você gastará tempo depurando, não analisando.

Se você precisa de dados de avaliações do Google Maps em grande escala — em centenas ou milhares de negócios — um banco de dados pré-indexado é mais rápido e confiável do que um scraper DIY.

O IBLead indexa mais de 50 milhões de negócios em 37 países, com até 500 avaliações do Google por listagem: texto completo, avaliação em estrelas, data e nome do avaliador. Os dados são atualizados semanalmente e exportam instantaneamente para CSV. Sem infraestrutura de scraping para manter, sem proxies para gerenciar, sem seletores para corrigir quando o Google atualiza seu frontend.

Para pesquisas pontuais em um punhado de negócios, a abordagem em Python deste guia funciona bem. Para geração de leads contínua ou monitoramento de reputação em larga escala, $52 por 10.000 leads é difícil de superar.

Comece grátis — 200 créditos incluídos


FAQ

Quantas avaliações posso raspar por dia sem ser bloqueado?

Comece conservador: 100–500 avaliações por dia, em 5–10 negócios, com atrasos de 2–3 segundos entre as ações. Com rotação de proxies e gerenciamento adequado de sessões, você pode chegar a 1.000–2.000 avaliações por dia. Scraping agressivo (5.000+ avaliações/dia) requer redes de proxies residenciais e várias sessões de navegador rodando em paralelo.

Playwright ou Selenium é melhor para raspar avaliações do Google Maps com Python?

Playwright é a melhor escolha para novos projetos em 2025. É 2–3x mais rápido, tem suporte async embutido e lida com anti-detecção com menos configuração manual. O Selenium ainda é válido se você tiver infraestrutura existente ou precisar de suporte máximo da comunidade. Ambos os métodos funcionam — o código neste guia demonstra ambos.

Por que meus seletores estão retornando resultados vazios?

O Google atualiza seu frontend regularmente. Um seletor que funcionou no mês passado pode não retornar nada hoje. A solução: use múltiplos seletores de fallback para cada campo e teste com headless=False para que você possa ver como a página realmente se parece. O padrão extract_with_fallbacks() mostrado neste guia lida com isso sistematicamente.

Posso raspar avaliações do Google para análise de concorrentes?

Sim. Dados de avaliações públicas são acessíveis publicamente. Analisar o sentimento dos concorrentes, rastrear tendências de avaliação ou identificar reclamações comuns é um caso de uso legítimo. Não republique avaliações individuais literalmente ou construa perfis pessoais sobre avaliadores. Foque em insights agregados.

Como lido com CAPTCHAs?

A prevenção é melhor que a solução. Diminua sua taxa de solicitação, use proxies residenciais, adicione atrasos realistas e aqueça sessões antes de raspar. Quando um CAPTCHA aparecer de qualquer maneira: em desenvolvimento, execute com headless=False e resolva manualmente. Em produção, integre um serviço de resolução de CAPTCHA ou implemente um backoff exponencial que espera 5–10 minutos antes de tentar novamente.

Pronto para começar?

Aceda a todas as empresas do Google Maps, enriquecidas com emails e dados legais.

Experimente o IBLead gratuitamente