crie um app em python que faca scrape de todos os resultados de uma palavra chave buscada no google. va ate a ultima pagina ate nao dar mais para ir para proxima.


O usuário quer um app Python para fazer scrape de resultados do Google para uma palavra-chave, navegando por todas as páginas até não haver mais resultados.

Vou criar um script Python que:
1. Busca uma palavra-chave no Google
2. Extrai todos os resultados (título, URL, descrição)
3. Navega para a próxima página automaticamente
4. Continua até não haver mais páginas
5. Salva os resultados

Preciso ter cuidado com:
- User-Agent para evitar bloqueio
- Rate limiting (delays entre requests)
- Parsing correto do HTML do Google
- Tratamento de CAPTCHAs/bloqueios

Vou usar requests + BeautifulSoup, que são as bibliotecas mais comuns para isso.


Vou criar um scraper de resultados do Google em Python:


salve todos os resultados em .json


Write

#!/usr/bin/env python3
"""
Google Search Results Scraper
Extrai todos os resultados de uma busca no Google, navegando por todas as páginas.

Uso: python3 google_scraper.py "sua palavra chave"
"""

import requests
from bs4 import BeautifulSoup
import time
import random
import json
import csv
import argparse
from urllib.parse import urlencode, urlparse, parse_qs
from datetime import datetime
import sys

class GoogleScraper:
    def __init__(self, delay_range=(2, 5)):
        self.base_url = "https://www.google.com/search"
        self.delay_range = delay_range
        self.session = requests.Session()
        self.results = []

        # Headers para simular navegador real
        self.headers = {
            'User-Agent': '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',
            'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8',
            'Accept-Language': 'pt-BR,pt;q=0.9,en-US;q=0.8,en;q=0.7',
            'Accept-Encoding': 'gzip, deflate, br',
            'DNT': '1',
            'Connection': 'keep-alive',
            'Upgrade-Insecure-Requests': '1',
            'Sec-Fetch-Dest': 'document',
            'Sec-Fetch-Mode': 'navigate',
            'Sec-Fetch-Site': 'none',
            'Sec-Fetch-User': '?1',
            'Cache-Control': 'max-age=0',
        }

    def _random_delay(self):
        """Delay aleatório entre requisições para evitar bloqueio"""
        delay = random.uniform(*self.delay_range)
        print(f"  ⏳ Aguardando {delay:.1f}s...")
        time.sleep(delay)

    def _extract_url(self, href):
        """Extrai URL limpa do resultado do Google"""
        if href.startswith('/url?'):
            parsed = urlparse(href)
            params = parse_qs(parsed.query)
            return params.get('q', [href])[0]
        elif href.startswith('http'):
            return href
        return None

    def _parse_results(self, soup, page_num):
        """Extrai resultados de uma página"""
        results = []

        # Seletores para resultados orgânicos do Google
        # O Google muda frequentemente, então tentamos múltiplos seletores
        selectors = [
            'div.g',           # Resultado padrão
            'div[data-hveid]', # Alternativo
        ]

        result_divs = []
        for selector in selectors:
            found = soup.select(selector)
            if found:
                result_divs = found
                break

        for div in result_divs:
            try:
                # Busca link e título
                link_elem = div.select_one('a[href]')
                title_elem = div.select_one('h3')

                if not link_elem or not title_elem:
                    continue

                url = self._extract_url(link_elem.get('href', ''))
                if not url or not url.startswith('http'):
                    continue

                # Ignora links internos do Google
                if 'google.com' in url and '/search' in url:
                    continue
                if 'accounts.google' in url:
                    continue

                title = title_elem.get_text(strip=True)

                # Busca descrição/snippet
                snippet = ""
                snippet_selectors = [
                    'div[data-sncf]',
                    'div.VwiC3b',
                    'span.aCOpRe',
                    'div.IsZvec',
                ]
                for sel in snippet_selectors:
                    snippet_elem = div.select_one(sel)
                    if snippet_elem:
                        snippet = snippet_elem.get_text(strip=True)
                        break

                # Evita duplicatas
                if not any(r['url'] == url for r in self.results):
                    result = {
                        'title': title,
                        'url': url,
                        'snippet': snippet,
                        'page': page_num,
                        'position': len(results) + 1
                    }
                    results.append(result)

            except Exception as e:
                continue

        return results

    def _has_next_page(self, soup):
        """Verifica se existe próxima página"""
        # Busca link "Próxima"
        next_links = soup.select('a#pnnext, a[aria-label="Próxima página"], a[aria-label="Next"]')
        return len(next_links) > 0

    def _get_next_page_url(self, soup, query):
        """Obtém URL da próxima página"""
        next_link = soup.select_one('a#pnnext, a[aria-label="Próxima página"], a[aria-label="Next"]')
        if next_link:
            href = next_link.get('href', '')
            if href.startswith('/'):
                return f"https://www.google.com{href}"
            return href
        return None

    def search(self, query, max_pages=None, lang='pt-BR', country='BR'):
        """
        Executa busca no Google

        Args:
            query: Palavra-chave para buscar
            max_pages: Limite de páginas (None = sem limite)
            lang: Idioma da busca
            country: País para resultados
        """
        self.results = []
        page_num = 1

        # Parâmetros iniciais
        params = {
            'q': query,
            'hl': lang.split('-')[0],
            'gl': country,
            'num': 10,  # Resultados por página
        }

        current_url = f"{self.base_url}?{urlencode(params)}"

        print(f"\n🔍 Buscando: '{query}'")
        print(f"{'='*60}")

        while True:
            try:
                print(f"\n📄 Página {page_num}...")

                response = self.session.get(
                    current_url,
                    headers=self.headers,
                    timeout=30
                )

                # Verifica bloqueio
                if response.status_code == 429:
                    print("⚠  Rate limited! Aguardando 60s...")
                    time.sleep(60)
                    continue

                if response.status_code != 200:
                    print(f"❌ Erro HTTP {response.status_code}")
                    break

                # Detecta CAPTCHA
                if 'captcha' in response.text.lower() or 'unusual traffic' in response.text.lower():
                    print("🚫 CAPTCHA detectado! Tente novamente mais tarde ou use VPN/proxy.")
                    break

                soup = BeautifulSoup(response.text, 'html.parser')

                # Extrai resultados
                page_results = self._parse_results(soup, page_num)

                if not page_results:
                    print("  📭 Nenhum resultado encontrado nesta página")
                    # Se não encontrou nada na primeira página, pode ser bloqueio
                    if page_num == 1:
                        print("  ⚠  Possível bloqueio do Google. Tente novamente mais tarde.")
                    break

                self.results.extend(page_results)
                print(f"  ✅ {len(page_results)} resultados extraídos (Total: {len(self.results)})")

                # Verifica limite de páginas
                if max_pages and page_num >= max_pages:
                    print(f"\n📊 Limite de {max_pages} páginas atingido")
                    break

                # Verifica próxima página
                next_url = self._get_next_page_url(soup, query)
                if not next_url:
                    print("\n📊 Última página alcançada")
                    break

                current_url = next_url
                page_num += 1

                # Delay entre páginas
                self._random_delay()

            except requests.RequestException as e:
                print(f"❌ Erro de conexão: {e}")
                break
            except Exception as e:
                print(f"❌ Erro: {e}")
                break

        print(f"\n{'='*60}")
        print(f"✅ Busca finalizada: {len(self.results)} resultados em {page_num} página(s)")

        return self.results

    def save_json(self, filename):
        """Salva resultados em JSON"""
        data = {
            'query': self.results[0].get('query', '') if self.results else '',
            'total_results': len(self.results),
            'scraped_at': datetime.now().isoformat(),
            'results': self.results
        }

        with open(filename, 'w', encoding='utf-8') as f:
            json.dump(data, f, ensure_ascii=False, indent=2)
        print(f"💾 Salvo em: {filename}")

    def save_csv(self, filename):
        """Salva resultados em CSV"""
        if not self.results:
            print("⚠  Nenhum resultado para salvar")
            return

        with open(filename, 'w', newline='', encoding='utf-8') as f:
            writer = csv.DictWriter(f, fieldnames=['page', 'position', 'title', 'url', 'snippet'])
            writer.writeheader()
            writer.writerows(self.results)
        print(f"💾 Salvo em: {filename}")

    def print_results(self, limit=None):
        """Exibe resultados no terminal"""
        results = self.results[:limit] if limit else self.results

        print(f"\n📋 Resultados ({len(results)} de {len(self.results)}):\n")

        for i, r in enumerate(results, 1):
            print(f"{i}. {r['title']}")
            print(f"   🔗 {r['url']}")
            if r['snippet']:
                snippet = r['snippet'][:150] + "..." if len(r['snippet']) > 150 else r['snippet']
                print(f"   📝 {snippet}")
            print()

def main():
    parser = argparse.ArgumentParser(
        description='Google Search Scraper - Extrai todos os resultados de uma busca'
    )
    parser.add_argument('query', help='Palavra-chave para buscar')
    parser.add_argument('-p', '--pages', type=int, default=None,
                        help='Limite de páginas (padrão: sem limite)')
    parser.add_argument('-o', '--output', default=None,
                        help='Arquivo de saída (JSON ou CSV baseado na extensão)')
    parser.add_argument('-d', '--delay', type=float, default=3,
                        help='Delay mínimo entre páginas em segundos (padrão: 3)')
    parser.add_argument('--lang', default='pt-BR',
                        help='Idioma da busca (padrão: pt-BR)')
    parser.add_argument('--country', default='BR',
                        help='País para resultados (padrão: BR)')
    parser.add_argument('-v', '--verbose', action='store_true',
                        help='Mostra todos os resultados no terminal')

    args = parser.parse_args()

    # Cria scraper com delay configurado
    delay_range = (args.delay, args.delay + 3)
    scraper = GoogleScraper(delay_range=delay_range)

    # Executa busca
    results = scraper.search(
        query=args.query,
        max_pages=args.pages,
        lang=args.lang,
        country=args.country
    )

    if not results:
        print("\n❌ Nenhum resultado encontrado")
        sys.exit(1)

    # Mostra resultados
    if args.verbose:
        scraper.print_results()
    else:
        scraper.print_results(limit=10)
        if len(results) > 10:
            print(f"... e mais {len(results) - 10} resultados")

    # Salva arquivo se especificado
    if args.output:
        if args.output.endswith('.csv'):
            scraper.save_csv(args.output)
        else:
            # Default para JSON
            if not args.output.endswith('.json'):
                args.output += '.json'
            scraper.save_json(args.output)
    else:
        # Salva automaticamente com timestamp
        timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
        safe_query = "".join(c if c.isalnum() else "_" for c in args.query)[:30]
        filename = f"google_{safe_query}_{timestamp}.json"
        scraper.save_json(filename)

