Web Scraping com Python e BeautifulSoup: guia prático com exemplos testados

Cansado de programar?

Conheça a melhor e mais completa formação de Python e Django e sinta-se um programador verdadeiramente competente. Além de Python e Django, você também vai aprender Banco de Dados, SQL, HTML, CSS, Javascript, Bootstrap e muito mais!

Quero aprender Python e Django de Verdade! Quero aprender!
Suporte

Tire suas dúvidas diretamente com o professor

Projetos práticos

Projetos práticos voltados para o mercado de trabalho

Prática profissional

Formação moderna com foco na prática profissional

Download do Artigo

✅ Artigo atualizado em Setembro 2026 (beautifulsoup4 4.15 e requests 2.34) Todos os exemplos foram executados de novo e migrados para sites feitos para praticar scraping (quotes.toscrape.com e books.toscrape.com), porque o HTML do Python.org mudou. Novas seções de seletores CSS, extração de texto e atributos, paginação, erros comuns com traceback e 8 exercícios resolvidos.

Resposta rápida

Para fazer web scraping com Python, baixe o HTML com requests.get(url) e entregue o conteúdo ao BeautifulSoup: soup = BeautifulSoup(html, "html.parser"). Depois localize os dados com find() (primeiro elemento), find_all() (todos) ou select() (seletor CSS).

1
2
3
4
5
6
7
import requests
from bs4 import BeautifulSoup

resposta = requests.get("https://quotes.toscrape.com/", timeout=10)
soup = BeautifulSoup(resposta.text, "html.parser")
print(soup.find("small", class_="author").text)  # Albert Einstein
print(len(soup.select("div.quote")))             # 10

Resumo em 30 segundos:

  • Instale com pip install requests beautifulsoup4 e importe com from bs4 import BeautifulSoup.
  • requests baixa a página; o BeautifulSoup só interpreta o HTML (não acessa a internet nem executa JavaScript).
  • find() devolve o primeiro elemento ou None; find_all() e select() devolvem uma lista (vazia se não achar).
  • Texto com tag.get_text(strip=True); atributos com tag["href"] ou tag.get("href").
  • Use timeout, raise_for_status(), pausas entre requisições e respeite o robots.txt e os termos de uso do site.

Salve salve Pythonista!

Hoje, vamos navegar pelo maravilhoso mundo do Web Scraping utilizando a popular biblioteca Python, o BeautifulSoup.

Ao longo deste artigo, vamos visitar temas importantes, como Crawler, Parser, o uso de BeautifulSoup, seus principais métodos, seletores CSS e paginação e, finalmente, vamos colocar a mão na massa com um mini projeto, erros comuns e exercícios resolvidos!

Vá Direto ao Assunto…

O que é web scraping com Python

Web scraping (ou raspagem de dados) é a coleta automática de informações de páginas da web: um programa baixa o HTML de uma página, encontra os trechos que interessam (preços, títulos, links, tabelas) e salva esses dados em um formato útil, como uma lista, um CSV ou um JSON.

Em Python, o fluxo mais comum tem três passos:

  1. Baixar a página com requests.get(url).
  2. Interpretar o HTML com BeautifulSoup(html, "html.parser").
  3. Extrair os dados com find(), find_all() ou select() e guardar o resultado.

Se você quer entender o conceito com mais calma (e quando usar uma API em vez de raspar), leia antes o artigo o que é Web Scraping e para que serve.

Primeiro, vamos começar do início. Pelos conceitos de Crawler e Parser!

Crawler e Parser

Um Crawler, também conhecido como Spider, é um tipo de script que realiza a varredura na web de maneira metódica e automatizada.

Ele serve para coletar recursos específicos, como, por exemplo, links ou dados específicos de páginas da web.

Suponha que a Página A contenha um link para a Página B e que a Página B possua um link para a Página C.

Através da leitura do código HTML, podemos - de forma automatizada - traçar esse “caminho”, saindo da Página A à Página C, passando pela Página B.

Para isso, basta analisar o código HTML da Página A, em busca de tags <a>, como por exemplo:

1
<a href="https://paginab.com.br">Página B</a>

E em seguida, realizar a mesma operação na Página B, em busca de outra tag <a> que faz referência à Página C, por exemplo:

1
<a href="https://paginac.com.br">Página C</a>

Assim como uma aranha “navega” em sua rede, um processo Spider (ou Crawler) navega na “Net” (que significa Rede, em Inglês)!

Já o Parser é uma ferramenta que nos ajuda a extrair dados de um documento. No mundo do Web Scraping, ele é comumente usado para extrair dados de páginas HTML e XML.

No processo de Parsing, algum programa irá “digerir” os dados “esquisitos” de uma página HTML para uma representação mais simples de ser compreendida e processada, possibilitando a busca e extração de dados!

Esse dois processos (Crawler e Parser) são muito importantes no contexto do Web Scraping.

BeautifulSoup: Seu Amigo na Coleta de Dados

O BeautifulSoup é uma biblioteca Python que facilita a raspagem de informações das páginas da web.

Ele possui um poderoso parser que proporciona uma maneira Pythonica de navegar, pesquisar e modificar os dados de um Website.

Para utilizá-lo, utilize o pip e instale o BeautifulSoup em um ambiente virtual (se ainda não sabe o que são ambientes virtuais, clique aqui e aprenda a utilizar o Virtualenv):

1
pip install beautifulsoup4

Atenção ao nome: o pacote instalado se chama beautifulsoup4, mas no código o import é from bs4 import BeautifulSoup.

Uma coisa importante a notar aqui é que o BeautifulSoup não baixa as páginas da web para nós. Por isso ele geralmente é utilizado com a biblioteca requests para buscar o código HTML de uma página web (instale-o no mesmo ambiente virtual com pip install requests).

A referência completa de métodos está na documentação oficial do Beautiful Soup e na documentação do requests.

Sabendo disso, vamos em frente.

Mas antes… Está curtindo esse conteúdo? :thumbsup:

Que tal receber 30 dias de conteúdo direto na sua Caixa de Entrada?

Sua assinatura não pôde ser validada.
Você fez sua assinatura com sucesso.

Assine as PyDicas e receba 30 dias do melhor conteúdo Python na sua Caixa de Entrada: direto e sem enrolação!

Principais métodos de utilização do BeautifulSoup

Antes de começarmos a usar o BeautifulSoup, devemos dar uma olhada em seus principais métodos:

  • find() e find_all(): Esses dois métodos são usados frequentemente para encontrar tags. O método find_all() busca todas as instâncias de uma tag e find() busca apenas a primeira instância de uma tag.

  • select() e select_one(): Fazem a mesma busca usando seletores CSS (como div.quote span.text). select() devolve uma lista e select_one() devolve o primeiro resultado.

  • get(): Este método é usado para acessar os atributos de uma tag.

  • text e get_text(): Obtêm o texto de dentro de uma tag. Com get_text(strip=True) você já remove os espaços das pontas.

Agora que conhecemos o básico, vamos utilizar o BeautifulSoup para um exemplo simples de WebScraping, no site quotes.toscrape.com, que foi criado justamente para quem quer praticar scraping:

1
2
3
4
5
6
7
8
9
10
from bs4 import BeautifulSoup
import requests

URL = 'https://quotes.toscrape.com/'
page = requests.get(URL, timeout=10)

soup = BeautifulSoup(page.content, 'html.parser')

header = soup.find('h1')
print(header)

Se você rodar este script, ele retornará o que há dentro da primeira tag <h1> que encontrar na página:

1
2
3
<h1>
<a href="/" style="text-decoration: none">Quotes to Scrape</a>
</h1>

find() vs select(): seletores CSS no BeautifulSoup

