Skip to content

Scraper para Excel #15

Description

@AschRJ

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":

executar_scraper()

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions