Neste módulo, exploraremos como o tornet_scraper funciona. Já aprendeu a extrair dados dos fóruns tornet e clearnet, mas para simplificar este processo, precisamos de um web scraper avançado equipado com funcionalidades para a recolha de dados em grande escala.

Este tópico pode parecer desafiante para alguns, mas na era da IA, em que grande parte da programação pode ser automatizada, e com este curso a aproveitar a IA para a defesa cibernética, construir uma aplicação web para scraping de dados está agora mais acessível.

Todas as aplicações web dependem de uma pilha de tecnologia, uma coleção de ferramentas de software usadas para desenvolver diferentes componentes da aplicação. Por exemplo, sites como o Facebook apresentam uma interface de utilizador (front-end) e uma lógica do lado do servidor (back-end) que torna as páginas acessíveis. Em alguns casos, o front-end e o back-end podem usar a mesma linguagem de programação. Por exemplo, aplicações web baseadas em JavaScript podem usar Vue para o front-end e Nuxt para o back-end, simplificando o desenvolvimento, uma vez que ambos são escritos em JavaScript.

Para os nossos fins, precisamos de uma pilha de tecnologia escalável e compatível com API que possa personalizar de forma independente. Este módulo não aprofundará conceitos de programação, pois pode copiar e colar o código na sua ferramenta de IA preferida para obter uma explicação personalizada que se adapte ao seu estilo de aprendizagem.

Se preferir ignorar este módulo e começar com um projeto funcional, pode fazê-lo, mas perderá a oportunidade de aprender como o projeto funciona, e será apenas mais uma ferramenta. O projeto final está disponível aqui:

https://github.com/CyberMounties/tornet_scraper

Os tópicos desta secção incluem o seguinte:

  1. Como funcionam os modelos
  2. Como funciona o main.py
  3. Como funcionam as bases de dados
  4. Como funcionam as rotas
  5. Como funcionam os scrapers
  6. Como funcionam os serviços

Como funcionam os modelos

As aplicações web modernas utilizam modelos, tais como um modelo padrão ou base, para definir elementos que aparecem em todas as páginas, como um menu de navegação ou uma barra de navegação. Embora seja possível copiar e colar a barra de navegação em todas as páginas, quaisquer alterações exigiriam a atualização de cada página individualmente.

Para simplificar isso, usamos um motor de modelos. Em aplicações web Python, o Jinja2 permite-nos criar um modelo base com blocos de conteúdo para um design consistente e eficiente.

Abra app/templates/base.html:

<!-- app/templates/base.html -->
<!DOCTYPE html>
<html lang="en" data-theme="nord">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>{% block title %}{% endblock %} - Tornet Scraper</title>
    <link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/full.min.css" rel="stylesheet" type="text/css" />
    <script src="https://cdn.tailwindcss.com"></script>
    <script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