Os dois estilos encontram os mesmos elementos. A diferença está em como você descreve o que procura:

  • find() / find_all(): você passa o nome da tag e filtros como argumentos (class_="preco", id="vitrine", href=True).
  • select() / select_one(): você escreve um seletor CSS, o mesmo que usaria numa folha de estilo ou no DevTools do navegador (#vitrine .preco, div.produto > h2).

Veja os dois lado a lado, com um HTML embutido no próprio código (roda sem internet):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
from bs4 import BeautifulSoup

html = """
<div id="vitrine">
  <div class="produto destaque">
    <h2>Teclado</h2>
    <span class="preco">R$ 150,00</span>
    <a href="/produtos/teclado" title="Ver teclado">detalhes</a>
  </div>
  <div class="produto">
    <h2>Mouse</h2>
    <span class="preco">R$ 80,00</span>
    <a href="/produtos/mouse">detalhes</a>
  </div>
</div>
"""
soup = BeautifulSoup(html, "html.parser")

# find / find_all: nome da tag + filtros
print(soup.find("h2").text)                                 # Teclado
print([h.text for h in soup.find_all("h2")])                # ['Teclado', 'Mouse']
print(soup.find("div", class_="destaque").h2.text)          # Teclado

# select / select_one: seletores CSS
print(soup.select_one("div.produto.destaque h2").text)      # Teclado
print([p.text for p in soup.select("#vitrine .preco")])     # ['R$ 150,00', 'R$ 80,00']

Regra prática: para buscas simples, find() é mais legível; quando o caminho envolve vários níveis (“o h2 dentro da div com duas classes dentro de #vitrine”), select() resolve em uma linha. Uma dica útil: no DevTools do Chrome ou do Firefox, clique com o botão direito no elemento e use Copiar > Copiar seletor para ter um ponto de partida.

Situação Use
Primeiro elemento com uma tag/classe soup.find("span", class_="preco")
Todos os elementos com uma tag/classe soup.find_all("span", class_="preco")
Caminho com vários níveis ou várias classes soup.select("div.produto.destaque h2")
Primeiro elemento de um seletor CSS soup.select_one("#vitrine .preco")
Só tags que têm um atributo soup.find_all("a", href=True) ou soup.select("a[href]")

Como extrair texto e atributos

Encontrar a tag é metade do trabalho. A outra metade é tirar dela o texto ou um atributo (como o href de um link ou o src de uma imagem):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from bs4 import BeautifulSoup

html = '<p class="info">  Frete <b>grátis</b> acima de R$ 99  </p><a href="/ofertas" title="Ofertas">ver</a>'
soup = BeautifulSoup(html, "html.parser")

p = soup.find("p")
print(repr(p.text))                    # '  Frete grátis acima de R$ 99  '
print(p.get_text(strip=True))          # Fretegrátisacima de R$ 99
print(p.get_text(" ", strip=True))     # Frete grátis acima de R$ 99

link = soup.find("a")
print(link["href"])                    # /ofertas
print(link.get("title"))               # Ofertas
print(link.get("target"))              # None (atributo não existe)
print(link.attrs)                      # {'href': '/ofertas', 'title': 'Ofertas'}
print(p["class"])                      # ['info'] (class sempre vem como lista)

Repare na linha 8: get_text(strip=True) remove os espaços de cada pedaço de texto e cola tudo, o que junta as palavras quando há tags no meio (<b>). Passe um separador, como em get_text(" ", strip=True), para manter as palavras separadas.

Bot de Web Scraping com BeautifulSoup

Finalmente, estamos prontos para começar a trabalhar em nosso mini projeto!

Vamos criar um simples bot de raspagem para extrair citações, autores e links do site quotes.toscrape.com (nas primeiras versões deste artigo o alvo era o blog do Python.org, mas o HTML dele mudou e o exemplo parou de funcionar - um lembrete de que scrapers quebram quando o site muda).

O primeiro passo para qualquer projeto de Web Scraping é analisar o código HTML da página de onde se quer extrair os dados.

Com a ajuda das Ferramentas de Desenvolvedor do Navegador (Chrome ou Firefox, por exemplo), podemos passar o mouse em cima da região onde queremos extrair os dados e analisar a estrutura do código HTML. Veja como fica essa inspeção no DevTools:

Código HTML do site Python.org/blogs

No quotes.toscrape.com, fazendo a mesma inspeção, cada citação tem esta estrutura (resumida):

1
2
3
4
5
6
<div class="quote">
  <span class="text">“The world as we have created it...”</span>
  <span>by <small class="author">Albert Einstein</small>
  <a href="/author/Albert-Einstein">(about)</a>
  </span>
</div>

Dessa forma, sabemos que o que queremos está: dentro de uma <div> com a classe CSS quote, que contém o texto em um <span class="text">, o autor em um <small class="author"> e um link <a> para a página do autor.

Com isso podemos buscar os dados que queremos da seguinte forma:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from bs4 import BeautifulSoup
import requests

URL = 'https://quotes.toscrape.com/'
page = requests.get(URL, timeout=10)
page.raise_for_status()

soup = BeautifulSoup(page.content, 'html.parser')

# Cada citação fica em uma <div class="quote">
citacoes = soup.find_all('div', class_='quote')

# Imprimimos as 3 primeiras: texto, autor e link da página do autor
for citacao in citacoes[:3]:
    texto = citacao.find('span', class_='text').get_text(strip=True)
    autor = citacao.find('small', class_='author').text
    link = citacao.find('a')['href']
    print(f"{autor}: {texto[:50]}...\n  Link: {link}")

Este script irá acessar a página e imprimirá as citações em um formato de texto limpo:

1
2
3
4
5
6
Albert Einstein: “The world as we have created it is a process of o...
  Link: /author/Albert-Einstein
J.K. Rowling: “It is our choices, Harry, that show what we truly...
  Link: /author/J-K-Rowling
Albert Einstein: “There are only two ways to live your life. One is...
  Link: /author/Albert-Einstein

Muito maneiro não é mesmo?!

Repare que o link veio relativo (/author/Albert-Einstein). Para montar a URL completa, use urljoin do módulo urllib.parse, como no exemplo de paginação logo abaixo.

Agora, se quiser aprender mais ainda sobre Web Scraping, eu posso te ajudar!

Em nosso primeiro Projeto da Jornada Python, o curso de Python completo da Python Academy, você vai aprender a buscar dados financeiros na internet para criar uma estratégia de investimentos para você!

Além disso, você vai aprender Python do básico ao avançado através de projetos completos e muito exercício.

E não para por aí: você ainda vai aprender a desenvolver aplicações Web utilizando o famoso framework Django!

Tudo isso com suporte às suas dúvidas, Comunidade Exclusiva de Alunos, certificado de conclusão, e-books exclusivos e muito mais!

:bulb: Estou construindo o Ebookr.ai, uma plataforma onde você cria ebooks profissionais com IA sobre qualquer assunto - do zero ao PDF pronto, com capas e infográficos gerados automaticamente. Dá uma olhada!

Te vejo na Jornada, caro Pythonista!

Como raspar várias páginas (paginação)

Quase todo site com listagens divide o conteúdo em páginas. O padrão é sempre o mesmo: raspe a página atual, procure o link “Próxima” e repita até ele não existir mais.

No quotes.toscrape.com, o link para a próxima página fica em <li class="next"><a href="/page/2/">. O script abaixo percorre todas as páginas e conta quantos autores diferentes existem no site:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import time
from urllib.parse import urljoin

import requests
from bs4 import BeautifulSoup

url = "https://quotes.toscrape.com/"
autores = set()
paginas = 0

while url:
    resposta = requests.get(url, timeout=10)
    resposta.raise_for_status()
    soup = BeautifulSoup(resposta.text, "html.parser")
    paginas += 1

    for autor in soup.select("div.quote small.author"):
        autores.add(autor.get_text(strip=True))

    proximo = soup.select_one("li.next > a")        # None na última página
    url = urljoin(url, proximo["href"]) if proximo else None
    time.sleep(1)                                     # pausa educada entre páginas

print(f"Páginas visitadas: {paginas}")
print(f"Autores diferentes: {len(autores)}")

Saída (em setembro de 2026):

1
2
Páginas visitadas: 10
Autores diferentes: 50

Três detalhes fazem diferença:

  • urljoin transforma o href relativo (/page/2/) em URL completa, sem você concatenar strings na mão.
  • select_one() devolve None na última página, e é isso que encerra o while.
  • time.sleep(1) evita disparar dezenas de requisições por segundo contra o servidor.

Quando o site tem a URL previsível (/page/1/, /page/2/…), também dá para usar um for com range(). E se forem milhares de páginas, vale conhecer a programação assíncrona no Python ou partir para o Scrapy, sempre com limite de requisições simultâneas.

Conclusão

Neste artigo você aprendeu sobre web scraping com Python e BeautifulSoup. Discutimos sobre Crawler e Parser, conhecemos melhor a biblioteca BeautifulSoup e até construímos um simples bot de raspagem.

A combinação de Python e BeautifulSoup torna o web scraping acessível, até mesmo para programadores menos experientes.

Com um pouco de prática, você pode começar a extrair uma grande quantidade de dados da web, que podem ser usados para análise de dados, aprendizado de máquina e uma variedade de outras aplicações.

BeautifulSoup vs Outras Bibliotecas

Quando usar cada ferramenta:

Critério requests + BeautifulSoup Selenium Playwright Scrapy
O que é Download HTTP + parser de HTML Automação de navegador real Automação de navegador real Framework completo de crawling
Executa JavaScript Não Sim Sim Não (só com extensões)
Facilidade Muito fácil Média Média Curva maior
Velocidade Rápida (não abre navegador) Lenta (abre navegador) Lenta (abre navegador) Rápida (requisições concorrentes)
Muitas páginas Loop manual Loop manual Loop manual Nativo: fila de URLs, retry, exportação
Uso ideal Sites estáticos, scripts pequenos, aprender Sites com JavaScript, automações legadas Sites com JavaScript, SPAs, login e cliques Crawling em escala e projetos recorrentes

Quando NÃO Usar BeautifulSoup

❌ Site usa JavaScript pesado Use Selenium ou Playwright em vez disso (veja web scraping com IA, LangChain e Playwright)

❌ Precisa scraping em grande escala O Scrapy foi feito para isso: requisições concorrentes, fila de URLs, retry e exportação já vêm prontos

❌ Site tem API oficial Sempre prefira APIs quando disponíveis

❌ Site bloqueia scraping agressivamente É um sinal de que o dono do site não quer a coleta automatizada: releia os termos de uso e procure uma API ou outra fonte de dados antes de insistir

Casos de Uso Avançados

1. Scraping com Headers Personalizados

Muitos sites respondem de forma diferente conforme o cabeçalho User-Agent. O requests se identifica como python-requests/versão por padrão; o mais transparente é identificar o seu script e deixar um contato:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import requests
from bs4 import BeautifulSoup

# Identifique seu script e diga como falar com você
headers = {
    'User-Agent': 'MeuScraperDeEstudos/1.0 (contato: [email protected])',
    'Accept': 'text/html,application/xhtml+xml',
    'Accept-Language': 'pt-BR,pt;q=0.9',
}

response = requests.get('https://books.toscrape.com/', headers=headers, timeout=10)
response.raise_for_status()
soup = BeautifulSoup(response.content, 'html.parser')
print(response.status_code, len(soup.select('article.product_pod')))  # 200 20

2. Tratamento de Erros Robusto

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import requests
from bs4 import BeautifulSoup
import time

def scrape_com_retry(url, max_tentativas=3):
    """Scraping com retry automático em caso de falha"""
    for tentativa in range(max_tentativas):
        try:
            response = requests.get(url, timeout=10)
            response.raise_for_status()  # Lança HTTPError se o status for 4xx ou 5xx

            soup = BeautifulSoup(response.content, 'html.parser')
            return soup

        except requests.RequestException as e:
            print(f"Tentativa {tentativa + 1} falhou: {e}")
            if tentativa < max_tentativas - 1:
                time.sleep(2 ** tentativa)  # Backoff exponencial
            else:
                raise

Se o try/except ainda é novidade, veja o guia de tratamento de erros e exceções no Python.

3. Extração de Dados Estruturados

O objetivo final quase sempre é uma lista de dicionários, pronta para virar CSV, JSON ou DataFrame. Exemplo com a vitrine do books.toscrape.com, uma loja fictícia feita para praticar:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
from urllib.parse import urljoin

import requests
from bs4 import BeautifulSoup

NOTAS = {'One': 1, 'Two': 2, 'Three': 3, 'Four': 4, 'Five': 5}

def extrair_livros(url):
    """Extrai nome, preço, nota e link de cada livro da página"""
    response = requests.get(url, timeout=10)
    response.raise_for_status()
    soup = BeautifulSoup(response.content, 'html.parser')

    livros = []
    for card in soup.select('article.product_pod'):
        link = card.select_one('h3 a')
        preco = card.select_one('p.price_color').get_text(strip=True)
        classes_nota = card.select_one('p.star-rating')['class']  # ['star-rating', 'Three']
        livros.append({
            'nome': link['title'],
            'preco': float(preco.replace('£', '')),
            'nota': NOTAS[classes_nota[1]],
            'link': urljoin(url, link['href']),
        })
    return livros

livros = extrair_livros('https://books.toscrape.com/')
print(len(livros))
print(livros[0])

Saída:

1
2
20
{'nome': 'A Light in the Attic', 'preco': 51.77, 'nota': 3, 'link': 'https://books.toscrape.com/catalogue/a-light-in-the-attic_1000/index.html'}

Repare que o nome completo do livro veio do atributo title do link (o texto visível é cortado com “…”) e a nota veio de uma classe CSS (star-rating Three). Para salvar o resultado, veja como gravar dados em arquivos CSV com Python ou manipular JSON no Python.

Web scraping vira ferramenta de verdade quando entra num projeto maior - com tratamento de erro, boas práticas e código organizado por trás. É esse tipo de projeto completo que você constrói na Jornada Python:

Boas Práticas

✅ Use parsers adequados

  • html.parser - Built-in, sem dependências
  • lxml - Mais rápido, requer instalação
  • html5lib - Mais tolerante a HTML malformado

✅ Respeite robots.txt

O arquivo robots.txt diz quais caminhos o dono do site pede que robôs não acessem. O Python já traz um leitor pronto, o urllib.robotparser:

1
2
3
4
5
6
7
8
9
import urllib.robotparser

rp = urllib.robotparser.RobotFileParser()
rp.set_url('https://www.python.org/robots.txt')
rp.read()

print(rp.can_fetch('*', 'https://www.python.org/downloads/'))  # True
print(rp.can_fetch('*', 'https://www.python.org/webstats/'))   # False
print(rp.crawl_delay('*'))                                     # None (sem Crawl-delay definido)

✅ Leia os termos de uso e cuidado com dados pessoais

  • Muitos sites proíbem coleta automatizada nos termos de uso; prefira a API oficial quando ela existir.
  • Evite coletar dados pessoais (nomes, e-mails, telefones) sem base legal: no Brasil, a LGPD se aplica a esses dados mesmo quando estão públicos.
  • Não republique conteúdo protegido por direito autoral como se fosse seu.

✅ Implemente rate limiting

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import time

import requests

class RateLimiter:
    def __init__(self, requests_por_segundo=1):
        self.delay = 1.0 / requests_por_segundo
        self.last_request = 0

    def wait(self):
        elapsed = time.time() - self.last_request
        if elapsed < self.delay:
            time.sleep(self.delay - elapsed)
        self.last_request = time.time()

# Uso
urls = [f'https://quotes.toscrape.com/page/{n}/' for n in range(1, 4)]
limiter = RateLimiter(requests_por_segundo=2)
for url in urls:
    limiter.wait()
    response = requests.get(url, timeout=10)
    print(response.status_code, url)

Saída:

1
2
3
200 https://quotes.toscrape.com/page/1/
200 https://quotes.toscrape.com/page/2/
200 https://quotes.toscrape.com/page/3/

Erros comuns

Estes são os erros que mais aparecem em quem está começando com scraping. Todos os tracebacks abaixo foram gerados de verdade (beautifulsoup4 4.15, requests 2.34, Python 3.10).

1. AttributeError: ‘NoneType’ object has no attribute ‘text’

O erro mais comum de todos. find() e select_one() devolvem None quando não encontram nada, e None não tem .text:

1
2
3
4
5
6
from bs4 import BeautifulSoup

html = '<div class="produto"><h2>Teclado</h2></div>'
soup = BeautifulSoup(html, "html.parser")

preco = soup.find("span", class_="preco").text
1
AttributeError: 'NoneType' object has no attribute 'text'

Correção: confira se a tag e a classe existem no HTML que você baixou (não no que o navegador mostra) e trate a ausência:

1
2
3
tag_preco = soup.find("span", class_="preco")
preco = tag_preco.get_text(strip=True) if tag_preco else "sem preço"
print(preco)  # sem preço

2. ModuleNotFoundError ao importar o BeautifulSoup

O pacote se chama beautifulsoup4, mas o módulo é bs4. Os imports abaixo falham:

1
2
from BeautifulSoup import BeautifulSoup   # import da versão 3, que não existe mais
import beautifulsoup4                      # nome do pacote, não do módulo
1
2
ModuleNotFoundError: No module named 'BeautifulSoup'
ModuleNotFoundError: No module named 'beautifulsoup4'

Correção: pip install beautifulsoup4 no mesmo ambiente virtual em que o script roda, e no código from bs4 import BeautifulSoup. Se aparecer No module named 'bs4', o pacote não está instalado no Python que está executando o script.

3. requests.exceptions.HTTPError e ConnectionError

requests.get() não lança erro quando a página responde 404 ou 500: ele devolve a resposta normalmente, e você acaba raspando uma página de erro. Chame raise_for_status() para transformar status 4xx/5xx em exceção:

1
2
3
4
5
import requests

resposta = requests.get("https://books.toscrape.com/pagina-que-nao-existe.html", timeout=10)
print(resposta.status_code)  # 404, mas nenhum erro foi lançado!
resposta.raise_for_status()
1
requests.exceptions.HTTPError: 404 Client Error: Not Found for url: https://books.toscrape.com/pagina-que-nao-existe.html

Já quando o domínio não existe, a internet caiu ou o servidor recusa a conexão, o erro é outro:

1
2
3
import requests

resposta = requests.get("https://site-que-nao-existe.invalid/", timeout=10)
1
requests.exceptions.ConnectionError: HTTPSConnectionPool(host='site-que-nao-existe.invalid', port=443): Max retries exceeded with url: / (Caused by NameResolutionError("HTTPSConnection(host='site-que-nao-existe.invalid', port=443): Failed to resolve 'site-que-nao-existe.invalid' ([Errno -2] Name or service not known)"))

Correção: trate cada caso separadamente:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import requests

def baixar_html(url):
    try:
        resposta = requests.get(url, timeout=10)
        resposta.raise_for_status()
        return resposta.text
    except requests.exceptions.HTTPError as erro:
        print(f"Página respondeu com erro: {erro}")
    except requests.exceptions.ConnectionError:
        print(f"Não foi possível conectar em {url}")
    except requests.exceptions.Timeout:
        print(f"{url} demorou demais para responder")
    return None

baixar_html("https://books.toscrape.com/pagina-que-nao-existe.html")
baixar_html("https://site-que-nao-existe.invalid/")
1
2
Página respondeu com erro: 404 Client Error: Not Found for url: https://books.toscrape.com/pagina-que-nao-existe.html
Não foi possível conectar em https://site-que-nao-existe.invalid/

4. O elemento aparece no navegador, mas o BeautifulSoup não acha (conteúdo carregado por JavaScript)

Aqui não há traceback: a busca simplesmente volta vazia. A página quotes.toscrape.com/js/ mostra as mesmas citações no navegador, mas elas são montadas por JavaScript:

1
2
3
4
5
6
7
8
import requests
from bs4 import BeautifulSoup

resposta = requests.get("https://quotes.toscrape.com/js/", timeout=10)
soup = BeautifulSoup(resposta.text, "html.parser")

print(len(soup.select("div.quote")))       # 0 - as citações não estão no HTML
print("var data" in resposta.text)         # True - os dados vêm num <script>

Correção: primeiro olhe o HTML cru (print(resposta.text) ou “Exibir código-fonte” no navegador, não o painel Elements do DevTools). Muitas vezes os dados estão num JSON dentro de um <script> ou numa API que a página chama (aba Network do DevTools). Neste caso dá para ler o JSON direto:

1
2
3
4
5
6
7
8
9
10
11
import json
import re

import requests

resposta = requests.get("https://quotes.toscrape.com/js/", timeout=10)
bloco = re.search(r"var data = (\[.*?\]);", resposta.text, re.DOTALL).group(1)
citacoes = json.loads(bloco)

print(len(citacoes))                      # 10
print(citacoes[0]["author"]["name"])      # Albert Einstein

Se não houver JSON acessível, use uma ferramenta que controla um navegador de verdade, como Playwright ou Selenium.

5. KeyError ao ler um atributo que não existe

tag["href"] funciona como um dicionário: se o atributo não existir, dá KeyError:

1
2
3
4
from bs4 import BeautifulSoup

soup = BeautifulSoup('<a name="topo">Topo</a>', "html.parser")
print(soup.a["href"])
1
KeyError: 'href'

Correção: use soup.a.get("href"), que devolve None, ou filtre só as tags que têm o atributo: soup.find_all("a", href=True).

Exercícios resolvidos

Todos os exercícios usam HTML embutido no próprio código, então rodam sem internet. Tente resolver antes de abrir a solução.

Exercício 1. Dado o HTML <html><head><title>Loja do Pythonista</title></head><body><h1>Ofertas</h1></body></html>, imprima o conteúdo da tag <title> e o texto do <h1>.

Ver solução
1
2
3
4
5
6
7
from bs4 import BeautifulSoup

html = "<html><head><title>Loja do Pythonista</title></head><body><h1>Ofertas</h1></body></html>"
soup = BeautifulSoup(html, "html.parser")

print(soup.title.string)
print(soup.find("h1").text)

Saída: Loja do Pythonista e Ofertas (uma por linha)

soup.title é um atalho para soup.find("title"), e .string devolve o texto quando a tag tem um único filho de texto.

Exercício 2. Extraia uma lista com os href de todos os links do menu abaixo, ignorando o <a> que não tem href.

1
2
3
4
5
6
<nav>
  <a href="/">Início</a>
  <a href="/blog">Blog</a>
  <a>Sem link</a>
  <a href="/contato">Contato</a>
</nav>
Ver solução
1
2
3
4
5
6
7
8
9
10
11
12
13
14
from bs4 import BeautifulSoup

html = """
<nav>
  <a href="/">Início</a>
  <a href="/blog">Blog</a>
  <a>Sem link</a>
  <a href="/contato">Contato</a>
</nav>
"""
soup = BeautifulSoup(html, "html.parser")

links = [a["href"] for a in soup.find_all("a", href=True)]
print(links)

Saída: ['/', '/blog', '/contato']

O filtro href=True traz só as tags que têm o atributo, então a["href"] nunca dá KeyError.

Exercício 3 (estilo prova). Considere o código:

1
2
3
4
from bs4 import BeautifulSoup

soup = BeautifulSoup('<div><p class="a">Oi</p></div>', "html.parser")
print(soup.find("p", class_="x"))

O que é impresso?

a) []
b) None
c) uma string vazia
d) AttributeError

Ver solução
1
2
3
4
5
from bs4 import BeautifulSoup

soup = BeautifulSoup('<div><p class="a">Oi</p></div>', "html.parser")
print(soup.find("p", class_="x"))
print(soup.find_all("p", class_="x"))

Saída: None e depois []

Resposta: b. find() devolve None quando não acha nada; quem devolve lista vazia é find_all(). O AttributeError só apareceria se você tentasse acessar .text nesse None.

Exercício 4. Some os preços do carrinho abaixo. Atenção ao formato brasileiro (R$ 1.199,90).

1
2
3
4
5
<ul class="carrinho">
  <li><span class="nome">Caderno</span> <span class="preco">R$ 12,50</span></li>
  <li><span class="nome">Caneta</span> <span class="preco">R$ 3,20</span></li>
  <li><span class="nome">Mochila</span> <span class="preco">R$ 1.199,90</span></li>
</ul>
Ver solução
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from bs4 import BeautifulSoup

html = """
<ul class="carrinho">
  <li><span class="nome">Caderno</span> <span class="preco">R$ 12,50</span></li>
  <li><span class="nome">Caneta</span> <span class="preco">R$ 3,20</span></li>
  <li><span class="nome">Mochila</span> <span class="preco">R$ 1.199,90</span></li>
</ul>
"""
soup = BeautifulSoup(html, "html.parser")

def para_float(texto):
    return float(texto.replace("R$", "").replace(".", "").replace(",", ".").strip())

precos = [para_float(s.text) for s in soup.select("ul.carrinho .preco")]
print(precos)
print(f"Total: R$ {sum(precos):.2f}")

Saída: [12.5, 3.2, 1199.9] e Total: R$ 1215.60

A ordem das trocas importa: primeiro remova o ponto de milhar, depois troque a vírgula decimal por ponto. Invertendo, 1.199,90 viraria 1.199.90 e o float() falharia.

Exercício 5 (estilo prova). Dado o HTML e o código abaixo, o que é impresso?

1
2
3
4
5
6
7
8
9
10
11
from bs4 import BeautifulSoup

html = """
<ul id="menu">
  <li><a href="/inicio">Início</a></li>
  <li class="ativo"><a href="/cursos">Cursos</a></li>
  <li><a href="/blog">Blog</a></li>
</ul>
"""
soup = BeautifulSoup(html, "html.parser")
print(soup.select_one("ul#menu li.ativo a")["href"])

a) /inicio
b) /cursos
c) ['/cursos']
d) KeyError

Ver solução

Saída: /cursos

Resposta: b. O seletor lê-se “o <a> dentro do <li> com classe ativo, dentro da <ul> com id menu”. select_one() devolve uma única tag (não uma lista, por isso não é a c), e o <a> encontrado tem href, então não há KeyError.

