Python

Web Scraping com BeautifulSoup e Selenium Já leu

13 min de leitura

Web Scraping com BeautifulSoup e Selenium
Nem toda informação que você precisa está disponível em uma API. Preços de produtos, dados públicos de portais governamentais, notícias, cotações, resultados de pesquisas — muitas vezes a única forma de obtê-los programa

Nem toda informação que você precisa está disponível em uma API. Preços de produtos, dados públicos de portais governamentais, notícias, cotações, resultados de pesquisas — muitas vezes a única forma de obtê-los programaticamente é extraindo diretamente do HTML das páginas web. Web scraping é essa técnica, e Python tem ferramentas excelentes para isso: BeautifulSoup para páginas estáticas e Selenium para páginas que exigem JavaScript.


Considerações Éticas e Legais

Antes de qualquer código, algumas regras importantes:

  • Sempre verifique o arquivo robots.txt do site — ele indica o que pode ou não ser coletado
  • Respeite os termos de serviço do site
  • Não sobrecarregue servidores — adicione pausas entre requisições
  • Prefira APIs oficiais quando disponíveis
  • Dados pessoais coletados estão sujeitos à LGPD e ao GDPR
import requests

# Verificando robots.txt
resposta = requests.get("https://exemplo.com/robots.txt")
print(resposta.text)

requests + BeautifulSoup: Páginas Estáticas

pip install requests beautifulsoup4 lxml
import requests
from bs4 import BeautifulSoup


def obter_soup(url: str, headers: dict = None) -> BeautifulSoup:
    """Faz requisição e retorna objeto BeautifulSoup."""
    headers_padrao = {
        "User-Agent": (
            "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
            "AppleWebKit/537.36 (KHTML, like Gecko) "
            "Chrome/120.0.0.0 Safari/537.36"
        )
    }
    headers = {**headers_padrao, **(headers or {})}
    resposta = requests.get(url, headers=headers, timeout=15)
    resposta.raise_for_status()
    return BeautifulSoup(resposta.text, "lxml")


# Exemplo com página de notícias
soup = obter_soup("https://news.ycombinator.com")

# Inspecionando a estrutura
print(soup.title.text)                      # título da página
print(soup.find("h1").text)                 # primeiro h1
print(soup.find("div", class_="content"))  # div com classe

Seletores CSS e Navegação no DOM

from bs4 import BeautifulSoup

html = """
<html>
<body>
  <div class="container">
    <h1 id="titulo">Sistema Escolar</h1>
    <ul class="lista-alunos">
      <li class="aluno aprovado" data-id="1">
        <span class="nome">Ana Silva</span>
        <span class="nota">9.5</span>
      </li>
      <li class="aluno reprovado" data-id="2">
        <span class="nome">Bruno Costa</span>
        <span class="nota">4.0</span>
      </li>
      <li class="aluno aprovado" data-id="3">
        <span class="nome">Carla Souza</span>
        <span class="nota">8.5</span>
      </li>
    </ul>
    <a href="/relatorio" class="btn">Ver Relatório</a>
    <a href="/exportar" class="btn btn-primary">Exportar</a>
  </div>
</body>
</html>
"""

soup = BeautifulSoup(html, "lxml")

# find() — primeiro elemento
titulo = soup.find("h1")
print(titulo.text)              # Sistema Escolar
print(titulo["id"])             # titulo

# find_all() — todos os elementos
alunos = soup.find_all("li", class_="aluno")
print(len(alunos))              # 3

# Apenas aprovados
aprovados = soup.find_all("li", class_="aprovado")

# select() — seletores CSS completos
nomes = soup.select(".lista-alunos .nome")
notas = soup.select(".lista-alunos .nota")

for nome, nota in zip(nomes, notas):
    print(f"{nome.text:15} — {nota.text}")

# select_one() — primeiro resultado
primeiro = soup.select_one(".aluno[data-id='1'] .nome")
print(primeiro.text)            # Ana Silva

# Atributos
links = soup.find_all("a", class_="btn")
for link in links:
    print(f"{link.text:20} → {link['href']}")