</head>
<body class="min-h-screen bg-base-200">
    <!-- Navbar -->
    <div class="navbar bg-base-100 shadow-xl">
        <div class="navbar-start">
            <a class="btn btn-ghost text-xl">Tornet Scraper</a>
        </div>
        <div class="navbar-center hidden lg:flex">
            <ul class="menu menu-horizontal px-1">
                <li><a href="/" class="btn btn-ghost">Dashboard</a></li>
                <li><a href="/proxy-gen" class="btn btn-ghost">Proxy Gen</a></li>
                <li><a href="/manage-api" class="btn btn-ghost">API Management</a></li>
                <li><a href="/bot-profile" class="btn btn-ghost">Bot Profile</a></li>
                <li><a href="/marketplace-scan" class="btn btn-ghost">Marketplace Scan</a></li>
                <li><a href="/posts-scans" class="btn btn-ghost">Posts Scans</a></li>
                <li><a href="/watchlist" class="btn btn-ghost">Watchlist</a></li>
            </ul>
        </div>
        <div class="navbar-end">
            <div class="dropdown dropdown-end lg:hidden">
                <label tabindex="0" class="btn btn-ghost">
                    <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
                        <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />
                    </svg>
                </label>
                <ul tabindex="0" class="menu menu-sm dropdown-content mt-3 p-2 shadow bg-base-100 rounded-box w-52">
                    <li><a href="/">Dashboard</a></li>
                    <li><a href="/proxy-gen">Proxy Gen</a></li>
                    <li><a href="/manage-api">API Management</a></li>
                    <li><a href="/bot-profile">Bot Profile</a></li>
                    <li><a href="/marketplace-scan">Marketplace Scan</a></li>
                    <li><a href="/posts-scans" class="btn btn-ghost">Posts Scans</a></li>
                    <li><a href="/watchlist" class="btn btn-ghost">Watchlist</a></li>
                </ul>
            </div>
        </div>
    </div>

    <!-- Main content -->
    <main class="container mx-auto p-4 mt-6 bg-base-300 rounded-box">
        <!-- Flash Messages Container -->
        <div class="mb-4" id="flash-messages">
            {% if messages %}
                {% for message in messages %}
                    <div class="alert alert-{{ message.category | default('info') }} shadow-lg mb-4 flex justify-between items-center flash-message">
                        <div>
                            <span>{{ message.text }}</span>
                        </div>
                        <button class="btn btn-sm btn-circle btn-ghost" onclick="this.parentElement.remove()">✕</button>
                    </div>
                {% endfor %}
            {% endif %}
        </div>

        {% block content %}
        {% endblock %}
    </main>

    <script>
        // Automatically remove flash messages after 5 seconds
        document.addEventListener('DOMContentLoaded', function() {
            function removeFlashMessages() {
                const flashMessages = document.querySelectorAll('.flash-message');
                flashMessages.forEach(function(message) {
                    // Skip if already fading
                    if (message.style.opacity === '0') return;
                    setTimeout(function() {
                        message.style.transition = 'opacity 0.5s ease';
                        message.style.opacity = '0';
                        setTimeout(function() {
                            message.remove();
                        }, 500);
                    }, 5000);
                });
            }
            removeFlashMessages();
            const flashContainer = document.querySelector('#flash-messages');
            if (flashContainer) {
                const observer = new MutationObserver(function() {
                    removeFlashMessages();
                });
                observer.observe(flashContainer, { childList: true });
            }
        });
    </script>
</body>
</html>

O modelo base.html é um modelo base Jinja2 para fornecer um layout consistente para todas as páginas. Inclui uma barra de navegação, mensagens flash e espaços reservados para título e conteúdo que os modelos filhos podem substituir. Abaixo está uma explicação técnica de como funciona e o seu mecanismo de extensão.

  1. Estrutura HTML:

    • Define um documento HTML5 padrão com um atributo data-theme=“nord” para o estilo DaisyUI.
    • Inclui recursos externos: CSS DaisyUI, CSS Tailwind e jQuery para estilo e interatividade.
  2. Bloco de título:

    • A tag <title> contém um bloco Jinja2 {% block title %}{% endblock %} - Tornet Scraper.
    • Os modelos filhos substituem o bloco title para definir um título específico da página, que é acrescentado com “ - Tornet Scraper”.
    • Exemplo: Um modelo filho com {% block title %}Dashboard{% endblock %} resulta em <title>Dashboard - Tornet Scraper</title>.
  3. Barra de navegação:

    • Uma barra de navegação responsiva com um logótipo (“Tornet Scraper”) e links de navegação (Painel, Proxy Gen, etc.).
    • Em ecrãs grandes (lg:flex), os links são apresentados horizontalmente; em ecrãs mais pequenos, um menu suspenso (ativado por um ícone de hambúrguer) mostra os links verticalmente.
  4. Mensagens flash:

    • Um <div id="flash-messages"> apresenta mensagens enviadas a partir do backend (por exemplo, notificações de sucesso ou erro).
    • Usa Jinja2: {% if messages %} percorre messages (uma lista de objetos com text e category).
    • Cada mensagem é estilizada como um alert DaisyUI com uma classe dinâmica (alert-{{ message.category }}, padrão para info).
    • Um botão Fechar () remove a mensagem ao clicar.
    • O JavaScript desvanece e remove automaticamente as mensagens flash após 5 segundos usando uma transição CSS (opacity).
    • Um MutationObserver garante que novas mensagens flash (adicionadas dinamicamente) também sejam removidas automaticamente.
  5. Bloco de conteúdo:

    • A secção <main> contém um espaço reservado {% block content %}{% endblock %}.
    • Os modelos filhos substituem este bloco para inserir conteúdo específico da página no contentor principal, estilizado com classes Tailwind/DaisyUI (container, mx-auto, etc.).
  6. Extensão em modelos filhos:

    • Os modelos filhos usam {% extends ‘base.html’ %} para herdar a estrutura base.
    • Eles substituem {% block title %} para o título da página e {% block content %} para o conteúdo principal.
    • Isso renderiza o layout completo base.html com “Painel” no título e o conteúdo especificado na seção <main>.