if __name__ == '__main__':
    main()


File created successfully at: /Users/neog/Projects/tools/google-scraper/google_scraper.py

#!/usr/bin/env python3
"""
Google Search Results Scraper
Extrai todos os resultados de uma busca no Google, navegando por todas as páginas.

Uso: python3 google_scraper.py "sua palavra chave"
"""

import requests
from bs4 import BeautifulSoup
import time
import random
import json
import csv
import argparse
from urllib.parse import urlencode, urlparse, parse_qs
from datetime import datetime
import sys

class GoogleScraper:
    def __init__(self, delay_range=(2, 5)):
        self.base_url = "https://www.google.com/search"
        self.delay_range = delay_range
        self.session = requests.Session()
        self.results = []

        # Headers para simular navegador real
        self.headers = {
            'User-Agent': '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',
            'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8',
            'Accept-Language': 'pt-BR,pt;q=0.9,en-US;q=0.8,en;q=0.7',
            'Accept-Encoding': 'gzip, deflate, br',
            'DNT': '1',
            'Connection': 'keep-alive',
            'Upgrade-Insecure-Requests': '1',
            'Sec-Fetch-Dest': 'document',
            'Sec-Fetch-Mode': 'navigate',
            'Sec-Fetch-Site': 'none',
            'Sec-Fetch-User': '?1',
            'Cache-Control': 'max-age=0',
        }

    def _random_delay(self):
        """Delay aleatório entre requisições para evitar bloqueio"""
        delay = random.uniform(*self.delay_range)
        print(f"  ⏳ Aguardando {delay:.1f}s...")
        time.sleep(delay)

    def _extract_url(self, href):
        """Extrai URL limpa do resultado do Google"""
        if href.startswith('/url?'):
            parsed = urlparse(href)
            params = parse_qs(parsed.query)
            return params.get('q', [href])[0]
        elif href.startswith('http'):
            return href
        return None

    def _parse_results(self, soup, page_num):
        """Extrai resultados de uma página"""
        results = []

        # Seletores para resultados orgânicos do Google
        # O Google muda frequentemente, então tentamos múltiplos seletores
        selectors = [
            'div.g',           # Resultado padrão
            'div[data-hveid]', # Alternativo
        ]

        result_divs = []
        for selector in selectors:
            found = soup.select(selector)
            if found:
                result_divs = found
                break

        for div in result_divs:
            try:
                # Busca link e título
                link_elem = div.select_one('a[href]')
                title_elem = div.select_one('h3')

                if not link_elem or not title_elem:
                    continue

                url = self._extract_url(link_elem.get('href', ''))
                if not url or not url.startswith('http'):
                    continue

                # Ignora links internos do Google
                if 'google.com' in url and '/search' in url:
                    continue
                if 'accounts.google' in url:
                    continue

                title = title_elem.get_text(strip=True)

                # Busca descrição/snippet
                snippet = ""
                snippet_selectors = [
                    'div[data-sncf]',
                    'div.VwiC3b',
                    'span.aCOpRe',
                    'div.IsZvec',
                ]
                for sel in snippet_selectors:
                    snippet_elem = div.select_one(sel)
                    if snippet_elem:
                        snippet = snippet_elem.get_text(strip=True)
                        break

                # Evita duplicatas
                if not any(r['url'] == url for r in self.results):
                    result = {
                        'title': title,
                        'url': url,
                        'snippet': snippet,
                        'page': page_num,
                        'position': len(results) + 1
                    }
                    results.append(result)

            except Exception as e:
                continue

        return results

    def _has_next_page(self, soup):
        """Verifica se existe próxima página"""
        # Busca link "Próxima"
        next_links = soup.select('a#pnnext, a[aria-label="Próxima página"], a[aria-label="Next"]')
        return len(next_links) > 0

    def _get_next_page_url(self, soup, query):
        """Obtém URL da próxima página"""
        next_link = soup.select_one('a#pnnext, a[aria-label="Próxima página"], a[aria-label="Next"]')
        if next_link:
            href = next_link.get('href', '')
            if href.startswith('/'):
                return f"https://www.google.com{href}"
            return href
        return None

    def search(self, query, max_pages=None, lang='pt-BR', country='BR'):
        """
        Executa busca no Google

        Args:
            query: Palavra-chave para buscar
            max_pages: Limite de páginas (None = sem limite)
            lang: Idioma da busca
            country: País para resultados
        """
        self.results = []
        page_num = 1

        # Parâmetros iniciais
        params = {
            'q': query,
            'hl': lang.split('-')[0],
            'gl': country,
            'num': 10,  # Resultados por página
        }

        current_url = f"{self.base_url}?{urlencode(params)}"

        print(f"\n🔍 Buscando: '{query}'")
        print(f"{'='*60}")

        while True:
            try:
                print(f"\n📄 Página {page_num}...")

                response = self.session.get(
                    current_url,
                    headers=self.headers,
                    timeout=30
                )

                # Verifica bloqueio
                if response.status_code == 429:
                    print("⚠  Rate limited! Aguardando 60s...")
                    time.sleep(60)
                    continue

                if response.status_code != 200:
                    print(f"❌ Erro HTTP {response.status_code}")
                    break

                # Detecta CAPTCHA
                if 'captcha' in response.text.lower() or 'unusual traffic' in response.text.lower():
                    print("🚫 CAPTCHA detectado! Tente novamente mais tarde ou use VPN/proxy.")
                    break

                soup = BeautifulSoup(response.text, 'html.parser')

                # Extrai resultados
                page_results = self._parse_results(soup, page_num)

                if not page_results:
                    print("  📭 Nenhum resultado encontrado nesta página")
                    # Se não encontrou nada na primeira página, pode ser bloqueio
                    if page_num == 1:
                        print("  ⚠  Possível bloqueio do Google. Tente novamente mais tarde.")
                    break

                self.results.extend(page_results)
                print(f"  ✅ {len(page_results)} resultados extraídos (Total: {len(self.results)})")

                # Verifica limite de páginas
                if max_pages and page_num >= max_pages:
                    print(f"\n📊 Limite de {max_pages} páginas atingido")
                    break

                # Verifica próxima página
                next_url = self._get_next_page_url(soup, query)
                if not next_url:
                    print("\n📊 Última página alcançada")
                    break

                current_url = next_url
                page_num += 1

                # Delay entre páginas
                self._random_delay()

            except requests.RequestException as e:
                print(f"❌ Erro de conexão: {e}")
                break
            except Exception as e:
                print(f"❌ Erro: {e}")
                break

        print(f"\n{'='*60}")
        print(f"✅ Busca finalizada: {len(self.results)} resultados em {page_num} página(s)")

        return self.results

    def save_json(self, filename):
        """Salva resultados em JSON"""
        data = {
            'query': self.results[0].get('query', '') if self.results else '',
            'total_results': len(self.results),
            'scraped_at': datetime.now().isoformat(),
            'results': self.results
        }

        with open(filename, 'w', encoding='utf-8') as f:
            json.dump(data, f, ensure_ascii=False, indent=2)
        print(f"💾 Salvo em: {filename}")

    def save_csv(self, filename):
        """Salva resultados em CSV"""
        if not self.results:
            print("⚠  Nenhum resultado para salvar")
            return

        with open(filename, 'w', newline='', encoding='utf-8') as f:
            writer = csv.DictWriter(f, fieldnames=['page', 'position', 'title', 'url', 'snippet'])
            writer.writeheader()
            writer.writerows(self.results)
        print(f"💾 Salvo em: {filename}")

    def print_results(self, limit=None):
        """Exibe resultados no terminal"""
        results = self.results[:limit] if limit else self.results

        print(f"\n📋 Resultados ({len(results)} de {len(self.results)}):\n")

        for i, r in enumerate(results, 1):
            print(f"{i}. {r['title']}")
            print(f"   🔗 {r['url']}")
            if r['snippet']:
                snippet = r['snippet'][:150] + "..." if len(r['snippet']) > 150 else r['snippet']
                print(f"   📝 {snippet}")
            print()

def main():
    parser = argparse.ArgumentParser(
        description='Google Search Scraper - Extrai todos os resultados de uma busca'
    )
    parser.add_argument('query', help='Palavra-chave para buscar')
    parser.add_argument('-p', '--pages', type=int, default=None,
                        help='Limite de páginas (padrão: sem limite)')
    parser.add_argument('-o', '--output', default=None,
                        help='Arquivo de saída (JSON ou CSV baseado na extensão)')
    parser.add_argument('-d', '--delay', type=float, default=3,
                        help='Delay mínimo entre páginas em segundos (padrão: 3)')
    parser.add_argument('--lang', default='pt-BR',
                        help='Idioma da busca (padrão: pt-BR)')
    parser.add_argument('--country', default='BR',
                        help='País para resultados (padrão: BR)')
    parser.add_argument('-v', '--verbose', action='store_true',
                        help='Mostra todos os resultados no terminal')

    args = parser.parse_args()

    # Cria scraper com delay configurado
    delay_range = (args.delay, args.delay + 3)
    scraper = GoogleScraper(delay_range=delay_range)

    # Executa busca
    results = scraper.search(
        query=args.query,
        max_pages=args.pages,
        lang=args.lang,
        country=args.country
    )

    if not results:
        print("\n❌ Nenhum resultado encontrado")
        sys.exit(1)

    # Mostra resultados
    if args.verbose:
        scraper.print_results()
    else:
        scraper.print_results(limit=10)
        if len(results) > 10:
            print(f"... e mais {len(results) - 10} resultados")

    # Salva arquivo se especificado
    if args.output:
        if args.output.endswith('.csv'):
            scraper.save_csv(args.output)
        else:
            # Default para JSON
            if not args.output.endswith('.json'):
                args.output += '.json'
            scraper.save_json(args.output)
    else:
        # Salva automaticamente com timestamp
        timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
        safe_query = "".join(c if c.isalnum() else "_" for c in args.query)[:30]
        filename = f"google_{safe_query}_{timestamp}.json"
        scraper.save_json(filename)