# Navegação
container = soup.find("div", class_="container")
print(container.find("h1").text)             # filho direto
lista     = container.find("ul")
primeiro_li = lista.find("li")               # primeiro filho
proximo_li  = primeiro_li.find_next_sibling("li")  # irmão seguinte
print(proximo_li.find("span", class_="nome").text)

Exemplo Prático: Cotações de Moedas

import requests
from bs4 import BeautifulSoup
from dataclasses import dataclass
from typing import List
import time


@dataclass
class Cotacao:
    moeda:     str
    codigo:    str
    compra:    float
    venda:     float
    variacao:  float


def coletar_cotacoes() -> List[Cotacao]:
    """Coleta cotações do Banco Central do Brasil."""
    url  = "https://www.bcb.gov.br/estabilidadefinanceira/fechamentodolar"
    soup = obter_soup(url)

    cotacoes = []

    tabela = soup.find("table")
    if not tabela:
        return cotacoes

    linhas = tabela.find_all("tr")[1:]   # pula o cabeçalho
    for linha in linhas:
        colunas = linha.find_all("td")
        if len(colunas) < 4:
            continue
        try:
            cotacao = Cotacao(
                moeda    = colunas[0].text.strip(),
                codigo   = colunas[1].text.strip(),
                compra   = float(colunas[2].text.strip().replace(",", ".")),
                venda    = float(colunas[3].text.strip().replace(",", ".")),
                variacao = 0.0
            )
            cotacoes.append(cotacao)
        except (ValueError, IndexError):
            continue

    return cotacoes

Scraping com Paginação

import requests
from bs4 import BeautifulSoup
from dataclasses import dataclass
from typing import List, Optional
import time


@dataclass
class Livro:
    titulo:       str
    preco:        float
    disponivel:   bool
    avaliacao:    int
    url:          str


def extrair_livros_pagina(soup: BeautifulSoup, base_url: str) -> List[Livro]:
    """Extrai todos os livros de uma página."""
    livros = []
    estrelas = {"One": 1, "Two": 2, "Three": 3, "Four": 4, "Five": 5}

    for artigo in soup.select("article.product_pod"):
        titulo = artigo.find("h3").find("a")["title"]
        preco_texto = artigo.find("p", class_="price_color").text
        preco       = float(preco_texto.replace("£", "").replace("Â", "").strip())
        disponivel  = "In stock" in artigo.find("p", class_="availability").text
        avaliacao_cls = artigo.find("p", class_="star-rating")["class"][1]
        avaliacao   = estrelas.get(avaliacao_cls, 0)
        link        = artigo.find("h3").find("a")["href"]

        livros.append(Livro(
            titulo=titulo, preco=preco,
            disponivel=disponivel, avaliacao=avaliacao,
            url=f"{base_url}/{link}"
        ))

    return livros


def coletar_todos_livros(max_paginas: int = 5) -> List[Livro]:
    """Percorre múltiplas páginas coletando livros."""
    BASE    = "https://books.toscrape.com/catalogue"
    todos   = []
    pagina  = 1

    while pagina <= max_paginas:
        if pagina == 1:
            url = "https://books.toscrape.com/catalogue/page-1.html"
        else:
            url = f"{BASE}/page-{pagina}.html"

        print(f"Coletando página {pagina}...")
        try:
            soup = obter_soup(url)
        except Exception as e:
            print(f"  Erro na página {pagina}: {e}")
            break

        livros = extrair_livros_pagina(soup, BASE)
        if not livros:
            break

        todos.extend(livros)
        print(f"  {len(livros)} livros coletados.")

        # Verifica se há próxima página
        proximo = soup.find("li", class_="next")
        if not proximo:
            break

        pagina += 1
        time.sleep(1)   # pausa educada entre requisições

    return todos


livros = coletar_todos_livros(max_paginas=3)
print(f"\nTotal coletado: {len(livros)} livros")

# Análise dos dados coletados
caros     = sorted(livros, key=lambda l: l.preco, reverse=True)[:5]
bem_avaliados = [l for l in livros if l.avaliacao == 5]

print(f"\nTop 5 mais caros:")
for l in caros:
    print(f"  £{l.preco:.2f} — {l.titulo[:40]}")

print(f"\nCom 5 estrelas: {len(bem_avaliados)}")

Selenium: Páginas com JavaScript

Para páginas que carregam conteúdo dinamicamente via JavaScript — SPAs, resultados de busca, páginas com login:

pip install selenium webdriver-manager
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.chrome.options import Options
from webdriver_manager.chrome import ChromeDriverManager
from selenium.webdriver.chrome.service import Service
import time


def criar_driver(headless: bool = True) -> webdriver.Chrome:
    """Cria e configura o driver do Chrome."""
    opcoes = Options()

    if headless:
        opcoes.add_argument("--headless")          # sem interface gráfica

    opcoes.add_argument("--no-sandbox")
    opcoes.add_argument("--disable-dev-shm-usage")
    opcoes.add_argument("--disable-gpu")
    opcoes.add_argument("--window-size=1920,1080")
    opcoes.add_argument(
        "user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
        "AppleWebKit/537.36 Chrome/120.0.0.0 Safari/537.36"
    )

    servico = Service(ChromeDriverManager().install())
    return webdriver.Chrome(service=servico, options=opcoes)


def aguardar_elemento(driver, by, seletor, timeout=10):
    """Aguarda elemento aparecer na página."""
    return WebDriverWait(driver, timeout).until(
        EC.presence_of_element_located((by, seletor))
    )


def aguardar_clicavel(driver, by, seletor, timeout=10):
    """Aguarda elemento ficar clicável."""
    return WebDriverWait(driver, timeout).until(
        EC.element_to_be_clickable((by, seletor))
    )

Selenium: Interações com Formulários

def pesquisar_github(termo: str) -> list:
    """Pesquisa repositórios no GitHub."""
    driver     = criar_driver(headless=True)
    resultados = []

    try:
        driver.get("https://github.com/search")

        # Localiza e preenche campo de busca
        campo = aguardar_elemento(driver, By.NAME, "q")
        campo.clear()
        campo.send_keys(termo)
        campo.send_keys(Keys.RETURN)

        time.sleep(2)   # aguarda resultados carregarem

        # Extrai resultados
        itens = driver.find_elements(By.CSS_SELECTOR, "[data-testid='results-list'] > div")

        for item in itens[:10]:
            try:
                nome  = item.find_element(By.CSS_SELECTOR, "h3 a").text
                link  = item.find_element(By.CSS_SELECTOR, "h3 a").get_attribute("href")
                try:
                    desc = item.find_element(By.CSS_SELECTOR, "p").text
                except:
                    desc = ""
                resultados.append({"nome": nome, "url": link, "descricao": desc})
            except Exception:
                continue

    finally:
        driver.quit()

    return resultados


# Uso
repos = pesquisar_github("fastapi python")
for repo in repos[:5]:
    print(f"  {repo['nome']}")
    if repo['descricao']:
        print(f"    {repo['descricao'][:60]}")

Selenium: Automação de Login

def automatizar_login(url_login: str, email: str, senha: str) -> webdriver.Chrome:
    """Realiza login em uma página e retorna o driver autenticado."""
    driver = criar_driver(headless=False)   # visível para debug

    try:
        driver.get(url_login)

        # Campo de e-mail
        campo_email = aguardar_elemento(driver, By.ID, "email")
        campo_email.send_keys(email)

        # Campo de senha
        campo_senha = driver.find_element(By.ID, "password")
        campo_senha.send_keys(senha)

        # Botão de login
        btn_login = aguardar_clicavel(driver, By.CSS_SELECTOR, "button[type='submit']")
        btn_login.click()

        # Aguarda redirecionamento
        WebDriverWait(driver, 10).until(
            EC.url_changes(url_login)
        )
        print(f"Login realizado. URL atual: {driver.current_url}")
        return driver

    except Exception as e:
        driver.quit()
        raise RuntimeError(f"Falha no login: {e}")