Esta configuração garante uma interface de utilizador consistente, títulos dinâmicos, navegação reutilizável e tratamento de mensagens flash intuitivo em todas as páginas.

dashboard.html

O ficheiro localizado em app/templates/dashboard.html fornece um exemplo claro de como ele estende base.html. Embora este ficheiro não seja estritamente necessário, criei o hábito de incluí-lo. Abaixo está o código contido nele:

<!-- app/templates/dashboard.html -->
{% extends "base.html" %}
{% block title %}Dashboard{% endblock %}

{% block content %}
<h1 class="text-3xl font-bold text-base-content mb-4">Dashboard</h1>
{% endblock %}

Repare como ele estende base.html? O conteúdo dentro de {% block content %} é inserido no contentor de conteúdo principal definido em base.html:

    <!-- Main content -->
    <main class="container mx-auto p-4 mt-6 bg-base-300 rounded-box">
        {% block content %}
        {% endblock %}
    </main>

O título também é definido:

{% block title %}Dashboard{% endblock %}

Em base.html, a estrutura do título é:

<title>{% block title %}{% endblock %} - Tornet Scraper</title>

Ao visualizar o código-fonte da página do painel, o título aparece como:

<title>Dashboard - Tornet Scraper</title>

Em vez de reescrever o título para cada página, pode defini-lo em base.html e, em seguida, definir apenas o nome da página para todas as outras páginas.


Como funciona o main.py

Pode encontrar o main.py em app/main.py.

O ficheiro main.py é o núcleo de uma aplicação web FastAPI, definindo rotas, modelos e interações com a base de dados para uma interface de web scraper. Abaixo está uma explicação técnica das variáveis globais, routers incluídos e funções de tratamento de rotas.

Variáveis globais

  1. app:

    • Finalidade: A instância principal da aplicação FastAPI.
    • Detalhes: Inicializa a estrutura FastAPI para lidar com pedidos e respostas HTTP.
  2. BASE_DIR:

    • Finalidade: Armazena o caminho absoluto do diretório raiz do projeto.
    • Detalhes: Derivado usando os.path para localizar o diretório app/templates para modelos Jinja2.
  3. TEMPLATES_DIR:

    • Objetivo: Especifica o diretório para modelos Jinja2.
    • Detalhes: definido como app/templates dentro da raiz do projeto para renderizar modelos HTML.
  4. templates:

    • Finalidade: instância do motor de modelos Jinja2.
    • Detalhes: configurado para carregar modelos de TEMPLATES_DIR para renderizar HTML dinâmico.

Roteadores incluídos

  1. proxy_gen_router:

    • Finalidade: Regista rotas para a funcionalidade de geração de proxy.
    • Detalhes: Importado de app.routes.proxy_gen, trata de pontos finais relacionados com proxy.
  2. manage_api_router:

    • Finalidade: Regista rotas para gestão de API.
    • Detalhes: Importado de app.routes.manage_api, gere chaves e configurações da API.
  3. bot_profile_router:

    • Finalidade: Regista rotas para gestão de perfis de bots.
    • Detalhes: Importado de app.routes.bot_profile, trata da configuração dos bots.
  4. marketplace_api_router:

    • Finalidade: Regista rotas para a verificação do marketplace.
    • Detalhes: Importado de app.routes.marketplace, gere a recuperação de dados do marketplace.
  5. posts_api_router:

    • Finalidade: Regista rotas para a verificação de publicações.
    • Detalhes: Importado de app.routes.posts, trata das operações de dados relacionadas com publicações.
  6. watchlist_api_router:

    • Finalidade: Regista rotas para a gestão da lista de observação.
    • Detalhes: Importado de app.routes.watchlist, gere itens da lista de observação e digitalizações.