if __name__ == '__main__':
    main()


O usuário quer que os resultados sejam salvos em JSON. O script já faz isso por padrão. Vou criar um requirements.txt e mostrar como usar.


requests>=2.31.0
beautifulsoup4>=4.12.0


File created successfully at: /Users/neog/Projects/tools/google-scraper/requirements.txt

requests>=2.31.0
beautifulsoup4>=4.12.0


Agora vou tornar o script executável e testar.


Bash

Make script executable


Script pronto. Vou mostrar ao usuário como usar.


Pronto! Criei o scraper em:

```
/Users/neog/Projects/tools/google-scraper/google_scraper.py
```

## Como usar:

```bash
# Instalar dependências (se necessário)
pip3 install requests beautifulsoup4

# Busca básica (salva JSON automaticamente)
python3 ~/Projects/tools/google-scraper/google_scraper.py "sua palavra chave"

# Com limite de páginas
python3 ~/Projects/tools/google-scraper/google_scraper.py "python tutorial" -p 5

# Especificar arquivo de saída
python3 ~/Projects/tools/google-scraper/google_scraper.py "machine learning" -o resultados.json

# Ver todos os resultados no terminal
python3 ~/Projects/tools/google-scraper/google_scraper.py "react hooks" -v

# Ajustar delay entre páginas (padrão: 3s)
python3 ~/Projects/tools/google-scraper/google_scraper.py "api rest" -d 5
```

## Formato do JSON salvo:

```json
{
  "total_results": 150,
  "scraped_at": "2025-12-22T10:30:00",
  "results": [
    {
      "title": "Título do resultado",
      "url": "https://exemplo.com/pagina",
      "snippet": "Descrição do resultado...",
      "page": 1,
      "position": 1
    }
  ]
}
```

## Notas importantes:

- **Delay automático** entre páginas (2-5s) para evitar bloqueio
- **Detecta CAPTCHA** e avisa se o Google bloquear
- **Sem limite de páginas** por padrão - vai até a última
- O Google geralmente limita a ~300-400 resultados por busca

Quer que eu teste com uma palavra-chave específica?


sim, Daniel Vorcaro.


O usuário quer testar o scraper com a palavra-chave "Daniel Vorcaro". Vou executar o script.


cd /Users/neog/Projects/tools/google-scraper && python3 google_scraper.py "Daniel Vorcaro" -o daniel_vorcaro.json

Scrape Google for Daniel Vorcaro