Exercício 6. Converta a tabela HTML abaixo em uma lista de dicionários, usando o cabeçalho como chaves.

1
2
3
4
5
6
<table id="notas">
  <tr><th>Aluno</th><th>Nota</th></tr>
  <tr><td>Ana</td><td>9.5</td></tr>
  <tr><td>Bruno</td><td>7.0</td></tr>
  <tr><td>Carla</td><td>8.25</td></tr>
</table>
Ver solução
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
from bs4 import BeautifulSoup

html = """
<table id="notas">
  <tr><th>Aluno</th><th>Nota</th></tr>
  <tr><td>Ana</td><td>9.5</td></tr>
  <tr><td>Bruno</td><td>7.0</td></tr>
  <tr><td>Carla</td><td>8.25</td></tr>
</table>
"""
soup = BeautifulSoup(html, "html.parser")

linhas = soup.select("table#notas tr")
cabecalho = [th.get_text(strip=True).lower() for th in linhas[0].find_all("th")]

alunos = []
for linha in linhas[1:]:
    valores = [td.get_text(strip=True) for td in linha.find_all("td")]
    alunos.append(dict(zip(cabecalho, valores)))

print(alunos)

Saída: [{'aluno': 'Ana', 'nota': '9.5'}, {'aluno': 'Bruno', 'nota': '7.0'}, {'aluno': 'Carla', 'nota': '8.25'}]