Funções do manipulador de rotas

  1. dashboard:

    • Objetivo: Renderiza a página do painel.
    • Parâmetros principais:
      • request: Objeto Request do FastAPI para contexto de modelo.
      • db: SQLAlchemy Session para acesso ao banco de dados (via Depends(get_db)).
    • Retorna: TemplateResponse renderizando dashboard.html com mensagens exibidas da sessão.
  2. proxy_gen:

    • Objetivo: Exibe a página de geração de proxies com uma lista de proxies.
    • Parâmetros-chave:
      • request: objeto FastAPI Request.
      • db: SQLAlchemy Session para consultar proxies.
    • Retorna: TemplateResponse renderizando proxy_gen.html com dados do proxy (nome do contêiner, IP, nó de saída do Tor, carimbo de data/hora, status de execução) e mensagens em flash.
  3. manage_api:

    • Objetivo: Mostra a página de gerenciamento da API com detalhes da API.
    • Parâmetros principais:
      • request: objeto Request do FastAPI.
      • db: Session do SQLAlchemy para consultar APIs.
    • Retorna: TemplateResponse renderizando manage_api.html com dados da API (ID, nome, provedor, chave, modelo, tokens máximos, prompt, carimbo de data/hora, status ativo) e mensagens instantâneas.
  4. bot_profile:

    • Objetivo: Exibe a página de gerenciamento do perfil do bot.
    • Parâmetros-chave:
      • request: objeto FastAPI Request.
      • db: SQLAlchemy Session para consultar perfis de bots e o URL onion mais recente.
    • Retorna: TemplateResponse renderizando bot_profile.html com dados do perfil do bot (ID, nome de utilizador, palavra-passe mascarada, finalidade, proxy Tor, carimbo de data/hora), URL onion mais recente e mensagens exibidas.
  5. marketplace:

    • Objetivo: Renderiza a página de varredura do marketplace com paginação e dados da varredura de publicações.
    • Parâmetros principais:
      • request: objeto Request do FastAPI.
      • db: Session do SQLAlchemy para consultar varreduras.
    • Retorna: TemplateResponse renderizando marketplace.html com varreduras paginadas, varreduras de postagem e mensagens exibidas.
  6. posts_scans:

    • Finalidade: Exibe a página de visão geral das varreduras de postagem.
    • Parâmetros principais:
      • request: objeto FastAPI Request.
      • db: SQLAlchemy Session para acesso ao banco de dados.
    • Retorna: TemplateResponse renderizando posts_scans.html com mensagens instantâneas.
  7. posts_scan_result:

    • Objetivo: Mostra os resultados de uma verificação de postagem específica.
    • Parâmetros principais:
      • scan_id: ID inteiro da verificação.
      • request: objeto FastAPI Request.
      • db: SQLAlchemy Session para acesso ao banco de dados.
      • name: nome opcional da verificação (string, padrão vazio).
    • Retorna: TemplateResponse renderizando posts_scan_result.html com ID da verificação, nome e mensagens exibidas.
  8. watchlist:

    • Objetivo: Renderiza a página de visão geral da lista de observação.
    • Parâmetros principais:
      • request: objeto FastAPI Request.
      • db: SQLAlchemy Session para acesso ao banco de dados.
    • Retorna: TemplateResponse renderizando watchlist.html com mensagens exibidas.
  9. watchlist_profile:

    • Objetivo: Exibe o perfil de um item específico da lista de observação e as varreduras associadas.
    • Parâmetros principais:
      • target_id: ID inteiro do item da lista de observação.
      • request: objeto FastAPI Request.
      • db: SQLAlchemy Session para consultar o item da lista de observação e as varreduras.
      • Retorna: TemplateResponse renderizando watchlist_profile.html com o item da lista de observação, os dados da varredura e as mensagens exibidas; gera HTTPException (404 para item ausente, 500 para erros do servidor).