Error: Exit code 1
/Users/neog/Library/Python/3.9/lib/python/site-packages/urllib3/__init__.py:35: NotOpenSSLWarning: urllib3 v2 only supports OpenSSL 1.1.1+, currently the 'ssl' module is compiled with 'LibreSSL 2.8.3'. See: https://github.com/urllib3/urllib3/issues/3020
  warnings.warn(

🔍 Buscando: 'Daniel Vorcaro'
============================================================

📄 Página 1...
  📭 Nenhum resultado encontrado nesta página
  ⚠  Possível bloqueio do Google. Tente novamente mais tarde.

============================================================
✅ Busca finalizada: 0 resultados em 1 página(s)

❌ Nenhum resultado encontrado


O Google está bloqueando. Os seletores CSS podem ter mudado ou está detectando o scraper. Vou ajustar o script para usar seletores mais atualizados e debug para ver o que está retornando.


O Google bloqueou. Vou ajustar os seletores e adicionar mais técnicas anti-bloqueio:


Write

#!/usr/bin/env python3
"""
Google Search Results Scraper
Extrai todos os resultados de uma busca no Google, navegando por todas as páginas.

Uso: python3 google_scraper.py "sua palavra chave"
"""

import requests
from bs4 import BeautifulSoup
import time
import random
import json
import csv
import argparse
from urllib.parse import urlencode, urlparse, parse_qs, unquote
from datetime import datetime
import sys
import re

class GoogleScraper:
    def __init__(self, delay_range=(3, 6)):
        self.base_url = "https://www.google.com/search"
        self.delay_range = delay_range
        self.session = requests.Session()
        self.results = []

        # Rotação de User-Agents
        self.user_agents = [
            '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 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2 Safari/605.1.15',
            '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 (Windows NT 10.0; Win64; x64; rv:121.0) Gecko/20100101 Firefox/121.0',
        ]

    def _get_headers(self):
        """Headers dinâmicos com User-Agent rotativo"""
        return {
            'User-Agent': random.choice(self.user_agents),
            'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8',
            'Accept-Language': 'pt-BR,pt;q=0.9,en-US;q=0.8,en;q=0.7',
            'Accept-Encoding': 'gzip, deflate, br',
            'DNT': '1',
            'Connection': 'keep-alive',
            'Upgrade-Insecure-Requests': '1',
            'Sec-Fetch-Dest': 'document',
            'Sec-Fetch-Mode': 'navigate',
            'Sec-Fetch-Site': 'none',
            'Sec-Fetch-User': '?1',
        }

    def _random_delay(self):
        """Delay aleatório entre requisições"""
        delay = random.uniform(*self.delay_range)
        print(f"  ⏳ Aguardando {delay:.1f}s...")
        time.sleep(delay)

    def _clean_url(self, url):
        """Limpa e extrai URL real"""
        if not url:
            return None

        # Remove prefixo de redirecionamento do Google
        if '/url?q=' in url:
            match = re.search(r'/url\?q=([^&]+)', url)
            if match:
                return unquote(match.group(1))

        if url.startswith('/url?'):
            parsed = urlparse(url)
            params = parse_qs(parsed.query)
            return params.get('q', [None])[0]

        if url.startswith('http'):
            return url

        return None

    def _parse_results(self, soup, page_num):
        """Extrai resultados usando múltiplas estratégias"""
        results = []
        seen_urls = set(r['url'] for r in self.results)

        # Estratégia 1: Buscar todos os links com h3
        for link in soup.find_all('a', href=True):
            h3 = link.find('h3')
            if not h3:
                continue

            url = self._clean_url(link.get('href', ''))
            if not url or not url.startswith('http'):
                continue

            # Ignora links do Google
            if any(x in url for x in ['google.com/search', 'google.com/url', 'accounts.google', 'support.google', 'policies.google']):
                continue

            if url in seen_urls:
                continue

            title = h3.get_text(strip=True)
            if not title:
                continue

            # Busca snippet no container pai
            snippet = ""
            parent = link.find_parent('div')
            if parent:
                # Procura por texto após o link
                for sibling in parent.find_next_siblings('div', limit=3):
                    text = sibling.get_text(strip=True)
                    if len(text) > 50:  # Snippet típico tem mais de 50 chars
                        snippet = text
                        break

                if not snippet:
                    # Tenta no container maior
                    grandparent = parent.find_parent('div')
                    if grandparent:
                        for div in grandparent.find_all('div'):
                            text = div.get_text(strip=True)
                            if len(text) > 80 and title not in text and url not in text:
                                snippet = text[:500]
                                break

            result = {
                'title': title,
                'url': url,
                'snippet': snippet,
                'page': page_num,
                'position': len(results) + 1
            }
            results.append(result)
            seen_urls.add(url)

        # Estratégia 2: Busca por divs com classe 'g' (fallback)
        if not results:
            for div in soup.select('div.g, div.Gx5Zad'):
                link = div.find('a', href=True)
                h3 = div.find('h3')

                if not link or not h3:
                    continue

                url = self._clean_url(link.get('href', ''))
                if not url or not url.startswith('http'):
                    continue

                if url in seen_urls:
                    continue

                title = h3.get_text(strip=True)
                snippet = ""

                # Busca snippet
                for span in div.find_all(['span', 'div']):
                    text = span.get_text(strip=True)
                    if len(text) > 80 and title not in text:
                        snippet = text[:500]
                        break

                if title:
                    results.append({
                        'title': title,
                        'url': url,
                        'snippet': snippet,
                        'page': page_num,
                        'position': len(results) + 1
                    })
                    seen_urls.add(url)

        return results

    def _get_next_start(self, soup, current_start):
        """Calcula próximo offset de página"""
        # Procura link de próxima página
        next_link = soup.select_one('a#pnnext, a[aria-label*="Next"], a[aria-label*="Próxima"], td.b a[href*="start="]')

        if next_link:
            href = next_link.get('href', '')
            match = re.search(r'start=(\d+)', href)
            if match:
                return int(match.group(1))

        # Se não encontrou, incrementa manualmente
        return current_start + 10

    def _has_next_page(self, soup):
        """Verifica se existe próxima página"""
        # Verifica link de próxima
        next_selectors = [
            'a#pnnext',
            'a[aria-label*="Next"]',
            'a[aria-label*="Próxima"]',
            'a[aria-label*="próxima"]',
        ]

        for sel in next_selectors:
            if soup.select_one(sel):
                return True

        # Verifica se há navegação de páginas
        nav = soup.select('td.b a, div[role="navigation"] a')
        return len(nav) > 1

    def search(self, query, max_pages=None, lang='pt-BR', country='BR', debug=False):
        """
        Executa busca no Google

        Args:
            query: Palavra-chave para buscar
            max_pages: Limite de páginas (None = sem limite)
            lang: Idioma da busca
            country: País para resultados
            debug: Salva HTML para debug
        """
        self.results = []
        self.query = query
        page_num = 1
        start = 0

        print(f"\n🔍 Buscando: '{query}'")
        print(f"{'='*60}")

        while True:
            try:
                print(f"\n📄 Página {page_num} (offset: {start})...")

                params = {
                    'q': query,
                    'hl': lang.split('-')[0],
                    'gl': country,
                    'start': start,
                    'num': 10,
                    'ie': 'UTF-8',
                    'oe': 'UTF-8',
                }

                url = f"{self.base_url}?{urlencode(params)}"

                response = self.session.get(
                    url,
                    headers=self._get_headers(),
                    timeout=30
                )

                if debug:
                    with open(f'debug_page_{page_num}.html', 'w') as f:
                        f.write(response.text)
                    print(f"  🐛 Debug salvo: debug_page_{page_num}.html")

                # Verifica bloqueio
                if response.status_code == 429:
                    print("⚠  Rate limited! Aguardando 60s...")
                    time.sleep(60)
                    continue

                if response.status_code != 200:
                    print(f"❌ Erro HTTP {response.status_code}")
                    break

                # Detecta CAPTCHA
                if 'captcha' in response.text.lower() or 'unusual traffic' in response.text.lower():
                    print("🚫 CAPTCHA detectado!")
                    print("   Dicas: Use VPN, aguarde alguns minutos, ou tente de outra rede.")
                    break

                soup = BeautifulSoup(response.text, 'html.parser')

                # Extrai resultados
                page_results = self._parse_results(soup, page_num)

                if not page_results:
                    if page_num == 1:
                        print("  📭 Nenhum resultado encontrado")
                        # Verifica se há mensagem "Nenhum resultado"
                        no_results = soup.find(string=re.compile(r'(nenhum resultado|did not match|no results)', re.I))
                        if no_results:
                            print("  ℹ  Google não encontrou resultados para esta busca")
                        else:
                            print("  ⚠  Possível bloqueio. Tente novamente mais tarde.")
                    else:
                        print("  📊 Fim dos resultados")
                    break

                self.results.extend(page_results)
                print(f"  ✅ {len(page_results)} resultados (Total: {len(self.results)})")

                # Verifica limite de páginas
                if max_pages and page_num >= max_pages:
                    print(f"\n📊 Limite de {max_pages} páginas atingido")
                    break

                # Verifica próxima página
                if not self._has_next_page(soup):
                    print("\n📊 Última página alcançada")
                    break

                start = self._get_next_start(soup, start)
                page_num += 1

                # Delay entre páginas
                self._random_delay()

            except requests.RequestException as e:
                print(f"❌ Erro de conexão: {e}")
                break
            except Exception as e:
                print(f"❌ Erro: {e}")
                if debug:
                    import traceback
                    traceback.print_exc()
                break

        print(f"\n{'='*60}")
        print(f"✅ Busca finalizada: {len(self.results)} resultados em {page_num} página(s)")

        return self.results

    def save_json(self, filename):
        """Salva resultados em JSON"""
        data = {
            'query': getattr(self, 'query', ''),
            'total_results': len(self.results),
            'scraped_at': datetime.now().isoformat(),
            'results': self.results
        }

        with open(filename, 'w', encoding='utf-8') as f:
            json.dump(data, f, ensure_ascii=False, indent=2)
        print(f"💾 Salvo em: {filename}")

    def save_csv(self, filename):
        """Salva resultados em CSV"""
        if not self.results:
            print("⚠  Nenhum resultado para salvar")
            return

        with open(filename, 'w', newline='', encoding='utf-8') as f:
            writer = csv.DictWriter(f, fieldnames=['page', 'position', 'title', 'url', 'snippet'])
            writer.writeheader()
            writer.writerows(self.results)
        print(f"💾 Salvo em: {filename}")

    def print_results(self, limit=None):
        """Exibe resultados no terminal"""
        results = self.results[:limit] if limit else self.results

        print(f"\n📋 Resultados ({len(results)} de {len(self.results)}):\n")

        for i, r in enumerate(results, 1):
            print(f"{i}. {r['title']}")
            print(f"   🔗 {r['url']}")
            if r['snippet']:
                snippet = r['snippet'][:150] + "..." if len(r['snippet']) > 150 else r['snippet']
                print(f"   📝 {snippet}")
            print()

def main():
    parser = argparse.ArgumentParser(
        description='Google Search Scraper - Extrai todos os resultados de uma busca'
    )
    parser.add_argument('query', help='Palavra-chave para buscar')
    parser.add_argument('-p', '--pages', type=int, default=None,
                        help='Limite de páginas (padrão: sem limite)')
    parser.add_argument('-o', '--output', default=None,
                        help='Arquivo de saída (JSON ou CSV baseado na extensão)')
    parser.add_argument('-d', '--delay', type=float, default=3,
                        help='Delay mínimo entre páginas em segundos (padrão: 3)')
    parser.add_argument('--lang', default='pt-BR',
                        help='Idioma da busca (padrão: pt-BR)')
    parser.add_argument('--country', default='BR',
                        help='País para resultados (padrão: BR)')
    parser.add_argument('-v', '--verbose', action='store_true',
                        help='Mostra todos os resultados no terminal')
    parser.add_argument('--debug', action='store_true',
                        help='Salva HTML das páginas para debug')

    args = parser.parse_args()

    # Cria scraper com delay configurado
    delay_range = (args.delay, args.delay + 3)
    scraper = GoogleScraper(delay_range=delay_range)

    # Executa busca
    results = scraper.search(
        query=args.query,
        max_pages=args.pages,
        lang=args.lang,
        country=args.country,
        debug=args.debug
    )

    if not results:
        print("\n❌ Nenhum resultado encontrado")
        sys.exit(1)

    # Mostra resultados
    if args.verbose:
        scraper.print_results()
    else:
        scraper.print_results(limit=10)
        if len(results) > 10:
            print(f"... e mais {len(results) - 10} resultados")

    # Salva arquivo
    if args.output:
        if args.output.endswith('.csv'):
            scraper.save_csv(args.output)
        else:
            if not args.output.endswith('.json'):
                args.output += '.json'
            scraper.save_json(args.output)
    else:
        timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
        safe_query = "".join(c if c.isalnum() else "_" for c in args.query)[:30]
        filename = f"google_{safe_query}_{timestamp}.json"
        scraper.save_json(filename)

if __name__ == '__main__':
    main()


The file /Users/neog/Projects/tools/google-scraper/google_scraper.py has been updated. Here's the result of running `cat -n` on a snippet of the edited file:
     1→#!/usr/bin/env python3
     2→"""
     3→Google Search Results Scraper
     4→Extrai todos os resultados de uma busca no Google, navegando por todas as páginas.
     5→
     6→Uso: python3 google_scraper.py "sua palavra chave"
     7→"""
     8→
     9→import requests
    10→from bs4 import BeautifulSoup
    11→import time
    12→import random
    13→import json
    14→import csv
    15→import argparse
    16→from urllib.parse import urlencode, urlparse, parse_qs, unquote
    17→from datetime import datetime
    18→import sys
    19→import re
    20→
    21→class GoogleScraper:
    22→    def __init__(self, delay_range=(3, 6)):
    23→        self.base_url = "https://www.google.com/search"
    24→        self.delay_range = delay_range
    25→        self.session = requests.Session()
    26→        self.results = []
    27→
    28→        # Rotação de User-Agents
    29→        self.user_agents = [
    30→            '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',
    31→            'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2 Safari/605.1.15',
    32→            'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
    33→            'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:121.0) Gecko/20100101 Firefox/121.0',
    34→        ]
    35→
    36→    def _get_headers(self):
    37→        """Headers dinâmicos com User-Agent rotativo"""
    38→        return {
    39→            'User-Agent': random.choice(self.user_agents),
    40→            'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8',
    41→            'Accept-Language': 'pt-BR,pt;q=0.9,en-US;q=0.8,en;q=0.7',
    42→            'Accept-Encoding': 'gzip, deflate, br',
    43→            'DNT': '1',
    44→            'Connection': 'keep-alive',
    45→            'Upgrade-Insecure-Requests': '1',
    46→            'Sec-Fetch-Dest': 'document',
    47→            'Sec-Fetch-Mode': 'navigate',
    48→            'Sec-Fetch-Site': 'none',
    49→            'Sec-Fetch-User': '?1',
    50→        }
    51→
    52→    def _random_delay(self):
    53→        """Delay aleatório entre requisições"""
    54→        delay = random.uniform(*self.delay_range)
    55→        print(f"  ⏳ Aguardando {delay:.1f}s...")
    56→        time.sleep(delay)
    57→
    58→    def _clean_url(self, url):
    59→        """Limpa e extrai URL real"""
    60→        if not url:
    61→            return None
    62→
    63→        # Remove prefixo de redirecionamento do Google
    64→        if '/url?q=' in url:
    65→            match = re.search(r'/url\?q=([^&]+)', url)
    66→            if match:
    67→                return unquote(match.group(1))
    68→
    69→        if url.startswith('/url?'):
    70→            parsed = urlparse(url)
    71→            params = parse_qs(parsed.query)
    72→            return params.get('q', [None])[0]
    73→
    74→        if url.startswith('http'):
    75→            return url
    76→
    77→        return None
    78→
    79→    def _parse_results(self, soup, page_num):
    80→        """Extrai resultados usando múltiplas estratégias"""
    81→        results = []
    82→        seen_urls = set(r['url'] for r in self.results)
    83→
    84→        # Estratégia 1: Buscar todos os links com h3
    85→        for link in soup.find_all('a', href=True):
    86→            h3 = link.find('h3')
    87→            if not h3:
    88→                continue
    89→
    90→            url = self._clean_url(link.get('href', ''))
    91→            if not url or not url.startswith('http'):
    92→                continue
    93→
    94→            # Ignora links do Google
    95→            if any(x in url for x in ['google.com/search', 'google.com/url', 'accounts.google', 'support.google', 'policies.google']):
    96→                continue
    97→
    98→            if url in seen_urls:
    99→                continue
   100→
   101→            title = h3.get_text(strip=True)
   102→            if not title:
   103→                continue
   104→
   105→            # Busca snippet no container pai
   106→            snippet = ""
   107→            parent = link.find_parent('div')
   108→            if parent:
   109→                # Procura por texto após o link
   110→                for sibling in parent.find_next_siblings('div', limit=3):
   111→                    text = sibling.get_text(strip=True)
   112→                    if len(text) > 50:  # Snippet típico tem mais de 50 chars
   113→                        snippet = text
   114→                        break
   115→
   116→                if not snippet:
   117→                    # Tenta no container maior
   118→                    grandparent = parent.find_parent('div')
   119→                    if grandparent:
   120→                        for div in grandparent.find_all('div'):
   121→                            text = div.get_text(strip=True)
   122→                            if len(text) > 80 and title not in text and url not in text:
   123→                                snippet = text[:500]
   124→                                break
   125→
   126→            result = {
   127→                'title': title,
   128→                'url': url,
   129→                'snippet': snippet,
   130→                'page': page_num,
   131→                'position': len(results) + 1
   132→            }
   133→            results.append(result)
   134→            seen_urls.add(url)
   135→
   136→        # Estratégia 2: Busca por divs com classe 'g' (fallback)
   137→        if not results:
   138→            for div in soup.select('div.g, div.Gx5Zad'):
   139→                link = div.find('a', href=True)
   140→                h3 = div.find('h3')
   141→
   142→                if not link or not h3:
   143→                    continue
   144→
   145→                url = self._clean_url(link.get('href', ''))
   146→                if not url or not url.startswith('http'):
   147→                    continue
   148→
   149→                if url in seen_urls:
   150→                    continue
   151→
   152→                title = h3.get_text(strip=True)
   153→                snippet = ""
   154→
   155→                # Busca snippet
   156→                for span in div.find_all(['span', 'div']):
   157→                    text = span.get_text(strip=True)
   158→                    if len(text) > 80 and title not in text:
   159→                        snippet = text[:500]
   160→                        break
   161→
   162→                if title:
   163→                    results.append({
   164→                        'title': title,
   165→                        'url': url,
   166→                        'snippet': snippet,
   167→                        'page': page_num,
   168→                        'position': len(results) + 1
   169→                    })
   170→                    seen_urls.add(url)
   171→
   172→        return results
   173→
   174→    def _get_next_start(self, soup, current_start):
   175→        """Calcula próximo offset de página"""
   176→        # Procura link de próxima página
   177→        next_link = soup.select_one('a#pnnext, a[aria-label*="Next"], a[aria-label*="Próxima"], td.b a[href*="start="]')
   178→
   179→        if next_link:
   180→            href = next_link.get('href', '')
   181→            match = re.search(r'start=(\d+)', href)
   182→            if match:
   183→                return int(match.group(1))
   184→
   185→        # Se não encontrou, incrementa manualmente
   186→        return current_start + 10
   187→
   188→    def _has_next_page(self, soup):
   189→        """Verifica se existe próxima página"""
   190→        # Verifica link de próxima
   191→        next_selectors = [
   192→            'a#pnnext',
   193→            'a[aria-label*="Next"]',
   194→            'a[aria-label*="Próxima"]',
   195→            'a[aria-label*="próxima"]',
   196→        ]
   197→
   198→        for sel in next_selectors:
   199→            if soup.select_one(sel):
   200→                return True
   201→
   202→        # Verifica se há navegação de páginas
   203→        nav = soup.select('td.b a, div[role="navigation"] a')
   204→        return len(nav) > 1
   205→
   206→    def search(self, query, max_pages=None, lang='pt-BR', country='BR', debug=False):
   207→        """
   208→        Executa busca no Google
   209→
   210→        Args:
   211→            query: Palavra-chave para buscar
   212→            max_pages: Limite de páginas (None = sem limite)
   213→            lang: Idioma da busca
   214→            country: País para resultados
   215→            debug: Salva HTML para debug
   216→        """
   217→        self.results = []
   218→        self.query = query
   219→        page_num = 1
   220→        start = 0
   221→
   222→        print(f"\n🔍 Buscando: '{query}'")
   223→        print(f"{'='*60}")
   224→
   225→        while True:
   226→            try:
   227→                print(f"\n📄 Página {page_num} (offset: {start})...")
   228→
   229→                params = {
   230→                    'q': query,
   231→                    'hl': lang.split('-')[0],
   232→                    'gl': country,
   233→                    'start': start,
   234→                    'num': 10,
   235→                    'ie': 'UTF-8',
   236→                    'oe': 'UTF-8',
   237→                }
   238→
   239→                url = f"{self.base_url}?{urlencode(params)}"
   240→
   241→                response = self.session.get(
   242→                    url,
   243→                    headers=self._get_headers(),
   244→                    timeout=30
   245→                )
   246→
   247→                if debug:
   248→                    with open(f'debug_page_{page_num}.html', 'w') as f:
   249→                        f.write(response.text)
   250→                    print(f"  🐛 Debug salvo: debug_page_{page_num}.html")
   251→
   252→                # Verifica bloqueio
   253→                if response.status_code == 429:
   254→                    print("⚠  Rate limited! Aguardando 60s...")
   255→                    time.sleep(60)
   256→                    continue
   257→
   258→                if response.status_code != 200:
   259→                    print(f"❌ Erro HTTP {response.status_code}")
   260→                    break
   261→
   262→                # Detecta CAPTCHA
   263→                if 'captcha' in response.text.lower() or 'unusual traffic' in response.text.lower():
   264→                    print("🚫 CAPTCHA detectado!")
   265→                    print("   Dicas: Use VPN, aguarde alguns minutos, ou tente de outra rede.")
   266→                    break
   267→
   268→                soup = BeautifulSoup(response.text, 'html.parser')
   269→
   270→                # Extrai resultados
   271→                page_results = self._parse_results(soup, page_num)
   272→
   273→                if not page_results:
   274→                    if page_num == 1:
   275→                        print("  📭 Nenhum resultado encontrado")
   276→                        # Verifica se há mensagem "Nenhum resultado"
   277→                        no_results = soup.find(string=re.compile(r'(nenhum resultado|did not match|no results)', re.I))
   278→                        if no_results:
   279→                            print("  ℹ  Google não encontrou resultados para esta busca")
   280→                        else:
   281→                            print("  ⚠  Possível bloqueio. Tente novamente mais tarde.")
   282→                    else:
   283→                        print("  📊 Fim dos resultados")
   284→                    break
   285→
   286→                self.results.extend(page_results)
   287→                print(f"  ✅ {len(page_results)} resultados (Total: {len(self.results)})")
   288→
   289→                # Verifica limite de páginas
   290→                if max_pages and page_num >= max_pages:
   291→                    print(f"\n📊 Limite de {max_pages} páginas atingido")
   292→                    break
   293→
   294→                # Verifica próxima página
   295→                if not self._has_next_page(soup):
   296→                    print("\n📊 Última página alcançada")
   297→                    break
   298→
   299→                start = self._get_next_start(soup, start)
   300→                page_num += 1
   301→
   302→                # Delay entre páginas
   303→                self._random_delay()
   304→
   305→            except requests.RequestException as e:
   306→                print(f"❌ Erro de conexão: {e}")
   307→                break
   308→            except Exception as e:
   309→                print(f"❌ Erro: {e}")
   310→                if debug:
   311→                    import traceback
   312→                    traceback.print_exc()
   313→                break
   314→
   315→        print(f"\n{'='*60}")
   316→        print(f"✅ Busca finalizada: {len(self.results)} resultados em {page_num} página(s)")
   317→
   318→        return self.results
   319→
   320→    def save_json(self, filename):
   321→        """Salva resultados em JSON"""
   322→        data = {
   323→            'query': getattr(self, 'query', ''),
   324→            'total_results': len(self.results),
   325→            'scraped_at': datetime.now().isoformat(),
   326→            'results': self.results
   327→        }
   328→
   329→        with open(filename, 'w', encoding='utf-8') as f:
   330→            json.dump(data, f, ensure_ascii=False, indent=2)
   331→        print(f"💾 Salvo em: {filename}")
   332→
   333→    def save_csv(self, filename):
   334→        """Salva resultados em CSV"""
   335→        if not self.results:
   336→            print("⚠  Nenhum resultado para salvar")
   337→            return
   338→
   339→        with open(filename, 'w', newline='', encoding='utf-8') as f:
   340→            writer = csv.DictWriter(f, fieldnames=['page', 'position', 'title', 'url', 'snippet'])
   341→            writer.writeheader()
   342→            writer.writerows(self.results)
   343→        print(f"💾 Salvo em: {filename}")
   344→
   345→    def print_results(self, limit=None):
   346→        """Exibe resultados no terminal"""
   347→        results = self.results[:limit] if limit else self.results
   348→
   349→        print(f"\n📋 Resultados ({len(results)} de {len(self.results)}):\n")
   350→
   351→        for i, r in enumerate(results, 1):
   352→            print(f"{i}. {r['title']}")
   353→            print(f"   🔗 {r['url']}")
   354→            if r['snippet']:
   355→                snippet = r['snippet'][:150] + "..." if len(r['snippet']) > 150 else r['snippet']
   356→                print(f"   📝 {snippet}")
   357→            print()
   358→
   359→
   360→def main():
   361→    parser = argparse.ArgumentParser(
   362→        description='Google Search Scraper - Extrai todos os resultados de uma busca'
   363→    )
   364→    parser.add_argument('query', help='Palavra-chave para buscar')
   365→    parser.add_argument('-p', '--pages', type=int, default=None,
   366→                        help='Limite de páginas (padrão: sem limite)')
   367→    parser.add_argument('-o', '--output', default=None,
   368→                        help='Arquivo de saída (JSON ou CSV baseado na extensão)')
   369→    parser.add_argument('-d', '--delay', type=float, default=3,
   370→                        help='Delay mínimo entre páginas em segundos (padrão: 3)')
   371→    parser.add_argument('--lang', default='pt-BR',
   372→                        help='Idioma da busca (padrão: pt-BR)')
   373→    parser.add_argument('--country', default='BR',
   374→                        help='País para resultados (padrão: BR)')
   375→    parser.add_argument('-v', '--verbose', action='store_true',
   376→                        help='Mostra todos os resultados no terminal')
   377→    parser.add_argument('--debug', action='store_true',
   378→                        help='Salva HTML das páginas para debug')
   379→
   380→    args = parser.parse_args()
   381→
   382→    # Cria scraper com delay configurado
   383→    delay_range = (args.delay, args.delay + 3)
   384→    scraper = GoogleScraper(delay_range=delay_range)
   385→
   386→    # Executa busca
   387→    results = scraper.search(
   388→        query=args.query,
   389→        max_pages=args.pages,
   390→        lang=args.lang,
   391→        country=args.country,
   392→        debug=args.debug
   393→    )
   394→
   395→    if not results:
   396→        print("\n❌ Nenhum resultado encontrado")
   397→        sys.exit(1)
   398→
   399→    # Mostra resultados
   400→    if args.verbose:
   401→        scraper.print_results()
   402→    else:
   403→        scraper.print_results(limit=10)
   404→        if len(results) > 10:
   405→            print(f"... e mais {len(results) - 10} resultados")
   406→
   407→    # Salva arquivo
   408→    if args.output:
   409→        if args.output.endswith('.csv'):
   410→            scraper.save_csv(args.output)
   411→        else:
   412→            if not args.output.endswith('.json'):
   413→                args.output += '.json'
   414→            scraper.save_json(args.output)
   415→    else:
   416→        timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
   417→        safe_query = "".join(c if c.isalnum() else "_" for c in args.query)[:30]
   418→        filename = f"google_{safe_query}_{timestamp}.json"
   419→        scraper.save_json(filename)
   420→
   421→
   422→if __name__ == '__main__':
   423→    main()
   424→

#!/usr/bin/env python3
"""
Google Search Results Scraper
Extrai todos os resultados de uma busca no Google, navegando por todas as páginas.

Uso: python3 google_scraper.py "sua palavra chave"
"""

import requests
from bs4 import BeautifulSoup
import time
import random
import json
import csv
import argparse
from urllib.parse import urlencode, urlparse, parse_qs, unquote
from datetime import datetime
import sys
import re

class GoogleScraper:
    def __init__(self, delay_range=(3, 6)):
        self.base_url = "https://www.google.com/search"
        self.delay_range = delay_range
        self.session = requests.Session()
        self.results = []

        # Rotação de User-Agents
        self.user_agents = [
            '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 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2 Safari/605.1.15',
            '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 (Windows NT 10.0; Win64; x64; rv:121.0) Gecko/20100101 Firefox/121.0',
        ]

    def _get_headers(self):
        """Headers dinâmicos com User-Agent rotativo"""
        return {
            'User-Agent': random.choice(self.user_agents),
            'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8',
            'Accept-Language': 'pt-BR,pt;q=0.9,en-US;q=0.8,en;q=0.7',
            'Accept-Encoding': 'gzip, deflate, br',
            'DNT': '1',
            'Connection': 'keep-alive',
            'Upgrade-Insecure-Requests': '1',
            'Sec-Fetch-Dest': 'document',
            'Sec-Fetch-Mode': 'navigate',
            'Sec-Fetch-Site': 'none',
            'Sec-Fetch-User': '?1',
        }

    def _random_delay(self):
        """Delay aleatório entre requisições"""
        delay = random.uniform(*self.delay_range)
        print(f"  ⏳ Aguardando {delay:.1f}s...")
        time.sleep(delay)

    def _clean_url(self, url):
        """Limpa e extrai URL real"""
        if not url:
            return None

        # Remove prefixo de redirecionamento do Google
        if '/url?q=' in url:
            match = re.search(r'/url\?q=([^&]+)', url)
            if match:
                return unquote(match.group(1))

        if url.startswith('/url?'):
            parsed = urlparse(url)
            params = parse_qs(parsed.query)
            return params.get('q', [None])[0]

        if url.startswith('http'):
            return url

        return None

    def _parse_results(self, soup, page_num):
        """Extrai resultados usando múltiplas estratégias"""
        results = []
        seen_urls = set(r['url'] for r in self.results)

        # Estratégia 1: Buscar todos os links com h3
        for link in soup.find_all('a', href=True):
            h3 = link.find('h3')
            if not h3:
                continue

            url = self._clean_url(link.get('href', ''))
            if not url or not url.startswith('http'):
                continue

            # Ignora links do Google
            if any(x in url for x in ['google.com/search', 'google.com/url', 'accounts.google', 'support.google', 'policies.google']):
                continue

            if url in seen_urls:
                continue

            title = h3.get_text(strip=True)
            if not title:
                continue

            # Busca snippet no container pai
            snippet = ""
            parent = link.find_parent('div')
            if parent:
                # Procura por texto após o link
                for sibling in parent.find_next_siblings('div', limit=3):
                    text = sibling.get_text(strip=True)
                    if len(text) > 50:  # Snippet típico tem mais de 50 chars
                        snippet = text
                        break

                if not snippet:
                    # Tenta no container maior
                    grandparent = parent.find_parent('div')
                    if grandparent:
                        for div in grandparent.find_all('div'):
                            text = div.get_text(strip=True)
                            if len(text) > 80 and title not in text and url not in text:
                                snippet = text[:500]
                                break

            result = {
                'title': title,
                'url': url,
                'snippet': snippet,
                'page': page_num,
                'position': len(results) + 1
            }
            results.append(result)
            seen_urls.add(url)

        # Estratégia 2: Busca por divs com classe 'g' (fallback)
        if not results:
            for div in soup.select('div.g, div.Gx5Zad'):
                link = div.find('a', href=True)
                h3 = div.find('h3')

                if not link or not h3:
                    continue

                url = self._clean_url(link.get('href', ''))
                if not url or not url.startswith('http'):
                    continue

                if url in seen_urls:
                    continue

                title = h3.get_text(strip=True)
                snippet = ""

                # Busca snippet
                for span in div.find_all(['span', 'div']):
                    text = span.get_text(strip=True)
                    if len(text) > 80 and title not in text:
                        snippet = text[:500]
                        break

                if title:
                    results.append({
                        'title': title,
                        'url': url,
                        'snippet': snippet,
                        'page': page_num,
                        'position': len(results) + 1
                    })
                    seen_urls.add(url)

        return results

    def _get_next_start(self, soup, current_start):
        """Calcula próximo offset de página"""
        # Procura link de próxima página
        next_link = soup.select_one('a#pnnext, a[aria-label*="Next"], a[aria-label*="Próxima"], td.b a[href*="start="]')

        if next_link:
            href = next_link.get('href', '')
            match = re.search(r'start=(\d+)', href)
            if match:
                return int(match.group(1))

        # Se não encontrou, incrementa manualmente
        return current_start + 10

    def _has_next_page(self, soup):
        """Verifica se existe próxima página"""
        # Verifica link de próxima
        next_selectors = [
            'a#pnnext',
            'a[aria-label*="Next"]',
            'a[aria-label*="Próxima"]',
            'a[aria-label*="próxima"]',
        ]

        for sel in next_selectors:
            if soup.select_one(sel):
                return True

        # Verifica se há navegação de páginas
        nav = soup.select('td.b a, div[role="navigation"] a')
        return len(nav) > 1

    def search(self, query, max_pages=None, lang='pt-BR', country='BR', debug=False):
        """
        Executa busca no Google

        Args:
            query: Palavra-chave para buscar
            max_pages: Limite de páginas (None = sem limite)
            lang: Idioma da busca
            country: País para resultados
            debug: Salva HTML para debug
        """
        self.results = []
        self.query = query
        page_num = 1
        start = 0

        print(f"\n🔍 Buscando: '{query}'")
        print(f"{'='*60}")

        while True:
            try:
                print(f"\n📄 Página {page_num} (offset: {start})...")

                params = {
                    'q': query,
                    'hl': lang.split('-')[0],
                    'gl': country,
                    'start': start,
                    'num': 10,
                    'ie': 'UTF-8',
                    'oe': 'UTF-8',
                }

                url = f"{self.base_url}?{urlencode(params)}"

                response = self.session.get(
                    url,
                    headers=self._get_headers(),
                    timeout=30
                )

                if debug:
                    with open(f'debug_page_{page_num}.html', 'w') as f:
                        f.write(response.text)
                    print(f"  🐛 Debug salvo: debug_page_{page_num}.html")

                # Verifica bloqueio
                if response.status_code == 429:
                    print("⚠  Rate limited! Aguardando 60s...")
                    time.sleep(60)
                    continue

                if response.status_code != 200:
                    print(f"❌ Erro HTTP {response.status_code}")
                    break

                # Detecta CAPTCHA
                if 'captcha' in response.text.lower() or 'unusual traffic' in response.text.lower():
                    print("🚫 CAPTCHA detectado!")
                    print("   Dicas: Use VPN, aguarde alguns minutos, ou tente de outra rede.")
                    break

                soup = BeautifulSoup(response.text, 'html.parser')

                # Extrai resultados
                page_results = self._parse_results(soup, page_num)

                if not page_results:
                    if page_num == 1:
                        print("  📭 Nenhum resultado encontrado")
                        # Verifica se há mensagem "Nenhum resultado"
                        no_results = soup.find(string=re.compile(r'(nenhum resultado|did not match|no results)', re.I))
                        if no_results:
                            print("  ℹ  Google não encontrou resultados para esta busca")
                        else:
                            print("  ⚠  Possível bloqueio. Tente novamente mais tarde.")
                    else:
                        print("  📊 Fim dos resultados")
                    break

                self.results.extend(page_results)
                print(f"  ✅ {len(page_results)} resultados (Total: {len(self.results)})")

                # Verifica limite de páginas
                if max_pages and page_num >= max_pages:
                    print(f"\n📊 Limite de {max_pages} páginas atingido")
                    break

                # Verifica próxima página
                if not self._has_next_page(soup):
                    print("\n📊 Última página alcançada")
                    break

                start = self._get_next_start(soup, start)
                page_num += 1

                # Delay entre páginas
                self._random_delay()

            except requests.RequestException as e:
                print(f"❌ Erro de conexão: {e}")
                break
            except Exception as e:
                print(f"❌ Erro: {e}")
                if debug:
                    import traceback
                    traceback.print_exc()
                break

        print(f"\n{'='*60}")
        print(f"✅ Busca finalizada: {len(self.results)} resultados em {page_num} página(s)")

        return self.results

    def save_json(self, filename):
        """Salva resultados em JSON"""
        data = {
            'query': getattr(self, 'query', ''),
            'total_results': len(self.results),
            'scraped_at': datetime.now().isoformat(),
            'results': self.results
        }

        with open(filename, 'w', encoding='utf-8') as f:
            json.dump(data, f, ensure_ascii=False, indent=2)
        print(f"💾 Salvo em: {filename}")

    def save_csv(self, filename):
        """Salva resultados em CSV"""
        if not self.results:
            print("⚠  Nenhum resultado para salvar")
            return

        with open(filename, 'w', newline='', encoding='utf-8') as f:
            writer = csv.DictWriter(f, fieldnames=['page', 'position', 'title', 'url', 'snippet'])
            writer.writeheader()
            writer.writerows(self.results)
        print(f"💾 Salvo em: {filename}")

    def print_results(self, limit=None):
        """Exibe resultados no terminal"""
        results = self.results[:limit] if limit else self.results

        print(f"\n📋 Resultados ({len(results)} de {len(self.results)}):\n")

        for i, r in enumerate(results, 1):
            print(f"{i}. {r['title']}")
            print(f"   🔗 {r['url']}")
            if r['snippet']:
                snippet = r['snippet'][:150] + "..." if len(r['snippet']) > 150 else r['snippet']
                print(f"   📝 {snippet}")
            print()

def main():
    parser = argparse.ArgumentParser(
        description='Google Search Scraper - Extrai todos os resultados de uma busca'
    )
    parser.add_argument('query', help='Palavra-chave para buscar')
    parser.add_argument('-p', '--pages', type=int, default=None,
                        help='Limite de páginas (padrão: sem limite)')
    parser.add_argument('-o', '--output', default=None,
                        help='Arquivo de saída (JSON ou CSV baseado na extensão)')
    parser.add_argument('-d', '--delay', type=float, default=3,
                        help='Delay mínimo entre páginas em segundos (padrão: 3)')
    parser.add_argument('--lang', default='pt-BR',
                        help='Idioma da busca (padrão: pt-BR)')
    parser.add_argument('--country', default='BR',
                        help='País para resultados (padrão: BR)')
    parser.add_argument('-v', '--verbose', action='store_true',
                        help='Mostra todos os resultados no terminal')
    parser.add_argument('--debug', action='store_true',
                        help='Salva HTML das páginas para debug')

    args = parser.parse_args()

    # Cria scraper com delay configurado
    delay_range = (args.delay, args.delay + 3)
    scraper = GoogleScraper(delay_range=delay_range)

    # Executa busca
    results = scraper.search(
        query=args.query,
        max_pages=args.pages,
        lang=args.lang,
        country=args.country,
        debug=args.debug
    )

    if not results:
        print("\n❌ Nenhum resultado encontrado")
        sys.exit(1)

    # Mostra resultados
    if args.verbose:
        scraper.print_results()
    else:
        scraper.print_results(limit=10)
        if len(results) > 10:
            print(f"... e mais {len(results) - 10} resultados")

    # Salva arquivo
    if args.output:
        if args.output.endswith('.csv'):
            scraper.save_csv(args.output)
        else:
            if not args.output.endswith('.json'):
                args.output += '.json'
            scraper.save_json(args.output)
    else:
        timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
        safe_query = "".join(c if c.isalnum() else "_" for c in args.query)[:30]
        filename = f"google_{safe_query}_{timestamp}.json"
        scraper.save_json(filename)

if __name__ == '__main__':
    main()

-from urllib.parse import urlencode, urlparse, parse_qs

+from urllib.parse import urlencode, urlparse, parse_qs, unquote

-    def __init__(self, delay_range=(2, 5)):

+    def __init__(self, delay_range=(3, 6)):

-        # Headers para simular navegador real

-            'User-Agent': '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',

-            'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8',

+            '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 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2 Safari/605.1.15',

+            'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',

+        """Headers dinâmicos com User-Agent rotativo"""

+            'User-Agent': random.choice(self.user_agents),

+            'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8',

-            'Cache-Control': 'max-age=0',

-        """Delay aleatório entre requisições para evitar bloqueio"""

+        """Delay aleatório entre requisições"""

print(f"  ⏳ Aguardando {delay:.1f}s...")

-        """Extrai URL limpa do resultado do Google"""

+        # Remove prefixo de redirecionamento do Google

+            match = re.search(r'/url\?q=([^&]+)', url)

+                return unquote(match.group(1))

-            return params.get('q', [href])[0]

+            return params.get('q', [None])[0]

def _parse_results(self, soup, page_num):

-        """Extrai resultados de uma página"""

+        """Extrai resultados usando múltiplas estratégias"""

+        seen_urls = set(r['url'] for r in self.results)

-        # Seletores para resultados orgânicos do Google

-        # O Google muda frequentemente, então tentamos múltiplos seletores

-            'div.g',           # Resultado padrão

-            'div[data-hveid]', # Alternativo

+        # Estratégia 1: Buscar todos os links com h3

+        for link in soup.find_all('a', href=True):

-            found = soup.select(selector)

+            url = self._clean_url(link.get('href', ''))

+            if not url or not url.startswith('http'):

-                link_elem = div.select_one('a[href]')

-                title_elem = div.select_one('h3')

+            if any(x in url for x in ['google.com/search', 'google.com/url', 'accounts.google', 'support.google', 'policies.google']):

-                if not link_elem or not title_elem:

+            title = h3.get_text(strip=True)

+            # Busca snippet no container pai

+            parent = link.find_parent('div')

+                # Procura por texto após o link

+                for sibling in parent.find_next_siblings('div', limit=3):

+                    text = sibling.get_text(strip=True)

+                    if len(text) > 50:  # Snippet típico tem mais de 50 chars

+                    # Tenta no container maior

+                    grandparent = parent.find_parent('div')

+                        for div in grandparent.find_all('div'):

+                            text = div.get_text(strip=True)

+                            if len(text) > 80 and title not in text and url not in text:

+                                snippet = text[:500]

+                'position': len(results) + 1

+        # Estratégia 2: Busca por divs com classe 'g' (fallback)

+            for div in soup.select('div.g, div.Gx5Zad'):

+                link = div.find('a', href=True)

-                url = self._extract_url(link_elem.get('href', ''))

+                url = self._clean_url(link.get('href', ''))

if not url or not url.startswith('http'):

-                # Ignora links internos do Google

-                if 'google.com' in url and '/search' in url:

-                if 'accounts.google' in url:

-                title = title_elem.get_text(strip=True)

-                # Busca descrição/snippet

+                title = h3.get_text(strip=True)

-                for sel in snippet_selectors:

-                    snippet_elem = div.select_one(sel)

-                        snippet = snippet_elem.get_text(strip=True)

+                for span in div.find_all(['span', 'div']):

+                    text = span.get_text(strip=True)

+                    if len(text) > 80 and title not in text:

+                        snippet = text[:500]

-                if not any(r['url'] == url for r in self.results):

-                    results.append(result)

-        """Verifica se existe próxima página"""

-        next_links = soup.select('a#pnnext, a[aria-label="Próxima página"], a[aria-label="Next"]')

+    def _get_next_start(self, soup, current_start):

+        """Calcula próximo offset de página"""

+        # Procura link de próxima página

+        next_link = soup.select_one('a#pnnext, a[aria-label*="Next"], a[aria-label*="Próxima"], td.b a[href*="start="]')

-    def _get_next_page_url(self, soup, query):

-        """Obtém URL da próxima página"""

-        next_link = soup.select_one('a#pnnext, a[aria-label="Próxima página"], a[aria-label="Next"]')

-                return f"https://www.google.com{href}"

+            match = re.search(r'start=(\d+)', href)

+                return int(match.group(1))

-    def search(self, query, max_pages=None, lang='pt-BR', country='BR'):

+        # Se não encontrou, incrementa manualmente

+        """Verifica se existe próxima página"""

+            'a[aria-label*="Próxima"]',

+            'a[aria-label*="próxima"]',

+        # Verifica se há navegação de páginas

+        nav = soup.select('td.b a, div[role="navigation"] a')

+    def search(self, query, max_pages=None, lang='pt-BR', country='BR', debug=False):

+            debug: Salva HTML para debug

-            'num': 10,  # Resultados por página

-        current_url = f"{self.base_url}?{urlencode(params)}"

-                print(f"\n📄 Página {page_num}...")

+                print(f"\n📄 Página {page_num} (offset: {start})...")

+                url = f"{self.base_url}?{urlencode(params)}"

-                    headers=self.headers,

+                    headers=self._get_headers(),

+                    with open(f'debug_page_{page_num}.html', 'w') as f:

+                        f.write(response.text)

+                    print(f"  🐛 Debug salvo: debug_page_{page_num}.html")

print("⚠  Rate limited! Aguardando 60s...")

-                    print("🚫 CAPTCHA detectado! Tente novamente mais tarde ou use VPN/proxy.")

+                    print("🚫 CAPTCHA detectado!")

+                    print("   Dicas: Use VPN, aguarde alguns minutos, ou tente de outra rede.")

-                    print("  📭 Nenhum resultado encontrado nesta página")

-                    # Se não encontrou nada na primeira página, pode ser bloqueio

-                        print("  ⚠  Possível bloqueio do Google. Tente novamente mais tarde.")

+                        print("  📭 Nenhum resultado encontrado")

+                        # Verifica se há mensagem "Nenhum resultado"

+                        no_results = soup.find(string=re.compile(r'(nenhum resultado|did not match|no results)', re.I))

+                            print("  ℹ  Google não encontrou resultados para esta busca")

+                            print("  ⚠  Possível bloqueio. Tente novamente mais tarde.")

+                        print("  📊 Fim dos resultados")

-                print(f"  ✅ {len(page_results)} resultados extraídos (Total: {len(self.results)})")

+                print(f"  ✅ {len(page_results)} resultados (Total: {len(self.results)})")

-                next_url = self._get_next_page_url(soup, query)

+                if not self._has_next_page(soup):

+                start = self._get_next_start(soup, start)

+                    traceback.print_exc()

-            'query': self.results[0].get('query', '') if self.results else '',

+            'query': getattr(self, 'query', ''),

help='País para resultados (padrão: BR)')

+    parser.add_argument('--debug', action='store_true',

+                        help='Salva HTML das páginas para debug')

-        # Salva automaticamente com timestamp

#!/usr/bin/env python3
"""
Google Search Results Scraper
Extrai todos os resultados de uma busca no Google, navegando por todas as páginas.

Uso: python3 google_scraper.py "sua palavra chave"
"""

import requests
from bs4 import BeautifulSoup
import time
import random
import json
import csv
import argparse
from urllib.parse import urlencode, urlparse, parse_qs
from datetime import datetime
import sys

class GoogleScraper:
    def __init__(self, delay_range=(2, 5)):
        self.base_url = "https://www.google.com/search"
        self.delay_range = delay_range
        self.session = requests.Session()
        self.results = []

        # Headers para simular navegador real
        self.headers = {
            'User-Agent': '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',
            'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8',
            'Accept-Language': 'pt-BR,pt;q=0.9,en-US;q=0.8,en;q=0.7',
            'Accept-Encoding': 'gzip, deflate, br',
            'DNT': '1',
            'Connection': 'keep-alive',
            'Upgrade-Insecure-Requests': '1',
            'Sec-Fetch-Dest': 'document',
            'Sec-Fetch-Mode': 'navigate',
            'Sec-Fetch-Site': 'none',
            'Sec-Fetch-User': '?1',
            'Cache-Control': 'max-age=0',
        }

    def _random_delay(self):
        """Delay aleatório entre requisições para evitar bloqueio"""
        delay = random.uniform(*self.delay_range)
        print(f"  ⏳ Aguardando {delay:.1f}s...")
        time.sleep(delay)

    def _extract_url(self, href):
        """Extrai URL limpa do resultado do Google"""
        if href.startswith('/url?'):
            parsed = urlparse(href)
            params = parse_qs(parsed.query)
            return params.get('q', [href])[0]
        elif href.startswith('http'):
            return href
        return None

    def _parse_results(self, soup, page_num):
        """Extrai resultados de uma página"""
        results = []

        # Seletores para resultados orgânicos do Google
        # O Google muda frequentemente, então tentamos múltiplos seletores
        selectors = [
            'div.g',           # Resultado padrão
            'div[data-hveid]', # Alternativo
        ]

        result_divs = []
        for selector in selectors:
            found = soup.select(selector)
            if found:
                result_divs = found
                break

        for div in result_divs:
            try:
                # Busca link e título
                link_elem = div.select_one('a[href]')
                title_elem = div.select_one('h3')

                if not link_elem or not title_elem:
                    continue

                url = self._extract_url(link_elem.get('href', ''))
                if not url or not url.startswith('http'):
                    continue

                # Ignora links internos do Google
                if 'google.com' in url and '/search' in url:
                    continue
                if 'accounts.google' in url:
                    continue

                title = title_elem.get_text(strip=True)

                # Busca descrição/snippet
                snippet = ""
                snippet_selectors = [
                    'div[data-sncf]',
                    'div.VwiC3b',
                    'span.aCOpRe',
                    'div.IsZvec',
                ]
                for sel in snippet_selectors:
                    snippet_elem = div.select_one(sel)
                    if snippet_elem:
                        snippet = snippet_elem.get_text(strip=True)
                        break

                # Evita duplicatas
                if not any(r['url'] == url for r in self.results):
                    result = {
                        'title': title,
                        'url': url,
                        'snippet': snippet,
                        'page': page_num,
                        'position': len(results) + 1
                    }
                    results.append(result)

            except Exception as e:
                continue

        return results

    def _has_next_page(self, soup):
        """Verifica se existe próxima página"""
        # Busca link "Próxima"
        next_links = soup.select('a#pnnext, a[aria-label="Próxima página"], a[aria-label="Next"]')
        return len(next_links) > 0

    def _get_next_page_url(self, soup, query):
        """Obtém URL da próxima página"""
        next_link = soup.select_one('a#pnnext, a[aria-label="Próxima página"], a[aria-label="Next"]')
        if next_link:
            href = next_link.get('href', '')
            if href.startswith('/'):
                return f"https://www.google.com{href}"
            return href
        return None

    def search(self, query, max_pages=None, lang='pt-BR', country='BR'):
        """
        Executa busca no Google

        Args:
            query: Palavra-chave para buscar
            max_pages: Limite de páginas (None = sem limite)
            lang: Idioma da busca
            country: País para resultados
        """
        self.results = []
        page_num = 1

        # Parâmetros iniciais
        params = {
            'q': query,
            'hl': lang.split('-')[0],
            'gl': country,
            'num': 10,  # Resultados por página
        }

        current_url = f"{self.base_url}?{urlencode(params)}"

        print(f"\n🔍 Buscando: '{query}'")
        print(f"{'='*60}")

        while True:
            try:
                print(f"\n📄 Página {page_num}...")

                response = self.session.get(
                    current_url,
                    headers=self.headers,
                    timeout=30
                )

                # Verifica bloqueio
                if response.status_code == 429:
                    print("⚠  Rate limited! Aguardando 60s...")
                    time.sleep(60)
                    continue

                if response.status_code != 200:
                    print(f"❌ Erro HTTP {response.status_code}")
                    break

                # Detecta CAPTCHA
                if 'captcha' in response.text.lower() or 'unusual traffic' in response.text.lower():
                    print("🚫 CAPTCHA detectado! Tente novamente mais tarde ou use VPN/proxy.")
                    break

                soup = BeautifulSoup(response.text, 'html.parser')

                # Extrai resultados
                page_results = self._parse_results(soup, page_num)

                if not page_results:
                    print("  📭 Nenhum resultado encontrado nesta página")
                    # Se não encontrou nada na primeira página, pode ser bloqueio
                    if page_num == 1:
                        print("  ⚠  Possível bloqueio do Google. Tente novamente mais tarde.")
                    break

                self.results.extend(page_results)
                print(f"  ✅ {len(page_results)} resultados extraídos (Total: {len(self.results)})")

                # Verifica limite de páginas
                if max_pages and page_num >= max_pages:
                    print(f"\n📊 Limite de {max_pages} páginas atingido")
                    break

                # Verifica próxima página
                next_url = self._get_next_page_url(soup, query)
                if not next_url:
                    print("\n📊 Última página alcançada")
                    break

                current_url = next_url
                page_num += 1

                # Delay entre páginas
                self._random_delay()

            except requests.RequestException as e:
                print(f"❌ Erro de conexão: {e}")
                break
            except Exception as e:
                print(f"❌ Erro: {e}")
                break

        print(f"\n{'='*60}")
        print(f"✅ Busca finalizada: {len(self.results)} resultados em {page_num} página(s)")

        return self.results

    def save_json(self, filename):
        """Salva resultados em JSON"""
        data = {
            'query': self.results[0].get('query', '') if self.results else '',
            'total_results': len(self.results),
            'scraped_at': datetime.now().isoformat(),
            'results': self.results
        }

        with open(filename, 'w', encoding='utf-8') as f:
            json.dump(data, f, ensure_ascii=False, indent=2)
        print(f"💾 Salvo em: {filename}")

    def save_csv(self, filename):
        """Salva resultados em CSV"""
        if not self.results:
            print("⚠  Nenhum resultado para salvar")
            return

        with open(filename, 'w', newline='', encoding='utf-8') as f:
            writer = csv.DictWriter(f, fieldnames=['page', 'position', 'title', 'url', 'snippet'])
            writer.writeheader()
            writer.writerows(self.results)
        print(f"💾 Salvo em: {filename}")

    def print_results(self, limit=None):
        """Exibe resultados no terminal"""
        results = self.results[:limit] if limit else self.results

        print(f"\n📋 Resultados ({len(results)} de {len(self.results)}):\n")

        for i, r in enumerate(results, 1):
            print(f"{i}. {r['title']}")
            print(f"   🔗 {r['url']}")
            if r['snippet']:
                snippet = r['snippet'][:150] + "..." if len(r['snippet']) > 150 else r['snippet']
                print(f"   📝 {snippet}")
            print()