Selenium: Screenshots e Execução de JavaScript

def capturar_pagina(url: str, caminho_saida: str):
    """Captura screenshot de página completa."""
    driver = criar_driver(headless=True)

    try:
        driver.get(url)
        time.sleep(2)   # aguarda carregar

        # Redimensiona para capturar página inteira
        altura = driver.execute_script(
            "return document.body.scrollHeight"
        )
        driver.set_window_size(1920, altura)

        driver.save_screenshot(caminho_saida)
        print(f"Screenshot salvo: {caminho_saida}")

    finally:
        driver.quit()


def extrair_com_js(url: str) -> dict:
    """Extrai dados executando JavaScript na página."""
    driver = criar_driver()

    try:
        driver.get(url)
        time.sleep(2)

        # Executa JS e retorna resultado
        titulo = driver.execute_script("return document.title")
        links  = driver.execute_script("""
            return Array.from(document.querySelectorAll('a'))
                .map(a => ({texto: a.textContent.trim(), href: a.href}))
                .filter(a => a.href.startsWith('https'))
                .slice(0, 10);
        """)

        return {"titulo": titulo, "links": links}

    finally:
        driver.quit()

Scrapy: Para Projetos Maiores

Para scraping em escala — dezenas de sites, milhões de páginas:

pip install scrapy
scrapy startproject escola_scraper
# escola_scraper/spiders/livros.py
import scrapy


class LivrosSpider(scrapy.Spider):
    name         = "livros"
    start_urls   = ["https://books.toscrape.com"]
    custom_settings = {
        "DOWNLOAD_DELAY":        1,
        "CONCURRENT_REQUESTS":   4,
        "ROBOTSTXT_OBEY":        True,
        "FEEDS": {
            "livros.json": {"format": "json", "encoding": "utf8"},
        }
    }

    def parse(self, response):
        # Extrai livros da página atual
        for livro in response.css("article.product_pod"):
            yield {
                "titulo":    livro.css("h3 a::attr(title)").get(),
                "preco":     livro.css("p.price_color::text").get(),
                "avaliacao": livro.css("p.star-rating::attr(class)").get(),
            }

        # Segue para próxima página
        proxima = response.css("li.next a::attr(href)").get()
        if proxima:
            yield response.follow(proxima, self.parse)
scrapy crawl livros

Exemplo Completo: Monitor de Preços

import requests
from bs4 import BeautifulSoup
from dataclasses import dataclass
from pathlib import Path
import json
import time
from datetime import datetime


@dataclass
class Produto:
    nome:      str
    preco:     float
    url:       str
    coletado_em: str


def coletar_preco(url: str, seletor_preco: str, seletor_nome: str) -> Produto:
    """Coleta preço de um produto em qualquer loja."""
    soup  = obter_soup(url)
    nome  = soup.select_one(seletor_nome)
    preco = soup.select_one(seletor_preco)

    if not nome or not preco:
        raise ValueError("Seletores não encontraram os elementos.")

    texto_preco = preco.text.strip()
    valor = float(
        texto_preco
        .replace("R$", "").replace("£", "")
        .replace(".", "").replace(",", ".")
        .strip()
    )

    return Produto(
        nome=nome.text.strip(),
        preco=valor,
        url=url,
        coletado_em=datetime.now().isoformat()
    )