Como funcionam as bases de dados

Na nossa aplicação FastAPI, db.py gere as configurações do motor da base de dados, enquanto models.py define o esquema e as tabelas da base de dados. A utilização de um ficheiro db.py separado permite alternar facilmente entre tipos de bases de dados. Embora tornet_scraper utilize SQLite3 para prototipagem e testes, não é ideal para a recolha de dados em grande escala. Para ambientes de produção, recomendamos a transição para um banco de dados mais robusto, como o PostgreSQL.

Você também pode implementar lógica em db.py para alternar dinamicamente entre o SQLite3 e um servidor de banco de dados com base em condições ou no ambiente (por exemplo, desenvolvimento, teste ou produção).

db.py

O ficheiro db.py configura a ligação à base de dados e a gestão de sessões para uma aplicação FastAPI utilizando SQLAlchemy. Abaixo encontra-se uma explicação concisa dos seus componentes e funcionalidades.

Pode encontrar db.py em app/database/db.py.

  1. Finalidade:
  • Configura uma ligação à base de dados SQLite, cria tabelas e fornece uma dependência para sessões de base de dados em rotas FastAPI.
  1. Variáveis globais:

    • BASE_DIR:
      • Objetivo: Define o diretório raiz do projeto.
      • Detalhes: Usa Path(__file__).resolve().parent.parent.parent para calcular o caminho absoluto para a raiz do projeto, garantindo que o ficheiro da base de dados esteja localizado corretamente.
    • DATABASE_URL:
      • Finalidade: Especifica a string de conexão com o banco de dados.
      • Detalhes: Configurado para SQLite como sqlite:///{BASE_DIR}/tornet_scraper.db, apontando para um ficheiro tornet_scraper.db na raiz do projeto.
    • engine:
      • Finalidade: motor SQLAlchemy para interações com a base de dados.
      • Detalhes: criado com create_engine(DATABASE_URL, connect_args={“check_same_thread”: False}) para lidar com ligações SQLite. O argumento check_same_thread permite que o SQLite seja usado num ambiente FastAPI multithread.
    • SessionLocal:
      • Finalidade: Fábrica para criar sessões de base de dados.
      • Detalhes: Configurado com sessionmaker(autocommit=False, autoflush=False, bind=engine) para criar sessões vinculadas ao motor, com controlo manual de commit e flush.
  2. Funções:

    • get_db:
      • Finalidade: Fornece uma sessão de base de dados para rotas FastAPI.
      • Parâmetros-chave: Nenhum (função de dependência).
      • Retorna: Produz uma sessão SQLAlchemy (db) e garante que ela seja fechada após o uso.
      • Detalhes: Usado como uma dependência FastAPI (via Depends(get_db)) para injetar uma sessão em manipuladores de rota. O bloco try/finally garante que a sessão seja encerrada, evitando fugas de recursos.
    • init_db:
      • Objetivo: Inicializa a base de dados criando tabelas.
      • Parâmetros-chave: Nenhum.
      • Retorna: Nenhum.
      • Detalhes: Importa Base de models.py e chama Base.metadata.create_all(bind=engine) para criar todas as tabelas definidas (por exemplo, proxies, apis) no banco de dados SQLite, caso elas não existam.
  3. Utilização:

    • Objetivo: Habilita operações de banco de dados na aplicação.
    • Detalhes: init_db é chamado em main.py para configurar o esquema do banco de dados na inicialização. get_db é usado em manipuladores de rotas (por exemplo, /proxy-gen, /manage-api) para consultar ou modificar dados em tabelas definidas em models.py. A sessão garante a integridade transacional e a limpeza adequada dos recursos.

models.py

O ficheiro models.py define o esquema da base de dados para uma aplicação FastAPI utilizando SQLAlchemy, especificando tabelas, colunas e relações. Abaixo está uma explicação concisa dos seus componentes e funcionalidades.