def main():
    parser = argparse.ArgumentParser(
        description='Google Search Scraper - Extrai todos os resultados de uma busca'
    )
    parser.add_argument('query', help='Palavra-chave para buscar')
    parser.add_argument('-p', '--pages', type=int, default=None,
                        help='Limite de páginas (padrão: sem limite)')
    parser.add_argument('-o', '--output', default=None,
                        help='Arquivo de saída (JSON ou CSV baseado na extensão)')
    parser.add_argument('-d', '--delay', type=float, default=3,
                        help='Delay mínimo entre páginas em segundos (padrão: 3)')
    parser.add_argument('--lang', default='pt-BR',
                        help='Idioma da busca (padrão: pt-BR)')
    parser.add_argument('--country', default='BR',
                        help='País para resultados (padrão: BR)')
    parser.add_argument('-v', '--verbose', action='store_true',
                        help='Mostra todos os resultados no terminal')

    args = parser.parse_args()

    # Cria scraper com delay configurado
    delay_range = (args.delay, args.delay + 3)
    scraper = GoogleScraper(delay_range=delay_range)

    # Executa busca
    results = scraper.search(
        query=args.query,
        max_pages=args.pages,
        lang=args.lang,
        country=args.country
    )

    if not results:
        print("\n❌ Nenhum resultado encontrado")
        sys.exit(1)

    # Mostra resultados
    if args.verbose:
        scraper.print_results()
    else:
        scraper.print_results(limit=10)
        if len(results) > 10:
            print(f"... e mais {len(results) - 10} resultados")

    # Salva arquivo se especificado
    if args.output:
        if args.output.endswith('.csv'):
            scraper.save_csv(args.output)
        else:
            # Default para JSON
            if not args.output.endswith('.json'):
                args.output += '.json'
            scraper.save_json(args.output)
    else:
        # Salva automaticamente com timestamp
        timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
        safe_query = "".join(c if c.isalnum() else "_" for c in args.query)[:30]
        filename = f"google_{safe_query}_{timestamp}.json"
        scraper.save_json(filename)

if __name__ == '__main__':
    main()


Vou tentar novamente com debug para ver o que está sendo retornado pelo Google.


Bash

cd /Users/neog/Projects/tools/google-scraper && python3 google_scraper.py "Daniel Vorcaro" -o daniel_vorcaro.json --debug 2>&1

Scrape Google with debug enabled


Error: Exit code 1
/Users/neog/Library/Python/3.9/lib/python/site-packages/urllib3/__init__.py:35: NotOpenSSLWarning: urllib3 v2 only supports OpenSSL 1.1.1+, currently the 'ssl' module is compiled with 'LibreSSL 2.8.3'. See: https://github.com/urllib3/urllib3/issues/3020
  warnings.warn(

🔍 Buscando: 'Daniel Vorcaro'
============================================================

📄 Página 1 (offset: 0)...
  🐛 Debug salvo: debug_page_1.html
  📭 Nenhum resultado encontrado
  ⚠  Possível bloqueio. Tente novamente mais tarde.

============================================================
✅ Busca finalizada: 0 resultados em 1 página(s)

❌ Nenhum resultado encontrado


Preciso ver o que está no arquivo de debug para entender o problema.


Check debug HTML content


<!DOCTYPE html><title>Google Search</title><meta content="0;url=/httpservice/retry/enablejs?sei=XftIaaDZHLvf1sQPhfyDuAM" http-equiv="refresh">
Clique aqui se o redirecionamento não iniciar em alguns segundos.


Vejo que o Google está retornando uma página com JavaScript que requer execução de JS. O scraper básico com requests não vai funcionar neste caso porque o Google carrega resultados via JavaScript.

Vou precisar usar uma abordagem diferente:
1. Usar Selenium/Playwright para renderizar JavaScript
2. Ou usar a SerpAPI (API paga)
3. Ou tentar usar a busca do Google via curl com parâmetros diferentes

Vou criar uma versão com Playwright que funciona melhor.


O Google está retornando uma página JavaScript-heavy. Preciso usar Playwright para renderizar. Vou criar uma versão que funciona:


[Request interrupted by user]