class MonitorPrecos:
    def __init__(self, arquivo_dados: str = "precos.json"):
        self.arquivo = Path(arquivo_dados)
        self.historico: dict = self._carregar()

    def _carregar(self) -> dict:
        if self.arquivo.exists():
            return json.loads(self.arquivo.read_text(encoding="utf-8"))
        return {}

    def _salvar(self):
        self.arquivo.write_text(
            json.dumps(self.historico, indent=2, ensure_ascii=False),
            encoding="utf-8"
        )

    def registrar(self, produto: Produto):
        chave = produto.url
        if chave not in self.historico:
            self.historico[chave] = {
                "nome":     produto.nome,
                "precos":   [],
                "minimo":   produto.preco,
                "maximo":   produto.preco
            }

        entrada = self.historico[chave]
        entrada["precos"].append({
            "valor": produto.preco,
            "data":  produto.coletado_em
        })
        entrada["minimo"] = min(entrada["minimo"], produto.preco)
        entrada["maximo"] = max(entrada["maximo"], produto.preco)
        self._salvar()

        # Alerta se preço caiu abaixo do mínimo histórico
        precos_anteriores = [p["valor"] for p in entrada["precos"][:-1]]
        if precos_anteriores and produto.preco < min(precos_anteriores):
            print(f"  🔔 ALERTA: {produto.nome}")
            print(f"     Novo mínimo histórico: R$ {produto.preco:.2f}")

    def relatorio(self):
        print("\n=== Relatório de Preços ===\n")
        for url, dados in self.historico.items():
            precos = [p["valor"] for p in dados["precos"]]
            atual  = precos[-1] if precos else 0
            print(f"  {dados['nome'][:40]}")
            print(f"    Atual: R$ {atual:.2f}")
            print(f"    Mínimo: R$ {dados['minimo']:.2f} | Máximo: R$ {dados['maximo']:.2f}")
            print(f"    Coletas: {len(precos)}\n")


# Configuração de produtos a monitorar
PRODUTOS = [
    {
        "url":            "https://books.toscrape.com/catalogue/a-light-in-the-attic_1000/index.html",
        "seletor_preco":  "p.price_color",
        "seletor_nome":   "h1"
    },
]

monitor = MonitorPrecos()

for config in PRODUTOS:
    try:
        produto = coletar_preco(
            config["url"],
            config["seletor_preco"],
            config["seletor_nome"]
        )
        monitor.registrar(produto)
        print(f"  Coletado: {produto.nome[:40]} — £{produto.preco:.2f}")
        time.sleep(1)
    except Exception as e:
        print(f"  Erro em {config['url']}: {e}")

monitor.relatorio()

Resumo

  • BeautifulSoup analisa HTML estático — use com requests para páginas sem JavaScript
  • find() localiza o primeiro elemento; find_all() retorna uma lista
  • select() usa seletores CSS — mais expressivo para estruturas complexas
  • Selenium controla um browser real — necessário para páginas com JavaScript dinâmico
  • WebDriverWait é preferível a time.sleep() — aguarda condições específicas
  • Adicione pausas entre requisições — respeite os servidores que você acessa
  • Scrapy é a escolha para projetos de scraping em escala — pipeline completo e configurável
  • Sempre verifique robots.txt e termos de serviço antes de fazer scraping

Referências e Leituras Complementares

  • BeautifulSoup4 — documentação oficial — https://www.crummy.com/software/BeautifulSoup/bs4/doc/
  • Selenium — documentação oficial — https://selenium-python.readthedocs.io/
  • Scrapy — documentação oficial — https://docs.scrapy.org/en/latest/
  • webdriver-manager — https://github.com/SergeyPirogov/webdriver_manager
  • books.toscrape.com — site para prática — https://books.toscrape.com/
  • quotes.toscrape.com — site para prática — https://quotes.toscrape.com/
  • MITCHELL, Ryan. Web Scraping with Python. 3. ed. O'Reilly Media, 2024. — referência completa sobre scraping com Python, cobrindo BeautifulSoup, Scrapy e Selenium.
  • LAWSON, Richard. Web Scraping with Python. Packt, 2015. — abordagem prática com foco em projetos reais.
Comentários

Mais em Python

Introdução ao Data Science com Python
Introdução ao Data Science com Python

Data Science é a disciplina que extrai conhecimento e insights de dados — com...

Artigo 40 — Análise de Dados com Pandas Avançado e Plotly
Artigo 40 — Análise de Dados com Pandas Avançado e Plotly

No Artigo 35 vimos os fundamentos do pandas. Neste artigo aprofundamos as téc...

Listas: criação, manipulação e métodos
Listas: criação, manipulação e métodos

Listas s&atilde;o a estrutura de dados mais utilizada em Python. Elas permite...