Pode encontrar models.py em app/database/models.py.

  1. Visão geral do SQLAlchemy:

    • Objetivo: O SQLAlchemy é uma biblioteca ORM (mapeamento objeto-relacional) para Python, que permite a interação com um banco de dados relacional usando objetos Python.
    • Detalhes: Mapeia classes Python para tabelas de banco de dados, permitindo operações CRUD por meio de sintaxe orientada a objetos. Usa declarative_base para criar uma classe base (Base) para definições de modelo.
  2. Colunas:

    • Objetivo: Representam campos em uma tabela de banco de dados.
    • Detalhes: Definidas usando a classe Column do SQLAlchemy, especificando atributos como tipo de dados (por exemplo, Integer, String, DateTime), restrições (por exemplo, primary_key, unique, nullable) e padrões (por exemplo, datetime.utcnow). As colunas armazenam dados para cada registo numa tabela.
  3. Tabelas:

    • Objetivo: Representar tabelas de bases de dados que armazenam dados estruturados.
    • Detalhes: Cada classe (por exemplo, Proxy, APIs) herda de Base e define uma tabela através de __tablename__. As tabelas incluem colunas e restrições opcionais (por exemplo, UniqueConstraint para combinações únicas de campos). Exemplos incluem proxies, apis, bot_profiles, etc.
  4. Relações:

    • Finalidade: Definem associações entre tabelas para consultas relacionais.
    • Detalhes: Estabelecidas usando colunas ForeignKey (por exemplo, MarketplacePost.scan_id refere-se a marketplace_post_scans.id). As relações permitem unir tabelas, como ligar MarketplacePost a MarketplacePostScan através de scan_id. O SQLAlchemy trata estas como referências de objetos em consultas.
  5. Enums:

    • Objetivo: Definir vocabulários controlados para campos específicos.
    • Detalhes: usa enum.Enum do Python (por exemplo, BotPurpose, ScanStatus) para restringir os valores das colunas a opções predefinidas (por exemplo, SCRAPE_MARKETPLACE, RUNNING). Armazenado como strings no banco de dados por meio de Enum.
  6. Componentes principais:

    • Base: A classe base do SQLAlchemy para todos os modelos, usada para gerar esquemas de tabelas.
    • Definições de tabelas: Classes como Proxy, APIs, BotProfile, etc., definem tabelas para armazenar detalhes de proxy, configurações de API, perfis de bot, URLs de cebola, varreduras de marketplace, publicações e listas de observação.
    • Restrições: Restrições exclusivas (por exemplo, UniqueConstraint em MarketplacePost) garantem a integridade dos dados, impedindo registros duplicados com base em combinações específicas de colunas.
    • Carimbos de data/hora: Muitas tabelas incluem uma coluna timestamp (padrão datetime.utcnow) para rastrear os horários de criação/atualização dos registros.
  7. Utilização:

    • Objetivo: Os modelos são usados pela aplicação FastAPI para interagir com a base de dados.
    • Detalhes: As rotas main.py consultam esses modelos (por meio de sessões SQLAlchemy) para recuperar ou armazenar dados, que são então passados para modelos Jinja2 para renderização ou processados na lógica da API. Por exemplo, os dados Proxy são consultados na rota /proxy-gen para exibir detalhes do proxy.

Como funcionam as rotas

No desenvolvimento de API, é fundamental organizar os pontos finais da API de forma eficaz, pois não é prático encaminhar todas as solicitações através de um único ponto final.

Por exemplo, o formato /api/posts/get-post indica que /api/posts/* é dedicado a operações relacionadas com publicações, tais como visualizar, eliminar ou modificar publicações. Todas as chamadas API relacionadas são prefixadas com /api/posts/.

Dado que a nossa aplicação inclui funcionalidades como geração de proxy, gestão de bots, scraping de marketplace, scraping de publicações, monitorização de ameaças e muito mais, é essencial organizar os pontos finais de forma adequada.

Pode encontrar todas as rotas em app/routes/*.py, que contém seis ficheiros Python. Abaixo está um exemplo de bot_profile.py:

# app/routes/bot_profile.py
import logging
from fastapi import APIRouter, Depends, HTTPException, Request
from sqlalchemy.orm import Session
from pydantic import BaseModel
from app.database.models import BotProfile, OnionUrl, BotPurpose, APIs
from app.database.db import get_db
from typing import Optional
from app.services.tornet_forum_login import login_to_tor_website
from app.services.gen_random_ua import gen_desktop_ua


logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)


bot_profile_router = APIRouter(prefix="/api/bot-profile", tags=["API", "Bot Profile Management"])


# Pydantic models for validation
class BotProfileCreate(BaseModel):
    username: str
    password: str
    purpose: str
    tor_proxy: Optional[str] = None
    session: Optional[str] = None


class BotProfileUpdate(BaseModel):
    username: Optional[str] = None
    password: Optional[str] = None
    purpose: Optional[str] = None
    tor_proxy: Optional[str] = None
    user_agent: Optional[str] = None
    session: Optional[str] = None


class OnionUrlCreate(BaseModel):
    url: str


# Get all bot profiles
@bot_profile_router.get("/list")
async def get_bot_profiles(db: Session = Depends(get_db)):
    try:
        profiles = db.query(BotProfile).all()
        return [
            {
                "id": p.id,
                "username": p.username,
                "password": "********",
                "actual_password": p.password,
                "purpose": p.purpose.value,
                "tor_proxy": p.tor_proxy,
                "has_session": bool(p.session and len(p.session) > 0),
                "session": p.session,
                "user_agent": p.user_agent,
                "timestamp": p.timestamp.isoformat()
            } for p in profiles
        ]
    except Exception as e:
        logger.error(f"Error fetching bot profiles: {str(e)}")
        raise HTTPException(status_code=500, detail="Internal server error")

Utilizamos o registo para rastrear eventos dentro da aplicação. O registo é fundamental para compreender o que está a acontecer na sua aplicação web; sem ele, estará a operar às cegas. Embora os registos possam ser guardados num ficheiro, normalmente evito fazê-lo durante o desenvolvimento local, mas recomendo-o para ambientes de produção.

No entanto, o aspeto mais crítico é a criação de routers:

bot_profile_router = APIRouter(prefix="/api/bot-profile", tags=["API", "Bot Profile Management"])

Este router estabelece um prefixo de ponto final, /api/bot-profile, que todos os pontos finais relacionados irão utilizar.

Por exemplo, a função seguinte recupera todos os perfis de bot da tabela BotProfile:

@bot_profile_router.get("/list")
async def get_bot_profiles(db: Session = Depends(get_db)):

Quando utiliza @bot_profile_router.get(“/list”), o ponto final é prefixado com /api/bot-profile, resultando em /api/bot-profile/list.

Esta estrutura permite uma organização eficiente de centenas ou milhares de rotas API. O bot_profile_router é importado para main.py e registado.


Como funcionam os scrapers

Todos os scrapers são armazenados em app/scrapers/*.py.

Esses ficheiros são normalmente importados como módulos em outras partes da base de código. Por exemplo, dentro de marketplace_scraper.py, a função create_pagination_batches recebe como entrada uma URL de página web, como:

http://site.url/pagination?page=1

Ele cria um lote de paginações como este:

http://site.url/pagination?page=1
http://site.url/pagination?page=2
http://site.url/pagination?page=3
--- snip ---
http://site.url/pagination?page=10

Esta abordagem é eficaz para a distribuição de tarefas, como atribuir cada lote de 10 páginas de paginação a um único bot para scraping.


Como funcionam os serviços

Todos os serviços estão localizados em app/services/*.py.

Os serviços também são usados como módulos, mas têm uma finalidade distinta, lidando com tarefas como:

  1. Gerenciar o Docker para proxies
  2. Gerar proxies
  3. Efetuar logins
  4. Ignorar CAPTCHAs

Como aprenderá mais adiante, usamos contentores Docker para criar proxies Tor localmente. Quando um proxy não é mais necessário, ele deve ser excluído. Para verificar se os proxies estão em execução, pode usar uma função como container_running de container_status.py para verificar o estado do contentor Docker.