zip() casa cada nome de coluna com o valor da mesma posição e dict() monta o dicionário. Os valores continuam sendo strings; converta com float() se for fazer contas. Essa lista de dicionários é exatamente o formato que o csv.DictWriter e o json.dump esperam.

Exercício 7. Simule uma paginação sem internet: o dicionário SITE faz o papel do servidor. Comece em /page/1/, colete o texto de todos os p.item e siga o link li.next a até ele não existir mais.

Ver solução
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
from bs4 import BeautifulSoup

# "Site" falso: cada chave é uma URL e cada valor é o HTML da página
SITE = {
    "/page/1/": '<p class="item">A</p><p class="item">B</p><li class="next"><a href="/page/2/">Próxima</a></li>',
    "/page/2/": '<p class="item">C</p><li class="next"><a href="/page/3/">Próxima</a></li>',
    "/page/3/": '<p class="item">D</p><p class="item">E</p>',
}

def baixar(url):
    return SITE[url]  # no mundo real: requests.get(url, timeout=10).text

url = "/page/1/"
itens = []
while url:
    soup = BeautifulSoup(baixar(url), "html.parser")
    itens += [p.text for p in soup.select("p.item")]
    proximo = soup.select_one("li.next a")
    url = proximo["href"] if proximo else None

print(itens)

Saída: ['A', 'B', 'C', 'D', 'E']

É o mesmo padrão da seção de paginação: o laço para quando select_one() devolve None na última página. Isolar o download numa função (baixar) também facilita testar o scraper sem depender do site.

Exercício 8. Imprima nome e preço de cada produto. Quando o preço não existir, mostre indisponível em vez de deixar o programa quebrar.

1
2
3
<div class="produto"><h2>Monitor</h2><span class="preco">R$ 900,00</span></div>
<div class="produto"><h2>Webcam</h2></div>
<div class="produto"><h2>Headset</h2><span class="preco">R$ 250,00</span></div>
Ver solução
1
2
3
4
5
6
7
8
9
10
11
12
13
14
from bs4 import BeautifulSoup

