import requests
from bs4 import BeautifulSoup
import pandas as pd
import re
import time
from urllib.parse import urljoin
from datetime import datetime
============================================================
CONFIGURAÇÕES
============================================================
SITE = "https://www.roendolivros.com.br/"
ARQUIVO_SAIDA = "roendo_livros.xlsx"
Quantidade máxima de páginas de resultados.
Coloque None para tentar percorrer todas.
MAX_PAGINAS = None
Tempo entre requisições para evitar sobrecarregar o site
INTERVALO = 3
HEADERS = {
"User-Agent": (
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
"AppleWebKit/537.36 (KHTML, like Gecko) "
"Chrome/140.0 Safari/537.36"
),
"Accept-Language": "pt-BR,pt;q=0.9,en;q=0.8",
}
============================================================
SESSÃO HTTP
============================================================
session = requests.Session()
session.headers.update(HEADERS)
============================================================
FUNÇÃO PARA BAIXAR UMA PÁGINA
============================================================
def baixar(url, tentativas=3):
for tentativa in range(1, tentativas + 1):
try:
print(f"Baixando: {url}")
resposta = session.get(
url,
timeout=30
)
# Site bloqueando muitas requisições
if resposta.status_code == 429:
espera = 10 * tentativa
print(
f"HTTP 429 - aguardando {espera} segundos..."
)
time.sleep(espera)
continue
resposta.raise_for_status()
resposta.encoding = resposta.apparent_encoding
time.sleep(INTERVALO)
return resposta.text
except requests.RequestException as erro:
print(
f"Erro na tentativa {tentativa}: {erro}"
)
if tentativa < tentativas:
time.sleep(10 * tentativa)
return None
============================================================
LIMPA TEXTO
============================================================
def limpar_texto(texto):
if not texto:
return ""
texto = texto.replace("\xa0", " ")
texto = re.sub(
r"\s+",
" ",
texto
)
return texto.strip()
============================================================
EXTRAI CAMPO DO LIVRO
============================================================
def extrair_campo(texto, nomes):
"""
Procura informações como:
Título: Livro X
Autor: Fulano
Autora: Fulana
Páginas: 300
Editora: Intrínseca
"""
for nome in nomes:
padrao = rf"{nome}\s*:\s*(.+?)(?=\s+(?:Título Original|Título|Autor|Autora|Páginas|Tradutor|Tradutora|Editora)\s*:|$)"
resultado = re.search(
padrao,
texto,
flags=re.IGNORECASE
)
if resultado:
valor = limpar_texto(
resultado.group(1)
)
if valor:
return valor
return ""
============================================================
EXTRAI DADOS DE UMA RESENHA
============================================================
def extrair_post(url):
html = baixar(url)
if not html:
return None
soup = BeautifulSoup(
html,
"html.parser"
)
dados = {}
dados["URL"] = url
# --------------------------------------------------------
# TÍTULO DO POST
# --------------------------------------------------------
titulo = ""
# Blogger normalmente utiliza h1
h1 = soup.find("h1")
if h1:
titulo = limpar_texto(
h1.get_text(" ", strip=True)
)
# Fallback
if not titulo:
meta = soup.find(
"meta",
property="og:title"
)
if meta:
titulo = meta.get("content", "")
dados["Título do post"] = titulo
# --------------------------------------------------------
# DATA
# --------------------------------------------------------
data = ""
meta_data = soup.find(
"meta",
property="article:published_time"
)
if meta_data:
data = meta_data.get("content", "")
if not data:
elemento_data = soup.find(
class_=re.compile(
r"(date|published|timestamp)",
re.I
)
)
if elemento_data:
data = limpar_texto(
elemento_data.get_text(
" ",
strip=True
)
)
dados["Data"] = data
# --------------------------------------------------------
# CONTEÚDO DO POST
# --------------------------------------------------------
post = (
soup.find(
class_=re.compile(
r"(post-body|entry-content|post-content)",
re.I
)
)
or soup.find("article")
)
if post:
texto = post.get_text(
"\n",
strip=True
)
else:
texto = soup.get_text(
"\n",
strip=True
)
texto_limpo = limpar_texto(texto)
dados["Texto da resenha"] = texto_limpo
# --------------------------------------------------------
# CAMPOS DO LIVRO
# --------------------------------------------------------
dados["Título do livro"] = extrair_campo(
texto_limpo,
["Título"]
)
dados["Título original"] = extrair_campo(
texto_limpo,
["Título Original"]
)
dados["Autor"] = extrair_campo(
texto_limpo,
["Autor", "Autora", "Autores", "Autoras"]
)
dados["Páginas"] = extrair_campo(
texto_limpo,
["Páginas", "Total de páginas"]
)
dados["Tradutor"] = extrair_campo(
texto_limpo,
["Tradutor", "Tradutora", "Tradutores", "Tradutoras"]
)
dados["Editora"] = extrair_campo(
texto_limpo,
["Editora"]
)
# --------------------------------------------------------
# LABELS / CATEGORIAS
# --------------------------------------------------------
labels = []
for elemento in soup.find_all(
class_=re.compile(
r"(label|category|post-labels)",
re.I
)
):
texto_label = limpar_texto(
elemento.get_text(
" ",
strip=True
)
)
if texto_label:
labels.append(texto_label)
dados["Categorias"] = " | ".join(
dict.fromkeys(labels)
)
# --------------------------------------------------------
# IMAGENS
# --------------------------------------------------------
imagens = []
if post:
for img in post.find_all(
"img",
src=True
):
imagem = urljoin(
url,
img["src"]
)
if imagem not in imagens:
imagens.append(imagem)
dados["Imagens"] = " | ".join(
imagens
)
return dados
============================================================
EXTRAI OS LINKS DE POSTS DE UMA PÁGINA
============================================================
def extrair_links_posts(url):
html = baixar(url)
if not html:
return [], None
soup = BeautifulSoup(
html,
"html.parser"
)
links = []
# --------------------------------------------------------
# Procura links dentro dos containers de posts
# --------------------------------------------------------
containers = soup.find_all(
class_=re.compile(
r"(post|blog-post|hentry)",
re.I
)
)
for container in containers:
# procura h2/h3/h1
titulo = container.find(
["h1", "h2", "h3"]
)
if titulo:
link = titulo.find(
"a",
href=True
)
if link:
href = urljoin(
url,
link["href"]
)
if href.startswith(SITE) and href not in links:
links.append(href)
# --------------------------------------------------------
# FALLBACK
# Caso a estrutura acima não encontre posts
# --------------------------------------------------------
if not links:
for link in soup.find_all(
"a",
href=True
):
href = urljoin(
url,
link["href"]
)
# Posts do Blogger normalmente possuem
# /ano/mes/nome-do-post.html
if re.search(
r"/20\d{2}/\d{2}/.+\.html$",
href
):
if href not in links:
links.append(href)
# --------------------------------------------------------
# LOCALIZA PRÓXIMA PÁGINA
# --------------------------------------------------------
proxima = None
# Procura pelo link "Postagens mais antigas"
textos = [
"postagens mais antigas",
"posts mais antigos",
"older posts",
"older",
"mais antigas",
"próxima"
]
for link in soup.find_all(
"a",
href=True
):
texto = limpar_texto(
link.get_text(
" ",
strip=True
)
).lower()
if any(
palavra in texto
for palavra in textos
):
proxima = urljoin(
url,
link["href"]
)
break
return links, proxima
============================================================
CRAWLER PRINCIPAL
============================================================
def executar_scraper():
todos_posts = []
pagina_atual = SITE
pagina_numero = 1
paginas_visitadas = set()
posts_visitados = set()
while pagina_atual:
# ----------------------------------------------------
# Limite de páginas
# ----------------------------------------------------
if MAX_PAGINAS is not None:
if pagina_numero > MAX_PAGINAS:
print(
"Limite de páginas atingido."
)
break
# ----------------------------------------------------
# Evita loop
# ----------------------------------------------------
if pagina_atual in paginas_visitadas:
print(
"Página já visitada. Encerrando."
)
break
paginas_visitadas.add(
pagina_atual
)
print()
print("=" * 60)
print(
f"PÁGINA DE RESULTADOS: {pagina_numero}"
)
print(pagina_atual)
print("=" * 60)
# ----------------------------------------------------
# Busca os posts
# ----------------------------------------------------
links, proxima_pagina = (
extrair_links_posts(
pagina_atual
)
)
print(
f"Posts encontrados: {len(links)}"
)
# ----------------------------------------------------
# Visita cada post
# ----------------------------------------------------
for contador, link in enumerate(
links,
start=1
):
if link in posts_visitados:
continue
posts_visitados.add(
link
)
print(
f"[{contador}/{len(links)}] "
f"Processando post..."
)
dados = extrair_post(
link
)
if dados:
dados["Página de resultados"] = (
pagina_numero
)
todos_posts.append(
dados
)
# ----------------------------------------------------
# Próxima página
# ----------------------------------------------------
pagina_atual = proxima_pagina
pagina_numero += 1
# ========================================================
# SALVA EXCEL
# ========================================================
if not todos_posts:
print(
"Nenhum post foi encontrado."
)
return
df = pd.DataFrame(
todos_posts
)
# Ordem das colunas
colunas = [
"Página de resultados",
"Data",
"Título do post",
"Título do livro",
"Título original",
"Autor",
"Páginas",
"Tradutor",
"Editora",
"Categorias",
"URL",
"Imagens",
"Texto da resenha"
]
colunas_existentes = [
coluna
for coluna in colunas
if coluna in df.columns
]
df = df[
colunas_existentes
]
# Remove duplicados
df = df.drop_duplicates(
subset=["URL"]
)
# Salva Excel
df.to_excel(
ARQUIVO_SAIDA,
index=False,
engine="openpyxl"
)
print()
print("=" * 60)
print("SCRAPING FINALIZADO")
print("=" * 60)
print(
f"Posts coletados: {len(df)}"
)
print(
f"Arquivo: {ARQUIVO_SAIDA}"
)
============================================================
EXECUÇÃO
============================================================
if name == "main":
import requests
from bs4 import BeautifulSoup
import pandas as pd
import re
import time
from urllib.parse import urljoin
from datetime import datetime
============================================================
CONFIGURAÇÕES
============================================================
SITE = "https://www.roendolivros.com.br/"
ARQUIVO_SAIDA = "roendo_livros.xlsx"
Quantidade máxima de páginas de resultados.
Coloque None para tentar percorrer todas.
MAX_PAGINAS = None
Tempo entre requisições para evitar sobrecarregar o site
INTERVALO = 3
HEADERS = {
"User-Agent": (
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
"AppleWebKit/537.36 (KHTML, like Gecko) "
"Chrome/140.0 Safari/537.36"
),
"Accept-Language": "pt-BR,pt;q=0.9,en;q=0.8",
}
============================================================
SESSÃO HTTP
============================================================
session = requests.Session()
session.headers.update(HEADERS)
============================================================
FUNÇÃO PARA BAIXAR UMA PÁGINA
============================================================
def baixar(url, tentativas=3):
============================================================
LIMPA TEXTO
============================================================
def limpar_texto(texto):
============================================================
EXTRAI CAMPO DO LIVRO
============================================================
def extrair_campo(texto, nomes):
============================================================
EXTRAI DADOS DE UMA RESENHA
============================================================
def extrair_post(url):
============================================================
EXTRAI OS LINKS DE POSTS DE UMA PÁGINA
============================================================
def extrair_links_posts(url):
============================================================
CRAWLER PRINCIPAL
============================================================
def executar_scraper():
============================================================
EXECUÇÃO
============================================================
if name == "main":