html = """
<div class="produto"><h2>Monitor</h2><span class="preco">R$ 900,00</span></div>
<div class="produto"><h2>Webcam</h2></div>
<div class="produto"><h2>Headset</h2><span class="preco">R$ 250,00</span></div>
"""
soup = BeautifulSoup(html, "html.parser")

for produto in soup.select("div.produto"):
    nome = produto.h2.get_text(strip=True)
    tag_preco = produto.select_one(".preco")
    preco = tag_preco.get_text(strip=True) if tag_preco else "indisponível"
    print(f"{nome}: {preco}")

Saída:

1
2
3
Monitor: R$ 900,00
Webcam: indisponível
Headset: R$ 250,00

Buscar o preço dentro de cada produto (produto.select_one) em vez de no documento inteiro garante que cada nome fique com o seu preço, e o teste if tag_preco evita o AttributeError da seção de erros comuns.

Até a próxima vez, continue programando e se divertindo! 🕷️

Conclusão

Neste guia sobre BeautifulSoup, você aprendeu:

✅ Conceitos fundamentais - Crawler vs Parser
✅ Instalação e setup - BeautifulSoup + requests
✅ Métodos principais - find(), find_all(), select()
✅ Projeto prático - Scraping de citações e paginação
✅ Casos avançados - Headers, retry, dados estruturados
✅ Comparações - BeautifulSoup vs Selenium vs Playwright vs Scrapy
✅ Boas práticas - robots.txt, termos de uso, rate limiting, tratamento de erros

Principais lições:

  • BeautifulSoup é ideal para iniciantes e sites estáticos
  • Use lxml parser para melhor performance
  • Sempre trate erros e implemente retry
  • Respeite rate limits para evitar bloqueios
  • Para JavaScript, use Selenium ou Playwright

Próximos passos:

Perguntas frequentes

O que é web scraping com Python?

Web scraping (raspagem de dados) é a técnica de baixar páginas da web e extrair delas informações de forma automática, como preços, títulos ou links. Em Python, o caminho mais comum é usar a biblioteca requests para baixar o HTML (requests.get(url)) e a biblioteca BeautifulSoup para interpretar esse HTML e localizar os dados com find(), find_all() ou select().

Como instalar e importar o BeautifulSoup no Python?

Instale com pip install beautifulsoup4 (de preferência dentro de um ambiente virtual) e, para baixar páginas, pip install requests. No código, o import é from bs4 import BeautifulSoup. Escrever import beautifulsoup4 ou from BeautifulSoup import BeautifulSoup gera ModuleNotFoundError, porque o pacote se chama beautifulsoup4, mas o módulo importado se chama bs4.

Qual a diferença entre find, find_all e select no BeautifulSoup?

find() devolve o primeiro elemento que casa com a busca, ou None se não achar nada. find_all() devolve uma lista com todos os elementos encontrados (lista vazia se não houver nenhum). select() faz a mesma busca de find_all(), mas usando seletores CSS, como div.produto > span.preco, e select_one() é a versão que devolve só o primeiro resultado ou None.

Como extrair o texto e os atributos de uma tag com BeautifulSoup?

Para o texto, use tag.get_text(strip=True) (ou tag.text, que mantém espaços e quebras de linha). Para atributos, use tag['href'], que lança KeyError se o atributo não existir, ou tag.get('href'), que devolve None nesse caso. O dicionário completo de atributos fica em tag.attrs, e o atributo class sempre vem como lista.

Por que o BeautifulSoup não encontra um elemento que aparece no navegador?

Normalmente porque o conteúdo é montado por JavaScript depois que a página carrega. O requests baixa apenas o HTML inicial e o BeautifulSoup não executa JavaScript, então o elemento não existe no texto que você recebeu. Confira com print(resposta.text): se os dados não estiverem lá, procure a API ou o JSON que a página usa, ou use uma ferramenta que controla um navegador, como Playwright ou Selenium.

Como resolver o erro AttributeError: ‘NoneType’ object has no attribute ‘text’?

Esse erro acontece quando find() ou select_one() não encontra o elemento e devolve None, e você tenta acessar .text nesse None. Confira se a tag, a classe e o seletor estão certos no HTML baixado e trate a ausência antes de ler o texto: tag = soup.find('span', class_='preco') e depois preco = tag.get_text(strip=True) if tag else None.

Web scraping é legal?

Depende do que é coletado, de como e para quê. Em geral, coletar dados públicos em ritmo moderado é uma prática comum, mas é preciso respeitar os termos de uso do site, o arquivo robots.txt, direitos autorais e leis de proteção de dados pessoais, como a LGPD no Brasil. Prefira APIs oficiais quando existirem, não sobrecarregue o servidor e, em caso de dúvida para um uso concreto, consulte um profissional da área jurídica.

Em web scraping com Python, qual biblioteca baixa a página e qual faz o parse do HTML?

A biblioteca requests faz a requisição HTTP e baixa o HTML (requests.get(url).text), e a BeautifulSoup faz o parse, transformando o HTML em uma árvore onde você busca tags com find(), find_all() e select(). O BeautifulSoup sozinho não acessa a internet e não executa JavaScript.

Começe agora sua Jornada na Programação!

Não deixe para amanhã o sucesso que você pode começar a construir hoje!

#newsletter Olá :wave: Curtiu o artigo? Então faça parte da nossa Newsletter! Privacidade Não se preocupe, respeitamos sua privacidade. Você pode se descadastrar a qualquer momento.